| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { mkdtemp, rm } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
| import { DatabaseSync } from 'node:sqlite'; |
| import { describe, it } from 'node:test'; |
| import { DEFAULT_TOOL_MODE } from '@maka/core/tool-mode'; |
| import type { RuntimeEvent } from '@maka/core/runtime-event'; |
| import { encodeCanonicalRuntimeEvent } from '@maka/core/canonical-runtime-event'; |
| import { RunSealedError } from '@maka/core/runtime-event-store'; |
| import { buildInvocationOpenedEvent } from '@maka/core/runtime-invocation'; |
| import { readLogicalRuntimeExecution } from '@maka/core/runtime-logical-execution'; |
| import { |
| RuntimeTranscriptOversizedTurnError, |
| RuntimeTranscriptQuery, |
| } from '../runtime-transcript-query.js'; |
| import type { RuntimeInvocationRecord } from '@maka/core/runtime-invocation'; |
| import { |
| acquireOperationalStateDatabase, |
| resolveOperationalStateDatabasePath, |
| } from '../operational-state-store.js'; |
| import { canonicalToolArgsHash } from '@maka/core/tool-args-identity'; |
| import { |
| buildImmutableRuntimePrefix, |
| createRuntimeBoundaryCursor, |
| runtimePrefixSegment, |
| type ContinuationClaimV1, |
| type ImmutableRuntimePrefixV1, |
| } from '@maka/core/runtime-boundary'; |
| import { |
| ToolLedgerCorruptionError, |
| ToolLedgerRejectionError, |
| } from '@maka/core/tool-ledger-scanner'; |
| import { |
| SQLITE_RUNTIME_SCHEMA_VERSION, |
| createSqliteRuntimeStore, |
| type SqliteRuntimeStoreFailpoint, |
| } from '../sqlite-runtime-store.js'; |
| |
| const PREFIX_PROOF_TEST_BUDGET = { |
| maxEvents: 64, |
| maxBytes: 1024 * 1024, |
| maxRecordBytes: 256 * 1024, |
| }; |
| |
| describe('SqliteRuntimeStore', () => { |
| it('applies versioned migrations and reopens the same database without rewriting schema', async () => { |
| await withStore(async (store, dbPath) => { |
| assert.equal(store.schemaVersion(), SQLITE_RUNTIME_SCHEMA_VERSION); |
| assert.equal(store.journalMode(), 'wal'); |
| assert.equal(store.foreignKeysEnabled(), true); |
| store.close(); |
| |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| assert.equal(reopened.schemaVersion(), SQLITE_RUNTIME_SCHEMA_VERSION); |
| assert.deepEqual(await reopened.readRuntimeEvents('session-1', 'run-1'), []); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| |
| it('refuses every post-terminal append as the typed sealed-run boundary', async () => { |
| await withStore(async (store) => { |
| const opening = functionCallEvent({ |
| id: 'sealed-run-opening', |
| content: { kind: 'text', text: 'hello' }, |
| }); |
| await store.appendRuntimeEvent(opening.sessionId, opening.runId, opening); |
| const terminal: RuntimeEvent = { |
| id: 'sealed-run-terminal', |
| invocationId: 'invocation-1', |
| runId: opening.runId, |
| sessionId: opening.sessionId, |
| turnId: 'turn-1', |
| ts: 2, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'aborted', |
| actions: { endInvocation: true, stateDelta: { abortSource: 'user_stop' } }, |
| }; |
| await store.appendRuntimeEvent(terminal.sessionId, terminal.runId, terminal); |
| |
| // A plain straggler and a tool-bearing one refuse identically: the |
| // seal is checked before tool-ledger semantics (#2311), so a late |
| // function_call cannot surface as a producer bug or as corruption. |
| await assert.rejects( |
| store.appendRuntimeEvent(opening.sessionId, opening.runId, { |
| ...opening, |
| id: 'late-plain-straggler', |
| ts: 3, |
| }), |
| (error: unknown) => error instanceof RunSealedError, |
| ); |
| await assert.rejects( |
| store.appendRuntimeEvent( |
| opening.sessionId, |
| opening.runId, |
| functionCallEvent({ |
| id: 'late-tool-straggler', |
| ts: 4, |
| }), |
| ), |
| (error: unknown) => error instanceof RunSealedError, |
| ); |
| // Exact-id retry of an already-stored event keeps its dedup answer. |
| await store.appendRuntimeEvent(terminal.sessionId, terminal.runId, terminal); |
| }); |
| }); |
| |
| it('bounds a transcript Turn by stored total and per-record bytes', async () => { |
| await withStore(async (store) => { |
| const run = { |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| }; |
| await store.appendRuntimeEvent( |
| run.sessionId, |
| run.runId, |
| buildInvocationOpenedEvent({ |
| id: 'oversized-opening', |
| run, |
| openedAt: 1, |
| opening: { |
| kind: 'invocation_opened', |
| protocol: 'invocation_opened_v1', |
| route: { |
| provenance: 'runtime', |
| backendKind: 'fake', |
| llmConnectionId: 'fake-connection', |
| llmConnectionSlug: 'fake', |
| modelId: 'fake-model', |
| }, |
| configuration: { |
| cwd: '/tmp', |
| permissionMode: 'ask', |
| collaborationMode: 'agent', |
| orchestrationMode: 'default', |
| orchestrationSource: 'session', |
| toolMode: DEFAULT_TOOL_MODE, |
| }, |
| root: { kind: 'user' }, |
| source: { kind: 'fresh' }, |
| }, |
| }), |
| ); |
| // Every character here is three stored bytes, so a budget read as UTF-16 |
| // code units admits a Turn three times the size it was asked to bound. |
| const text = '本'.repeat(4_000); |
| await store.appendRuntimeEvent(run.sessionId, run.runId, { |
| id: 'oversized-prompt', |
| ...run, |
| ts: 2, |
| partial: false, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text }, |
| }); |
| await store.appendRuntimeEvent(run.sessionId, run.runId, { |
| id: 'oversized-terminal', |
| ...run, |
| ts: 3, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }); |
| |
| const request = { |
| direction: 'newer' as const, |
| throughOrdinal: Number.MAX_SAFE_INTEGER, |
| position: 1, |
| maxEvents: 64, |
| maxRecordBytes: 64_000, |
| }; |
| await assert.rejects( |
| store.readTranscriptRun(run.sessionId, { ...request, maxBytes: 6_000 }, (_run, events) => [ |
| ...events, |
| ]), |
| (error: unknown) => error instanceof RuntimeTranscriptOversizedTurnError, |
| ); |
| await assert.rejects( |
| store.readTranscriptRun( |
| run.sessionId, |
| { ...request, maxBytes: 64_000, maxRecordBytes: 6_000 }, |
| (_run, events) => [...events], |
| ), |
| (error: unknown) => error instanceof RuntimeTranscriptOversizedTurnError, |
| ); |
| await assert.rejects( |
| store.readTranscriptRun( |
| run.sessionId, |
| { ...request, maxEvents: 2, maxBytes: 64_000 }, |
| (_run, events) => [...events], |
| ), |
| (error: unknown) => error instanceof RuntimeTranscriptOversizedTurnError, |
| ); |
| const served = await store.readTranscriptRun( |
| run.sessionId, |
| { ...request, maxBytes: 64_000 }, |
| (_run, events) => [...events], |
| ); |
| assert.equal(served?.length, 3); |
| }); |
| }); |
| |
| it('projects transcript RuntimeEvents from a bounded row iterator', async () => { |
| await withStore(async (store) => { |
| await appendSettledTurn(store, 1); |
| |
| const projected = await store.readTranscriptRun( |
| 'session-1', |
| { |
| direction: 'newer', |
| throughOrdinal: Number.MAX_SAFE_INTEGER, |
| position: 1, |
| maxEvents: 3, |
| maxBytes: 64_000, |
| maxRecordBytes: 32_000, |
| }, |
| (run, events) => { |
| assert.equal(Array.isArray(events), false); |
| return { |
| invocationId: run.invocation.invocationId, |
| firstOrdinal: run.firstOrdinal, |
| lastOrdinal: run.lastOrdinal, |
| rows: [...events].map(({ ordinal, event }) => ({ ordinal, eventId: event.id })), |
| }; |
| }, |
| ); |
| |
| assert.deepEqual(projected, { |
| invocationId: 'invocation-1', |
| firstOrdinal: 1, |
| // The Session holds nothing after this Turn, so its run reaches the |
| // read's own bound rather than stopping at another Turn's first event. |
| lastOrdinal: Number.MAX_SAFE_INTEGER, |
| rows: [ |
| { ordinal: 1, eventId: 'opened-1' }, |
| { ordinal: 2, eventId: 'prompt-1' }, |
| { ordinal: 3, eventId: 'terminal-1' }, |
| ], |
| }); |
| }); |
| }); |
| |
| it('pages the transcript without reading rows the page does not contain', async () => { |
| await withStore(async (store, dbPath) => { |
| for (let turn = 0; turn < 4; turn += 1) await appendSettledTurn(store, turn); |
| store.close(); |
| const db = new DatabaseSync(dbPath); |
| try { |
| const executed: { sql: string; bind: unknown[] }[] = []; |
| const query = new RuntimeTranscriptQuery( |
| watchStatements(db, executed), |
| () => |
| ({ |
| sessionId: 'session-1', |
| }) as unknown as RuntimeInvocationRecord, |
| ); |
| const request = { |
| throughOrdinal: Number.MAX_SAFE_INTEGER, |
| position: 6, |
| maxEvents: 64, |
| maxBytes: 64_000, |
| maxRecordBytes: 64_000, |
| }; |
| query.highWater('session-1'); |
| query.run('session-1', { ...request, direction: 'older' }, (_run, events) => [...events]); |
| query.run('session-1', { ...request, direction: 'newer' }, (_run, events) => [...events]); |
| // A full scan is how a page starts costing the Session it sits in: the |
| // rows it walks are every Turn's, not the page's. |
| for (const { sql, bind } of executed) { |
| const plan = db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...(bind as [])) as unknown as { |
| detail: string; |
| }[]; |
| const scans = plan.filter((step) => step.detail.startsWith('SCAN')); |
| assert.deepEqual(scans, [], `${scans[0]?.detail} in ${sql}`); |
| } |
| } finally { |
| db.close(); |
| } |
| }); |
| }); |
| |
| it('assigns stable Session ordinals in commit order across Runs', async () => { |
| await withStore(async (store, dbPath) => { |
| const first = functionCallEvent({ id: 'ordinal-1', ts: 20 }); |
| const second = functionCallEvent({ |
| id: 'ordinal-2', |
| invocationId: 'invocation-2', |
| runId: 'run-2', |
| turnId: 'turn-2', |
| ts: 10, |
| }); |
| await store.appendRuntimeEvent(first.sessionId, first.runId, first); |
| await store.appendRuntimeEvent(second.sessionId, second.runId, second); |
| await store.appendRuntimeEvent(first.sessionId, first.runId, first); |
| |
| assert.deepEqual( |
| (await store.readSessionRuntimeEventEntries('session-1')).map(({ ordinal, event }) => ({ |
| ordinal, |
| eventId: event.id, |
| })), |
| [ |
| { ordinal: 1, eventId: 'ordinal-1' }, |
| { ordinal: 2, eventId: 'ordinal-2' }, |
| ], |
| ); |
| |
| store.close(); |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| assert.deepEqual( |
| (await reopened.readSessionRuntimeEventEntries('session-1')).map( |
| ({ ordinal, event }) => ({ ordinal, eventId: event.id }), |
| ), |
| [ |
| { ordinal: 1, eventId: 'ordinal-1' }, |
| { ordinal: 2, eventId: 'ordinal-2' }, |
| ], |
| ); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| it('publishes RuntimeEvent commits once, after a committed transaction', async () => { |
| await withStore(async (store) => { |
| const commits: string[] = []; |
| store.subscribeRuntimeEventCommits((sessionId) => commits.push(sessionId)); |
| const first = textEvent('batch-1'); |
| const second = textEvent('batch-2'); |
| await assert.rejects( |
| store.importRuntimeEventsBatch({ |
| sessionId: first.sessionId, |
| runId: first.runId, |
| events: [first, { ...first, ts: 99 }], |
| }), |
| ); |
| assert.deepEqual(await store.readSessionRuntimeEventEntries('session-1'), []); |
| assert.deepEqual(commits, []); |
| await store.importRuntimeEventsBatch({ |
| sessionId: first.sessionId, |
| runId: first.runId, |
| events: [first, second], |
| }); |
| assert.deepEqual(commits, ['session-1']); |
| }); |
| }); |
| |
| it('publishes leased RuntimeEvent commits when the outermost transaction settles', async () => { |
| const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-lease-')); |
| const outer = acquireOperationalStateDatabase(root); |
| const store = createSqliteRuntimeStore(resolveOperationalStateDatabasePath(root), { |
| databaseLease: acquireOperationalStateDatabase(root), |
| }); |
| try { |
| const commits: string[] = []; |
| store.subscribeRuntimeEventCommits((sessionId) => commits.push(sessionId)); |
| const appends: Promise<void>[] = []; |
| assert.throws(() => |
| outer.transaction('write', () => { |
| appends.push(store.appendRuntimeEvent('session-1', 'run-1', textEvent('lost'))); |
| throw new Error('roll back'); |
| }), |
| ); |
| await Promise.all(appends); |
| assert.deepEqual(commits, []); |
| assert.deepEqual(await store.readSessionRuntimeEventEntries('session-1'), []); |
| |
| outer.transaction('write', () => { |
| for (const id of ['kept-1', 'kept-2']) { |
| appends.push(store.appendRuntimeEvent('session-1', 'run-1', textEvent(id))); |
| } |
| assert.deepEqual(commits, []); |
| }); |
| assert.deepEqual(commits, ['session-1']); |
| await Promise.all(appends); |
| } finally { |
| store.close(); |
| outer.close(); |
| await rm(root, { recursive: true, force: true }); |
| } |
| }); |
| |
| it('makes a raw canonical-equivalent terminal durability retry idempotent', async () => { |
| await withStore(async (store) => { |
| const terminal: RuntimeEvent = { |
| id: 'terminal-event-1', |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| ts: 5, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'completed', |
| content: { |
| kind: 'text', |
| text: 'done', |
| displayText: 'done', |
| attachments: [], |
| quotes: [], |
| }, |
| actions: { endInvocation: true }, |
| }; |
| |
| await store.appendRuntimeEvent('session-1', 'run-1', terminal); |
| await store.ensureTerminalRuntimeEventDurable('session-1', 'run-1', terminal); |
| |
| const events = await store.readImmutableRuntimeEvents('session-1', 'run-1'); |
| assert.equal(events.length, 1); |
| assert.deepEqual(events[0]?.content, { kind: 'text', text: 'done' }); |
| }); |
| }); |
| |
| it('imports a conversation-copy tool ledger with its derived projections', async () => { |
| await withStore(async (store) => { |
| const events = [functionCallEvent(), toolDispatchEvent(), functionResponseEvent({ ts: 11 })]; |
| |
| await store.importConversationCopyRuntimeEvents('session-1', [{ runId: 'run-1', events }]); |
| await store.importConversationCopyRuntimeEvents('session-1', [{ runId: 'run-1', events }]); |
| |
| assert.deepEqual(await store.readImmutableRuntimeEvents('session-1', 'run-1'), events); |
| assert.equal( |
| (await store.readToolOperation('operation-1'))?.currentState, |
| 'outcome_committed', |
| ); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => event.state), |
| ['prepared', 'outcome_committed'], |
| ); |
| }); |
| }); |
| |
| // These two pin the ERROR CLASS, not the message. AgentRun exempts exactly |
| // one class from the store-unavailable latch (`ToolLedgerRejectionError`), so |
| // the class is a behavioural contract between storage and runtime — and both |
| // messages are byte-identical to the plain `Error` strings they replaced, so |
| // a regression to `throw new Error(...)` would leave every message-matching |
| // assertion in this suite green while the exemption silently stopped working. |
| it('rejects an inadmissible candidate with ToolLedgerRejectionError, naming the code', async () => { |
| await withStore(async (store) => { |
| // Untagged, so it takes the generic lane — a tagged response is a |
| // reserved boundary fact and never reaches the transition check. This is |
| // the exact shape #2234 produced: a result with no call to answer. |
| const orphan = functionResponseEvent({ |
| id: 'orphan-response-event', |
| ts: 11, |
| refs: { toolCallId: 'provider-call-1' }, |
| }); |
| await assert.rejects( |
| store.appendRuntimeEvent(orphan.sessionId, orphan.runId, orphan), |
| (error: unknown) => |
| error instanceof ToolLedgerRejectionError && |
| error.code === 'orphan_response' && |
| error.eventId === 'orphan-response-event', |
| ); |
| }); |
| }); |
| |
| it('reports pre-existing damage as ToolLedgerCorruptionError, even from another session', async () => { |
| await withStore(async (store, dbPath) => { |
| store.close(); |
| |
| // Seed damage the store would never have written itself, in a session |
| // this run never touches: the health scan has no WHERE clause, so one |
| // damaged operation anywhere in the workspace is what a later append meets. |
| const raw = new DatabaseSync(dbPath); |
| const stranded = functionResponseEvent({ |
| id: 'stranded-response', |
| sessionId: 'some-other-session', |
| invocationId: 'some-other-invocation', |
| runId: 'some-other-run', |
| ts: 5, |
| }); |
| raw |
| .prepare(` |
| INSERT INTO runtime_events |
| (event_id, session_id, invocation_id, run_id, turn_id, event_seq, event_kind, |
| payload_json, committed_at) |
| VALUES (?, ?, ?, ?, ?, 1, 'function_response', ?, 5) |
| `) |
| .run( |
| stranded.id, |
| stranded.sessionId, |
| stranded.invocationId, |
| stranded.runId, |
| stranded.turnId, |
| JSON.stringify(stranded), |
| ); |
| raw.close(); |
| |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| const healthy = functionCallEvent(); |
| await assert.rejects( |
| reopened.appendRuntimeEvent(healthy.sessionId, healthy.runId, healthy), |
| (error: unknown) => |
| error instanceof ToolLedgerCorruptionError && |
| !(error instanceof ToolLedgerRejectionError) && |
| error.code === 'orphan_response', |
| ); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| |
| it('commits function_call, dispatch fact, and operation projection atomically in T1', async () => { |
| await withStore(async (store) => { |
| const call = functionCallEvent(); |
| const dispatch = toolDispatchEvent(); |
| |
| const input = { |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_prepared', |
| runtimeEvent: call, |
| dispatchRuntimeEvent: dispatch, |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| committedAt: 10, |
| } as const; |
| const result = await store.commitToolPrepared(input); |
| |
| assert.equal(result.created, true); |
| assert.equal(result.runtimeEventSeq, 2); |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), [call, dispatch]); |
| assert.deepEqual(await store.readToolOperation('operation-1'), { |
| operationId: 'operation-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| currentState: 'prepared', |
| callEventId: 'call-event-1', |
| dispatchEventId: 'dispatch-event-1', |
| version: 1, |
| }); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => event.state), |
| ['prepared'], |
| ); |
| assert.equal((await store.readToolJournal('operation-1'))[0]?.runtimeEventId, dispatch.id); |
| assert.deepEqual( |
| (await store.listUnsettledToolOperations()).map((operation) => operation.operationId), |
| ['operation-1'], |
| ); |
| }); |
| }); |
| |
| it('commits nested T1 events with parent operation linkage', async () => { |
| await withStore(async (store) => { |
| const parentRefs = { |
| parentToolCallId: 'exec-call-1', |
| parentOperationId: 'exec-operation-1', |
| } as const; |
| const call = functionCallEvent({ |
| refs: { |
| operationId: 'operation-1', |
| toolCallId: 'provider-call-1', |
| ...parentRefs, |
| }, |
| }); |
| const dispatch = toolDispatchEvent({ |
| refs: { |
| operationId: 'operation-1', |
| toolCallId: 'provider-call-1', |
| ...parentRefs, |
| }, |
| }); |
| |
| const result = await store.commitToolPrepared({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_prepared', |
| runtimeEvent: call, |
| dispatchRuntimeEvent: dispatch, |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| committedAt: 10, |
| }); |
| |
| assert.equal(result.created, true); |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), [call, dispatch]); |
| }); |
| }); |
| |
| it('claims an exact function_call that was committed while permission was pending', async () => { |
| await withStore(async (store) => { |
| const call = functionCallEvent(); |
| await store.appendRuntimeEvent('session-1', 'run-1', call); |
| |
| const result = await commitPrepared(store); |
| |
| assert.equal(result.created, true); |
| assert.equal(result.runtimeEventSeq, 2); |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), [ |
| call, |
| toolDispatchEvent(), |
| ]); |
| assert.equal((await store.readToolOperation('operation-1'))?.currentState, 'prepared'); |
| }); |
| }); |
| |
| it('rolls back every T1 row when failure occurs after the RuntimeEvent insert', async () => { |
| await withStore(async (store, _dbPath, setFailpoint) => { |
| setFailpoint('after_runtime_event_insert'); |
| |
| await assert.rejects( |
| store.commitToolPrepared({ |
| operationId: 'operation-t1-failure', |
| journalEventId: 'operation-t1-failure_prepared', |
| runtimeEvent: functionCallEvent({ id: 'call-t1-failure' }), |
| dispatchRuntimeEvent: toolDispatchEvent({ |
| id: 'dispatch-t1-failure', |
| refs: { operationId: 'operation-t1-failure', toolCallId: 'provider-call-1' }, |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'operation-t1-failure', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| }, |
| }, |
| }), |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| committedAt: 11, |
| }), |
| /sqlite runtime failpoint: after_runtime_event_insert/, |
| ); |
| |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), []); |
| assert.equal(await store.readToolOperation('operation-t1-failure'), undefined); |
| assert.deepEqual(await store.readToolJournal('operation-t1-failure'), []); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 0); |
| }); |
| }); |
| |
| it('commits function_response, outcome journal fact, and projection atomically in T2', async () => { |
| await withStore(async (store) => { |
| await commitPrepared(store, { resultProjectionVersion: 1 }); |
| const outcome = functionResponseEvent({ |
| content: { |
| kind: 'function_response', |
| id: 'provider-call-1', |
| name: 'Read', |
| result: 'private execution contents', |
| modelProjection: { version: 1, kind: 'text', text: 'bounded model contents' }, |
| }, |
| }); |
| |
| const result = await store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: outcome, |
| committedAt: 20, |
| }); |
| |
| assert.equal(result.created, true); |
| assert.equal(result.runtimeEventSeq, 3); |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), [ |
| functionCallEvent(), |
| toolDispatchEvent({ |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'operation-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| resultProjectionVersion: 1, |
| }, |
| }, |
| }), |
| outcome, |
| ]); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3); |
| assert.deepEqual(await store.readToolOperation('operation-1'), { |
| operationId: 'operation-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| currentState: 'outcome_committed', |
| callEventId: 'call-event-1', |
| dispatchEventId: 'dispatch-event-1', |
| resultEventId: 'response-event-1', |
| version: 2, |
| }); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => event.state), |
| ['prepared', 'outcome_committed'], |
| ); |
| assert.deepEqual(await store.listUnsettledToolOperations(), []); |
| }); |
| }); |
| |
| it('keeps projected T2 prepared when its atomic model projection is missing', async () => { |
| await withStore(async (store) => { |
| await commitPrepared(store, { resultProjectionVersion: 1 }); |
| |
| await assert.rejects( |
| store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: functionResponseEvent(), |
| committedAt: 20, |
| }), |
| /requires its durable model projection/, |
| ); |
| |
| assert.deepEqual( |
| (await store.readRuntimeEvents('session-1', 'run-1')).map((event) => event.id), |
| ['call-event-1', 'dispatch-event-1'], |
| ); |
| assert.equal((await store.readToolOperation('operation-1'))?.currentState, 'prepared'); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => event.state), |
| ['prepared'], |
| ); |
| }); |
| }); |
| |
| it('rolls back T2 without hiding the previously committed prepared boundary', async () => { |
| await withStore(async (store, _dbPath, setFailpoint) => { |
| await commitPrepared(store); |
| setFailpoint('after_runtime_event_insert'); |
| |
| await assert.rejects( |
| store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: functionResponseEvent({ id: 'response-t2-failure' }), |
| committedAt: 21, |
| }), |
| /sqlite runtime failpoint: after_runtime_event_insert/, |
| ); |
| |
| assert.deepEqual( |
| (await store.readRuntimeEvents('session-1', 'run-1')).map((event) => event.id), |
| ['call-event-1', 'dispatch-event-1'], |
| ); |
| assert.equal((await store.readToolOperation('operation-1'))?.currentState, 'prepared'); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => event.state), |
| ['prepared'], |
| ); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 2); |
| }); |
| }); |
| |
| it('deduplicates exact T1/T2 retries and rejects operation identity drift', async () => { |
| await withStore(async (store) => { |
| const firstPrepared = await commitPrepared(store); |
| const duplicatePrepared = await commitPrepared(store); |
| assert.equal(firstPrepared.created, true); |
| assert.equal(duplicatePrepared.created, false); |
| |
| const firstOutcome = await store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: functionResponseEvent(), |
| committedAt: 20, |
| }); |
| const duplicateOutcome = await store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: functionResponseEvent(), |
| committedAt: 20, |
| }); |
| assert.equal(firstOutcome.created, true); |
| assert.equal(duplicateOutcome.created, false); |
| assert.equal((await store.readToolJournal('operation-1')).length, 2); |
| assert.equal((await store.readRuntimeEvents('session-1', 'run-1')).length, 3); |
| |
| await assert.rejects( |
| store.commitToolPrepared({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_prepared', |
| runtimeEvent: functionCallEvent({ |
| content: { |
| kind: 'function_call', |
| id: 'provider-call-1', |
| name: 'Read', |
| args: { path: '/workspace/repo/OTHER.md' }, |
| }, |
| }), |
| dispatchRuntimeEvent: toolDispatchEvent({ |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'operation-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: DIFFERENT_READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| }, |
| }, |
| }), |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: DIFFERENT_READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| committedAt: 30, |
| }), |
| /duplicate_event_id/, |
| ); |
| }); |
| }); |
| |
| it('validates a tool transition after unrelated invocation history', async () => { |
| await withStore(async (store) => { |
| const unrelated = functionCallEvent({ |
| id: 'unrelated-event', |
| sessionId: 'session-2', |
| invocationId: 'invocation-2', |
| runId: 'run-2', |
| turnId: 'turn-2', |
| content: { kind: 'text', text: 'unrelated history' }, |
| }); |
| await store.appendRuntimeEvent(unrelated.sessionId, unrelated.runId, unrelated); |
| |
| const result = await commitPrepared(store); |
| |
| assert.equal(result.created, true); |
| assert.equal((await store.readToolOperation('operation-1'))?.currentState, 'prepared'); |
| }); |
| }); |
| |
| it('rebuilds disposable tool projections from RuntimeEvent facts', async () => { |
| await withStore(async (store) => { |
| await commitPrepared(store); |
| await store.commitToolOutcome({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_outcome', |
| runtimeEvent: functionResponseEvent(), |
| committedAt: 20, |
| }); |
| |
| const result = await store.rebuildToolProjectionsFromRuntimeEvents(); |
| |
| assert.deepEqual(result, { operations: 1, journalEvents: 2 }); |
| assert.equal( |
| (await store.readToolOperation('operation-1'))?.dispatchEventId, |
| 'dispatch-event-1', |
| ); |
| assert.deepEqual( |
| (await store.readToolJournal('operation-1')).map((event) => ({ |
| state: event.state, |
| runtimeEventId: event.runtimeEventId, |
| })), |
| [ |
| { state: 'prepared', runtimeEventId: 'dispatch-event-1' }, |
| { state: 'outcome_committed', runtimeEventId: 'response-event-1' }, |
| ], |
| ); |
| }); |
| }); |
| |
| it('coalesces stream chunks outside the immutable high-water ledger', async () => { |
| await withStore(async (store) => { |
| for (const [index, text] of ['hel', 'lo', '!'].entries()) { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: `partial-${index}`, |
| ts: index + 1, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| } |
| |
| const visible = await store.readRuntimeEvents('session-1', 'run-1'); |
| const scanned: RuntimeEvent[] = []; |
| const scan = await store.scanRuntimeEvents( |
| 'session-1', |
| 'run-1', |
| { |
| maxBatchBytes: 1024, |
| maxRecordBytes: 1024, |
| maxImmutableRecords: 10, |
| maxImmutableBytes: 1024, |
| maxPartialRecords: 10, |
| maxPartialBytes: 1024, |
| }, |
| (events) => scanned.push(...events), |
| ); |
| assert.equal(scan.status, 'complete'); |
| assert.equal(visible.length, 1); |
| assert.deepEqual(scanned, visible); |
| assert.deepEqual(visible[0]?.content, { kind: 'text', text: 'hello!' }); |
| assert.deepEqual(await store.readImmutableRuntimeEvents('session-1', 'run-1'), []); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 0); |
| }); |
| }); |
| |
| it('rejects an oversized scan record before visiting its decoded body', async () => { |
| await withStore(async (store) => { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'large-event', |
| content: { kind: 'text', text: 'x'.repeat(4096) }, |
| }), |
| ); |
| let visits = 0; |
| const result = await store.scanRuntimeEvents( |
| 'session-1', |
| 'run-1', |
| { |
| maxBatchBytes: 128, |
| maxRecordBytes: 128, |
| maxImmutableRecords: 10, |
| maxImmutableBytes: 1024, |
| maxPartialRecords: 10, |
| maxPartialBytes: 1024, |
| }, |
| () => { |
| visits += 1; |
| }, |
| ); |
| assert.equal(result.status, 'limit_exceeded'); |
| assert.equal(visits, 0); |
| }); |
| }); |
| |
| it('rejects an immutable ledger that exceeds its cumulative scan budget before decoding it', async () => { |
| await withStore(async (store) => { |
| for (const index of [1, 2]) { |
| const event: RuntimeEvent = { |
| id: `event-${index}`, |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| sessionId: 'session-1', |
| turnId: 'turn-1', |
| ts: index, |
| partial: false, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: `message-${index}` }, |
| }; |
| await store.appendRuntimeEvent('session-1', 'run-1', event); |
| } |
| let visits = 0; |
| const result = await store.scanRuntimeEvents( |
| 'session-1', |
| 'run-1', |
| { |
| maxBatchBytes: 16 * 1024, |
| maxRecordBytes: 16 * 1024, |
| maxImmutableRecords: 1, |
| maxImmutableBytes: 16 * 1024, |
| maxPartialRecords: 10, |
| maxPartialBytes: 16 * 1024, |
| }, |
| () => { |
| visits += 1; |
| }, |
| ); |
| |
| assert.equal(result.status, 'limit_exceeded'); |
| assert.equal(visits, 0); |
| }); |
| }); |
| |
| it('streams fragmented legacy partial segments without retaining their row set', async () => { |
| await withStore(async (store, dbPath) => { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'partial-segment-seed', |
| partial: true, |
| content: { kind: 'text', text: '' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| const inspect = new DatabaseSync(dbPath); |
| try { |
| const { stream_key: streamKey } = inspect |
| .prepare('SELECT stream_key FROM runtime_partial_snapshots') |
| .get() as { stream_key: string }; |
| const insert = inspect.prepare(` |
| INSERT INTO runtime_partial_segments(stream_key, segment_seq, text_content, updated_at) |
| VALUES (?, ?, 'x', ?) |
| `); |
| inspect.exec('BEGIN IMMEDIATE'); |
| for (let sequence = 1; sequence <= 9_000; sequence += 1) { |
| insert.run(streamKey, sequence, sequence); |
| } |
| inspect.exec('COMMIT'); |
| } finally { |
| inspect.close(); |
| } |
| const scanned: RuntimeEvent[] = []; |
| const result = await store.scanRuntimeEvents( |
| 'session-1', |
| 'run-1', |
| { |
| maxBatchBytes: 1024, |
| maxRecordBytes: 16 * 1024, |
| maxImmutableRecords: 10, |
| maxImmutableBytes: 16 * 1024, |
| maxPartialRecords: 10, |
| maxPartialBytes: 16 * 1024, |
| }, |
| (events) => scanned.push(...events), |
| ); |
| assert.equal(result.status, 'complete'); |
| assert.equal(scanned.length, 1); |
| assert.equal(scanned[0]?.content?.kind, 'text'); |
| assert.equal( |
| scanned[0]?.content?.kind === 'text' ? scanned[0].content.text : undefined, |
| 'x'.repeat(9_000), |
| ); |
| }); |
| }); |
| |
| it('atomically claims a source with exactly one terminal RuntimeEvent at its tail', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| |
| const acquired = await store.claimContinuation({ claim }); |
| const existing = await store.claimContinuation({ claim: { ...claim } }); |
| |
| assert.equal(acquired.kind, 'acquired'); |
| assert.equal(existing.kind, 'existing'); |
| assert.deepEqual(existing.claim, claim); |
| assert.deepEqual(await store.readContinuationClaimByBoundary(claim.boundaryDigest), claim); |
| }); |
| }); |
| |
| it('refuses a continuation target opening it cannot read, stored or submitted', async () => { |
| await withStore(async (store, dbPath) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| await assert.rejects( |
| () => |
| store.claimContinuation({ |
| claim: { |
| ...claim, |
| targetOpening: { |
| ...claim.targetOpening, |
| configuration: { |
| ...claim.targetOpening.configuration, |
| permissionMode: 'execute', |
| }, |
| } as unknown as ContinuationClaimV1['targetOpening'], |
| }, |
| }), |
| /Invalid RuntimeEvent invocation_opened schema/, |
| ); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| const database = new DatabaseSync(dbPath); |
| try { |
| database.exec(` |
| UPDATE runtime_continuation_claims |
| SET target_opening_json = json_set( |
| target_opening_json, |
| '$.configuration.permissionMode', |
| 'execute' |
| ) |
| WHERE claim_id = 'claim-1'; |
| `); |
| } finally { |
| database.close(); |
| } |
| |
| // A persisted Run header used to be widened on read. The opening fact has |
| // no legacy layer and none is wanted: a claim whose frozen opening cannot |
| // be read cannot authenticate the start event it exists to authenticate, |
| // and admitting one against a guessed opening would be the failure this |
| // record is meant to prevent. |
| await assert.rejects( |
| store.readContinuationClaimByBoundary(claim.boundaryDigest), |
| /Invalid RuntimeEvent invocation_opened schema/, |
| ); |
| }); |
| }); |
| |
| for (const initialKind of ['fresh', 'continuation'] as const) { |
| it(`authenticates repeated handoff under one ${initialKind} logical admission across reopen`, async () => { |
| await withStore(async (store, dbPath) => { |
| const manual = continuationClaim(); |
| let source: RuntimeEvent; |
| const segments: ReturnType<typeof runtimePrefixSegment>[] = []; |
| if (initialKind === 'continuation') { |
| const ancestor = continuationSourcePrefix(); |
| await persistImmutablePrefix(store, ancestor); |
| segments.push(runtimePrefixSegment(ancestor)); |
| await store.claimContinuation({ claim: manual }); |
| source = continuationStartEvent(manual); |
| await store.commitContinuationStart({ claim: manual, event: source }); |
| } else { |
| source = { |
| ...continuationStartEvent(manual), |
| id: 'root-opening', |
| actions: undefined, |
| content: { |
| ...manual.targetOpening, |
| source: { kind: 'fresh' }, |
| root: { kind: 'goal', goalId: 'original-goal' }, |
| lineage: { parentRunId: 'owning-agent', parentTurnId: 'owning-turn' }, |
| }, |
| }; |
| await store.appendRuntimeEvent(source.sessionId, source.runId, source); |
| } |
| const rootRunId = source.runId; |
| const logicalIdentity = { |
| sessionId: source.sessionId, |
| turnId: source.turnId, |
| runId: rootRunId, |
| }; |
| let fullEventReads = 0; |
| const membershipStore = { |
| listSessionInvocations: store.listSessionInvocations.bind(store), |
| readRunInvocation: store.readRunInvocation.bind(store), |
| readContinuationClaimStateByBoundary: |
| store.readContinuationClaimStateByBoundary.bind(store), |
| readImmutableRuntimePrefixProof: (input: { |
| sessionId: string; |
| runId: string; |
| upToEventSeq?: number; |
| }) => store.readImmutableRuntimePrefixProof(input, PREFIX_PROOF_TEST_BUDGET), |
| readImmutableRuntimeEvents: async () => { |
| fullEventReads += 1; |
| throw new Error('membership read loaded full events'); |
| }, |
| }; |
| for (let index = 0; index < 2; index += 1) { |
| const target = { |
| sessionId: source.sessionId, |
| turnId: source.turnId, |
| runId: `handoff-run-${index}`, |
| invocationId: `handoff-invocation-${index}`, |
| }; |
| const claimId = `handoff-claim-${index}`; |
| const seal: RuntimeEvent = { |
| ...source, |
| id: `pause-${index}`, |
| content: undefined, |
| ts: 15 + index, |
| actions: { |
| endInvocation: true, |
| handoffPause: { |
| protocol: 'runtime_handoff_pause_v1', |
| handoffId: `handoff-${index}`, |
| remainingSteps: null, |
| hostEpoch: 'old-host', |
| rootRunId, |
| successorRunId: target.runId, |
| successorInvocationId: target.invocationId, |
| claimId, |
| }, |
| }, |
| }; |
| await store.appendRuntimeEvent(seal.sessionId, seal.runId, seal); |
| assert.equal( |
| (await readLogicalRuntimeExecution(store, logicalIdentity))?.pendingHandoff?.claimId, |
| claimId, |
| ); |
| assert.equal( |
| ( |
| await readLogicalRuntimeExecution(membershipStore, logicalIdentity, undefined, { |
| mode: 'membership', |
| }) |
| )?.pendingHandoff?.claimId, |
| claimId, |
| ); |
| segments.push( |
| runtimePrefixSegment( |
| await store.readImmutableRuntimePrefix({ |
| sessionId: source.sessionId, |
| runId: source.runId, |
| }), |
| ), |
| ); |
| const boundary = createRuntimeBoundaryCursor( |
| segments as [(typeof segments)[number], ...typeof segments], |
| ); |
| const proposed = continuationClaimForBoundary(boundary, { claimId, target }); |
| assert.equal(source.content?.kind, 'invocation_opened'); |
| const opening = source.content as ContinuationClaimV1['targetOpening']; |
| assert.equal(proposed.targetOpening.source.kind, 'continuation'); |
| const claim: ContinuationClaimV1 = { |
| ...proposed, |
| targetOpening: { |
| ...opening, |
| source: { |
| ...(proposed.targetOpening.source as Extract< |
| ContinuationClaimV1['targetOpening']['source'], |
| { kind: 'continuation' } |
| >), |
| kind: 'handoff', |
| rootRunId, |
| claimId, |
| boundaryDigest: boundary.manifestDigest, |
| }, |
| }, |
| }; |
| for (const targetOpening of [ |
| { ...claim.targetOpening, root: { kind: 'user' as const } }, |
| { ...claim.targetOpening, lineage: { parentRunId: 'stolen-owner' } }, |
| { ...claim.targetOpening, configuration: { ...opening.configuration, cwd: '/other' } }, |
| ]) { |
| if (JSON.stringify(targetOpening) === JSON.stringify(claim.targetOpening)) continue; |
| await assert.rejects( |
| store.claimContinuation({ claim: { ...claim, targetOpening } }), |
| /sealed source authority/, |
| ); |
| } |
| await assert.rejects( |
| store.claimContinuation({ |
| claim: { |
| ...claim, |
| target: { ...claim.target, invocationId: 'unauthorized-physical-target' }, |
| }, |
| }), |
| /sealed source authority/, |
| ); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'existing'); |
| assert.equal( |
| (await readLogicalRuntimeExecution(store, logicalIdentity))?.pendingHandoff?.claimId, |
| claimId, |
| ); |
| source = continuationStartEvent(claim, { id: `handoff-start-${index}` }); |
| await store.commitContinuationStart({ claim, event: source }); |
| await store.commitContinuationStart({ claim, event: source }); |
| const live = await readLogicalRuntimeExecution(store, logicalIdentity); |
| const membership = await readLogicalRuntimeExecution( |
| membershipStore, |
| logicalIdentity, |
| undefined, |
| { mode: 'membership' }, |
| ); |
| assert.equal(live?.root.runId, rootRunId); |
| assert.equal(live?.tip.runId, source.runId); |
| assert.equal(live?.pendingHandoff, undefined); |
| assert.deepEqual(membership?.runIds, live?.runIds); |
| assert.equal(membership?.root.runId, live?.root.runId); |
| assert.equal(membership?.tip.runId, live?.tip.runId); |
| assert.equal(membership?.pendingHandoff, undefined); |
| assert.equal(fullEventReads, 0); |
| if (index === 0) { |
| await assert.rejects( |
| readLogicalRuntimeExecution( |
| { |
| ...membershipStore, |
| readImmutableRuntimePrefixProof: async (input) => ({ |
| ...(await membershipStore.readImmutableRuntimePrefixProof(input)), |
| prefixDigest: |
| 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| }), |
| }, |
| logicalIdentity, |
| undefined, |
| { mode: 'membership' }, |
| ), |
| /successor opened without its continuation claim/, |
| ); |
| } |
| await assert.rejects( |
| store.appendRuntimeEvent(source.sessionId, 'rogue', { |
| ...source, |
| id: `rogue-${index}`, |
| runId: 'rogue', |
| invocationId: 'rogue', |
| content: { kind: 'text', text: 'unauthorized' }, |
| actions: undefined, |
| }), |
| /target identity conflict/, |
| ); |
| } |
| const terminal: RuntimeEvent = { |
| ...source, |
| id: 'logical-completion', |
| content: undefined, |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }; |
| await store.appendRuntimeEvent(terminal.sessionId, terminal.runId, terminal); |
| store.close(); |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| const claims = await reopened.listContinuationClaimsForRecovery(source.sessionId); |
| assert.equal(claims.length, initialKind === 'fresh' ? 2 : 3); |
| assert.equal(claims.at(-1)?.claim.target.turnId, source.turnId); |
| assert.deepEqual( |
| (await reopened.readRuntimeEvents(source.sessionId, source.runId)).at(-1), |
| encodeCanonicalRuntimeEvent(terminal).event, |
| ); |
| assert.equal( |
| (await readLogicalRuntimeExecution(reopened, logicalIdentity))?.tip.terminalEvent |
| ?.status, |
| 'completed', |
| ); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| } |
| |
| it('rejects a continuation claim whose immediate source boundary is not durable', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| |
| await assert.rejects(store.claimContinuation({ claim }), /source boundary is missing/i); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| }); |
| }); |
| |
| it('rejects a non-terminal continuation source without sealing the active Run', async () => { |
| await withStore(async (store) => { |
| const source = activeContinuationSourcePrefix(); |
| const claim = continuationClaimForBoundary( |
| createRuntimeBoundaryCursor([runtimePrefixSegment(source)]), |
| ); |
| await persistImmutablePrefix(store, source); |
| |
| await assert.rejects( |
| store.claimContinuation({ claim }), |
| /source boundary must end with exactly one terminal RuntimeEvent/i, |
| ); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| |
| const terminal: RuntimeEvent = functionCallEvent({ |
| id: 'source-terminal-after-rejected-claim', |
| ts: 2, |
| content: undefined, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }); |
| await store.ensureTerminalRuntimeEventDurable(terminal.sessionId, terminal.runId, terminal); |
| assert.deepEqual( |
| (await store.readImmutableRuntimeEvents(terminal.sessionId, terminal.runId)).map( |
| (event) => event.id, |
| ), |
| [source.events[0]!.id, terminal.id], |
| ); |
| }); |
| }); |
| |
| it('rejects a continuation source whose terminal RuntimeEvent has a corrupt suffix', async () => { |
| await withStore(async (store, dbPath) => { |
| const source = continuationSourcePrefix(); |
| const suffix = functionCallEvent({ |
| id: 'corrupt-source-suffix', |
| ts: 3, |
| content: { kind: 'text', text: 'must not follow the terminal fact' }, |
| }); |
| await persistImmutablePrefix(store, source); |
| |
| const raw = new DatabaseSync(dbPath); |
| try { |
| raw |
| .prepare(` |
| INSERT INTO runtime_events ( |
| event_id, session_id, invocation_id, run_id, turn_id, event_seq, |
| event_kind, payload_json, committed_at |
| ) VALUES (?, ?, ?, ?, ?, 3, 'text', ?, ?) |
| `) |
| .run( |
| suffix.id, |
| suffix.sessionId, |
| suffix.invocationId, |
| suffix.runId, |
| suffix.turnId, |
| JSON.stringify(suffix), |
| suffix.ts, |
| ); |
| } finally { |
| raw.close(); |
| } |
| |
| const corruptedPrefix = buildImmutableRuntimePrefix(source.identity, [ |
| ...source.events.map((event, index) => ({ eventSeq: index + 1, event })), |
| { eventSeq: 3, event: suffix }, |
| ]); |
| const claim = continuationClaimForBoundary( |
| createRuntimeBoundaryCursor([runtimePrefixSegment(corruptedPrefix)]), |
| ); |
| |
| await assert.rejects( |
| store.claimContinuation({ claim }), |
| /source boundary must end with exactly one terminal RuntimeEvent/i, |
| ); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| }); |
| }); |
| |
| it('rejects a stale claim after the source advances beyond its planned boundary', async () => { |
| await withStore(async (store) => { |
| const source = activeContinuationSourcePrefix(); |
| const claim = continuationClaimForBoundary( |
| createRuntimeBoundaryCursor([runtimePrefixSegment(source)]), |
| ); |
| await persistImmutablePrefix(store, source); |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'source-event-2', |
| ts: 2, |
| role: 'system', |
| author: 'system', |
| content: undefined, |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }), |
| ); |
| |
| await assert.rejects(store.claimContinuation({ claim }), /source boundary changed/i); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| }); |
| }); |
| |
| it('seals the claimed source against later immutable RuntimeEvents', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| await assert.rejects( |
| store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'source-event-2', |
| ts: 2, |
| role: 'system', |
| author: 'system', |
| content: undefined, |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }), |
| ), |
| /sealed by continuation claim/i, |
| ); |
| }); |
| }); |
| |
| it('rolls back a continuation claim when the process fails after its insert', async () => { |
| await withStore(async (store, _dbPath, setFailpoint) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| setFailpoint('after_continuation_claim_insert'); |
| |
| await assert.rejects(store.claimContinuation({ claim }), /after_continuation_claim_insert/); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| |
| setFailpoint(undefined); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| }); |
| }); |
| |
| it('rejects a second boundary that tries to reuse an acquired target identity', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const source = continuationSourcePrefix(); |
| const otherSource = buildImmutableRuntimePrefix( |
| { |
| sessionId: 'session-1', |
| invocationId: 'invocation-source-2', |
| runId: 'run-source-2', |
| turnId: 'turn-source-2', |
| }, |
| [ |
| { |
| eventSeq: 1, |
| event: functionCallEvent({ |
| id: 'source-2-event', |
| invocationId: 'invocation-source-2', |
| runId: 'run-source-2', |
| turnId: 'turn-source-2', |
| content: { kind: 'text', text: 'source request' }, |
| role: 'user', |
| author: 'user', |
| }), |
| }, |
| { |
| eventSeq: 2, |
| event: functionCallEvent({ |
| id: 'source-2-terminal', |
| invocationId: 'invocation-source-2', |
| runId: 'run-source-2', |
| turnId: 'turn-source-2', |
| ts: 2, |
| content: undefined, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }), |
| }, |
| ], |
| ); |
| const otherBoundary = createRuntimeBoundaryCursor([runtimePrefixSegment(otherSource)]); |
| await persistImmutablePrefix(store, source); |
| await persistImmutablePrefix(store, otherSource); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| const conflict = await store.claimContinuation({ |
| claim: continuationClaimForBoundary(otherBoundary, { |
| claimId: 'claim-2', |
| claimedAt: 11, |
| target: claim.target, |
| }), |
| }); |
| |
| assert.equal(conflict.kind, 'conflict'); |
| assert.deepEqual(conflict.claim, claim); |
| }); |
| }); |
| |
| it('lets a started continuation target be purged instead of refusing the delete', async () => { |
| await withStore(async (store, dbPath) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| const db = new DatabaseSync(dbPath); |
| try { |
| db.exec('PRAGMA foreign_keys = ON'); |
| // Stand the claim up the way starting a continuation does: its start |
| // event is event one of the target Session's run. |
| const start = db |
| .prepare('SELECT event_id, session_id FROM runtime_events ORDER BY event_seq ASC LIMIT 1') |
| .get() as { event_id: string; session_id: string }; |
| db.prepare( |
| "UPDATE runtime_continuation_claims SET start_event_id = ?, start_kind = 'runtime_admission' WHERE claim_id = ?", |
| ).run(start.event_id, claim.claimId); |
| |
| // Purging a conversation deletes its events. The claim used to have no |
| // ON DELETE clause, so the constraint refused this and rolled the whole |
| // purge back — for the user's delete, a copy rollback, an import |
| // discard and Session retirement alike. |
| db.prepare('DELETE FROM runtime_events WHERE session_id = ?').run(start.session_id); |
| |
| assert.equal( |
| ( |
| db |
| .prepare( |
| 'SELECT COUNT(*) AS count FROM runtime_continuation_claims WHERE claim_id = ?', |
| ) |
| .get(claim.claimId) as { count: number } |
| ).count, |
| 0, |
| 'a continuation whose target was deleted no longer names anything, so it goes too', |
| ); |
| } finally { |
| db.close(); |
| } |
| }); |
| }); |
| |
| it('fails closed when continuation claim columns disagree with canonical payload', async () => { |
| await withStore(async (store, dbPath) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| const tamper = new DatabaseSync(dbPath); |
| try { |
| tamper |
| .prepare('UPDATE runtime_continuation_claims SET source_run_id = ? WHERE claim_id = ?') |
| .run('forged-source-run', claim.claimId); |
| } finally { |
| tamper.close(); |
| } |
| |
| await assert.rejects( |
| store.readContinuationClaimByBoundary(claim.boundaryDigest), |
| /row\/payload identity mismatch/, |
| ); |
| await assert.rejects( |
| store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'source-write-after-claim-corruption', |
| ts: 2, |
| content: { kind: 'text', text: 'must remain sealed' }, |
| }), |
| ), |
| /row\/payload identity mismatch/, |
| ); |
| }); |
| }); |
| |
| it('requires a continuation claim target to have an empty RuntimeEvent ledger', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| await store.appendRuntimeEvent( |
| claim.target.sessionId, |
| claim.target.runId, |
| functionCallEvent({ |
| id: 'unexpected-target-event', |
| ...claim.target, |
| ts: 9, |
| content: { kind: 'text', text: 'not a continuation start' }, |
| }), |
| ); |
| |
| await assert.rejects( |
| store.claimContinuation({ claim }), |
| /target RuntimeEvent ledger is not empty/i, |
| ); |
| assert.equal(await store.readContinuationClaimByBoundary(claim.boundaryDigest), undefined); |
| }); |
| }); |
| |
| it('reserves a claimed target first event for its dedicated continuation-start writer', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const start = continuationStartEvent(claim); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| await assert.rejects( |
| store.appendRuntimeEvent( |
| claim.target.sessionId, |
| claim.target.runId, |
| functionCallEvent({ |
| id: 'racing-target-event', |
| ...claim.target, |
| ts: 11, |
| content: { kind: 'text', text: 'must not steal event sequence one' }, |
| }), |
| ), |
| /reserved for continuation-start/i, |
| ); |
| assert.deepEqual(await store.commitContinuationStart({ claim, event: start }), { |
| created: true, |
| runtimeEventSeq: 1, |
| }); |
| |
| const afterStart: RuntimeEvent = { |
| id: 'continued-model-event', |
| ...claim.target, |
| ts: 13, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'provider dispatch is now admitted' }, |
| }; |
| await store.appendRuntimeEvent(claim.target.sessionId, claim.target.runId, afterStart); |
| assert.deepEqual( |
| (await store.readImmutableRuntimeEvents(claim.target.sessionId, claim.target.runId)).map( |
| (event) => event.id, |
| ), |
| [start.id, afterStart.id], |
| ); |
| }); |
| }); |
| |
| it('commits continuation-start exactly once through its dedicated authority writer', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const event = continuationStartEvent(claim); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| assert.deepEqual(await store.readContinuationClaimStateByBoundary(claim.boundaryDigest), { |
| claim, |
| }); |
| assert.deepEqual(await store.listContinuationClaimsForRecovery(claim.target.sessionId), [ |
| { claim }, |
| ]); |
| |
| await assert.rejects( |
| store.appendRuntimeEvent(claim.target.sessionId, claim.target.runId, event), |
| /continuation authority writer/i, |
| ); |
| assert.deepEqual(await store.commitContinuationStart({ claim, event }), { |
| created: true, |
| runtimeEventSeq: 1, |
| }); |
| assert.deepEqual(await store.commitContinuationStart({ claim, event }), { |
| created: false, |
| runtimeEventSeq: 1, |
| }); |
| assert.deepEqual( |
| await store.readImmutableRuntimeEvents(claim.target.sessionId, claim.target.runId), |
| [event], |
| ); |
| assert.deepEqual(await store.readContinuationClaimStateByBoundary(claim.boundaryDigest), { |
| claim, |
| startEventId: event.id, |
| startKind: 'runtime_admission', |
| }); |
| assert.deepEqual(await store.listContinuationClaimsForRecovery(claim.target.sessionId), [ |
| { claim, startEventId: event.id, startKind: 'runtime_admission' }, |
| ]); |
| }); |
| }); |
| |
| it('stores continuation start provenance through separate admission and repair commands', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const repairEvent = continuationStartEvent(claim, { |
| id: 'repair-start-event', |
| provenance: 'claim_repair', |
| }); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| await assert.rejects( |
| store.commitContinuationStart({ claim, event: repairEvent }), |
| /invalid continuation-start authority event/i, |
| ); |
| assert.deepEqual(await store.commitContinuationRepairStart({ claim, event: repairEvent }), { |
| created: true, |
| runtimeEventSeq: 1, |
| }); |
| assert.deepEqual(await store.readContinuationClaimStateByBoundary(claim.boundaryDigest), { |
| claim, |
| startEventId: repairEvent.id, |
| startKind: 'claim_repair', |
| }); |
| }); |
| }); |
| |
| it('binds the durable tool boundary marker to a live continuation start only', async () => { |
| await withStore(async (store) => { |
| const liveClaim = continuationClaim(); |
| const liveEvent = continuationStartEvent(liveClaim, { |
| toolBoundaryProtocol: 't1_after_preflight_v1', |
| }); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim: liveClaim })).kind, 'acquired'); |
| assert.deepEqual( |
| await store.commitContinuationStart({ claim: liveClaim, event: liveEvent }), |
| { |
| created: true, |
| runtimeEventSeq: 1, |
| }, |
| ); |
| |
| const repairSource = buildImmutableRuntimePrefix( |
| { |
| sessionId: 'session-1', |
| invocationId: 'invocation-repair-source', |
| runId: 'run-repair-source', |
| turnId: 'turn-repair-source', |
| }, |
| [ |
| { |
| eventSeq: 1, |
| event: functionCallEvent({ |
| id: 'repair-source-event', |
| sessionId: 'session-1', |
| invocationId: 'invocation-repair-source', |
| runId: 'run-repair-source', |
| turnId: 'turn-repair-source', |
| content: { kind: 'text', text: 'repair source request' }, |
| role: 'user', |
| author: 'user', |
| }), |
| }, |
| { |
| eventSeq: 2, |
| event: functionCallEvent({ |
| id: 'repair-source-terminal', |
| sessionId: 'session-1', |
| invocationId: 'invocation-repair-source', |
| runId: 'run-repair-source', |
| turnId: 'turn-repair-source', |
| ts: 2, |
| content: undefined, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }), |
| }, |
| ], |
| ); |
| const repairClaim = continuationClaimForBoundary( |
| createRuntimeBoundaryCursor([runtimePrefixSegment(repairSource)]), |
| { |
| claimId: 'continuation-claim-repair-protocol', |
| target: { |
| sessionId: 'session-1', |
| invocationId: 'invocation-repair-protocol', |
| runId: 'run-repair-protocol', |
| turnId: 'turn-repair-protocol', |
| }, |
| }, |
| ); |
| const repairEvent = continuationStartEvent(repairClaim, { |
| id: 'repair-start-with-protocol', |
| provenance: 'claim_repair', |
| toolBoundaryProtocol: 't1_after_preflight_v1', |
| }); |
| await persistImmutablePrefix(store, repairSource); |
| assert.equal((await store.claimContinuation({ claim: repairClaim })).kind, 'acquired'); |
| await assert.rejects( |
| store.commitContinuationRepairStart({ claim: repairClaim, event: repairEvent }), |
| /invalid continuation-start authority event/i, |
| ); |
| }); |
| }); |
| |
| it('seals a continuation invocation after its terminal fact while allowing exact retry', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const start = continuationStartEvent(claim); |
| const terminal: RuntimeEvent = { |
| id: 'continuation-terminal-1', |
| ...claim.target, |
| ts: 13, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { |
| endInvocation: true, |
| stateDelta: { failureClass: 'continuation_test_terminal' }, |
| }, |
| }; |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| await store.commitContinuationStart({ claim, event: start }); |
| await store.ensureTerminalRuntimeEventDurable( |
| claim.target.sessionId, |
| claim.target.runId, |
| terminal, |
| ); |
| await store.ensureTerminalRuntimeEventDurable( |
| claim.target.sessionId, |
| claim.target.runId, |
| terminal, |
| ); |
| |
| await assert.rejects( |
| store.appendRuntimeEvent(claim.target.sessionId, claim.target.runId, { |
| id: 'post-terminal-model-event', |
| ...claim.target, |
| ts: 14, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'must not be appended' }, |
| }), |
| /sealed by its terminal fact/i, |
| ); |
| await assert.rejects( |
| store.appendRuntimeEvent(claim.target.sessionId, claim.target.runId, { |
| id: 'post-terminal-fresh-invocation', |
| sessionId: claim.target.sessionId, |
| invocationId: 'fresh-invocation-after-terminal', |
| runId: claim.target.runId, |
| turnId: claim.target.turnId, |
| ts: 15, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'must not bypass the run terminal seal' }, |
| }), |
| /run identity conflict|sealed by its terminal fact/i, |
| ); |
| assert.deepEqual( |
| (await store.readImmutableRuntimeEvents(claim.target.sessionId, claim.target.runId)).map( |
| (event) => event.id, |
| ), |
| [start.id, terminal.id], |
| ); |
| }); |
| }); |
| |
| it('does not bless an exact terminal retry when a corrupt suffix follows it', async () => { |
| await withStore(async (store, dbPath) => { |
| const terminal: RuntimeEvent = { |
| id: 'terminal-before-corrupt-suffix', |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| ts: 2, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }; |
| await store.appendRuntimeEvent('session-1', 'run-1', terminal); |
| store.close(); |
| |
| const suffix: RuntimeEvent = { |
| id: 'corrupt-post-terminal-suffix', |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| ts: 3, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'must make the ledger invalid' }, |
| }; |
| const raw = new DatabaseSync(dbPath); |
| try { |
| raw |
| .prepare(` |
| INSERT INTO runtime_events ( |
| event_id, session_id, invocation_id, run_id, turn_id, event_seq, |
| event_kind, payload_json, committed_at |
| ) VALUES (?, ?, ?, ?, ?, 2, 'text', ?, ?) |
| `) |
| .run( |
| suffix.id, |
| suffix.sessionId, |
| suffix.invocationId, |
| suffix.runId, |
| suffix.turnId, |
| JSON.stringify(suffix), |
| suffix.ts, |
| ); |
| } finally { |
| raw.close(); |
| } |
| |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| await assert.rejects( |
| reopened.ensureTerminalRuntimeEventDurable('session-1', 'run-1', terminal), |
| /terminal RuntimeEvent must be the immutable ledger tail/i, |
| ); |
| await assert.rejects( |
| reopened.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'append-after-corrupt-terminal-suffix', |
| ts: 4, |
| content: { kind: 'text', text: 'must remain sealed' }, |
| }), |
| ), |
| /sealed by its terminal fact/i, |
| ); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| |
| it('rejects a continuation-start whose provider replay identity differs from its claim', async () => { |
| await withStore(async (store) => { |
| const claim = continuationClaim(); |
| const event = continuationStartEvent(claim); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| |
| await assert.rejects( |
| store.commitContinuationStart({ |
| claim, |
| event: { |
| ...event, |
| actions: { |
| continuationStart: { |
| ...event.actions!.continuationStart!, |
| providerReplayDigest: `sha256:${'c'.repeat(64)}`, |
| }, |
| }, |
| }, |
| }), |
| /invalid continuation-start authority event/i, |
| ); |
| await assert.rejects( |
| store.commitContinuationStart({ |
| claim, |
| event: { ...event, ts: claim.claimedAt - 1 }, |
| }), |
| /invalid continuation-start authority event/i, |
| ); |
| assert.deepEqual(await store.readContinuationClaimStateByBoundary(claim.boundaryDigest), { |
| claim, |
| }); |
| }); |
| }); |
| |
| it('rolls back continuation-start when failure occurs after the event insert', async () => { |
| await withStore(async (store, _dbPath, setFailpoint) => { |
| const claim = continuationClaim(); |
| const event = continuationStartEvent(claim); |
| await persistImmutablePrefix(store, continuationSourcePrefix()); |
| assert.equal((await store.claimContinuation({ claim })).kind, 'acquired'); |
| setFailpoint('after_continuation_start_insert'); |
| |
| await assert.rejects( |
| store.commitContinuationStart({ claim, event }), |
| /after_continuation_start_insert/, |
| ); |
| assert.deepEqual( |
| await store.readImmutableRuntimeEvents(claim.target.sessionId, claim.target.runId), |
| [], |
| ); |
| assert.deepEqual(await store.readContinuationClaimByBoundary(claim.boundaryDigest), claim); |
| |
| setFailpoint(undefined); |
| assert.deepEqual(await store.commitContinuationStart({ claim, event }), { |
| created: true, |
| runtimeEventSeq: 1, |
| }); |
| }); |
| }); |
| |
| it('pins a physical immutable prefix independently of mutable partial snapshots', async () => { |
| await withStore(async (store) => { |
| const first = functionCallEvent({ |
| id: 'user-event-1', |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'hello' }, |
| }); |
| await store.appendRuntimeEvent('session-1', 'run-1', first); |
| const beforePartial = await store.readImmutableRuntimePrefix({ |
| sessionId: 'session-1', |
| runId: 'run-1', |
| }); |
| |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'partial-1', |
| ts: 2, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'working' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| const afterPartial = await store.readImmutableRuntimePrefix({ |
| sessionId: 'session-1', |
| runId: 'run-1', |
| }); |
| |
| assert.equal((await store.readRuntimeEvents('session-1', 'run-1')).length, 2); |
| assert.deepEqual(afterPartial.position, { |
| lastEventSeq: 1, |
| eventCount: 1, |
| lastEventId: 'user-event-1', |
| }); |
| assert.equal(afterPartial.prefixDigest, beforePartial.prefixDigest); |
| |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'model-event-2', |
| ts: 3, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'done' }, |
| }), |
| ); |
| const pinned = await store.readImmutableRuntimePrefix({ |
| sessionId: 'session-1', |
| runId: 'run-1', |
| upToEventSeq: 1, |
| }); |
| const latest = await store.readImmutableRuntimePrefix({ |
| sessionId: 'session-1', |
| runId: 'run-1', |
| }); |
| const proof = await store.readImmutableRuntimePrefixProof( |
| { sessionId: 'session-1', runId: 'run-1' }, |
| PREFIX_PROOF_TEST_BUDGET, |
| ); |
| const pinnedProof = await store.readImmutableRuntimePrefixProof( |
| { sessionId: 'session-1', runId: 'run-1', upToEventSeq: 1 }, |
| PREFIX_PROOF_TEST_BUDGET, |
| ); |
| |
| assert.equal(pinned.prefixDigest, beforePartial.prefixDigest); |
| assert.equal(latest.position.lastEventSeq, 2); |
| assert.notEqual(latest.prefixDigest, beforePartial.prefixDigest); |
| assert.equal(proof.prefixDigest, latest.prefixDigest); |
| assert.deepEqual(proof.position, latest.position); |
| assert.equal(proof.firstEvent.id, 'user-event-1'); |
| assert.equal(proof.lastEvent.id, 'model-event-2'); |
| assert.equal(pinnedProof.prefixDigest, pinned.prefixDigest); |
| assert.equal(pinnedProof.firstEvent.id, pinnedProof.lastEvent.id); |
| for (const [budget, message] of [ |
| [{ ...PREFIX_PROOF_TEST_BUDGET, maxEvents: 1 }, /event limit/], |
| [{ ...PREFIX_PROOF_TEST_BUDGET, maxBytes: 1 }, /byte limit/], |
| [{ ...PREFIX_PROOF_TEST_BUDGET, maxRecordBytes: 1 }, /record byte limit/], |
| ] as const) { |
| await assert.rejects( |
| store.readImmutableRuntimePrefixProof({ sessionId: 'session-1', runId: 'run-1' }, budget), |
| message, |
| ); |
| } |
| }); |
| }); |
| |
| it('stores a partial batch as one append-only segment and reconstructs the same text', async () => { |
| await withStore(async (store, dbPath) => { |
| const partial = (id: string, ts: number, text: string): RuntimeEvent => |
| functionCallEvent({ |
| id, |
| ts, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text }, |
| refs: { providerEventId: 'message-1' }, |
| }); |
| await store.appendRuntimeEvent('session-1', 'run-1', partial('partial-1', 1, 'a')); |
| await store.appendRuntimePartialBatch('session-1', 'run-1', [ |
| partial('partial-2', 2, 'b'), |
| partial('partial-3', 3, 'c'), |
| ]); |
| |
| const events = await store.readRuntimeEvents('session-1', 'run-1'); |
| assert.equal(events.length, 1); |
| assert.equal(events[0]?.content?.kind, 'text'); |
| assert.equal(events[0]?.content?.kind === 'text' ? events[0].content.text : undefined, 'abc'); |
| |
| const inspect = new DatabaseSync(dbPath); |
| try { |
| assert.deepEqual( |
| inspect |
| .prepare(` |
| SELECT segment_seq, text_content |
| FROM runtime_partial_segments |
| ORDER BY segment_seq ASC |
| `) |
| .all() |
| .map((row) => ({ ...row })), |
| [{ segment_seq: 1, text_content: 'abc' }], |
| ); |
| } finally { |
| inspect.close(); |
| } |
| }); |
| }); |
| |
| it('coalesces partial text into fixed-size tail segments', async () => { |
| await withStore(async (store, dbPath) => { |
| const chunks = ['x'.repeat(40 * 1024), 'y'.repeat(40 * 1024), 'z']; |
| for (const [index, text] of chunks.entries()) { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: `partial-${index}`, |
| ts: index + 1, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| } |
| |
| const events = await store.readRuntimeEvents('session-1', 'run-1'); |
| assert.equal(events[0]?.content?.kind, 'text'); |
| assert.equal( |
| events[0]?.content?.kind === 'text' ? events[0].content.text : undefined, |
| chunks.join(''), |
| ); |
| |
| const inspect = new DatabaseSync(dbPath); |
| try { |
| const snapshot = inspect |
| .prepare('SELECT text_content FROM runtime_partial_snapshots') |
| .get() as { text_content?: unknown }; |
| const segments = inspect |
| .prepare(` |
| SELECT segment_seq, length(CAST(text_content AS BLOB)) AS stored_bytes |
| FROM runtime_partial_segments |
| ORDER BY segment_seq ASC |
| `) |
| .all() |
| .map((row) => ({ ...row })); |
| assert.equal(snapshot.text_content, ''); |
| assert.deepEqual(segments, [ |
| { segment_seq: 1, stored_bytes: 40 * 1024 }, |
| { segment_seq: 2, stored_bytes: 40 * 1024 + 1 }, |
| ]); |
| } finally { |
| inspect.close(); |
| } |
| }); |
| }); |
| |
| it('rejects a partial batch that crosses presentation streams atomically', async () => { |
| await withStore(async (store) => { |
| const partial = (id: string, providerEventId: string, text: string): RuntimeEvent => |
| functionCallEvent({ |
| id, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text }, |
| refs: { providerEventId }, |
| }); |
| await assert.rejects( |
| store.appendRuntimePartialBatch('session-1', 'run-1', [ |
| partial('partial-1', 'message-1', 'a'), |
| partial('partial-2', 'message-2', 'b'), |
| ]), |
| /exactly one presentation stream/, |
| ); |
| assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), []); |
| }); |
| }); |
| |
| it('rejects a physical immutable prefix with an event-seq gap', async () => { |
| await withStore(async (store, dbPath) => { |
| for (let eventSeq = 1; eventSeq <= 3; eventSeq += 1) { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: `event-${eventSeq}`, |
| ts: eventSeq, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: String(eventSeq) }, |
| }), |
| ); |
| } |
| store.close(); |
| const raw = new DatabaseSync(dbPath); |
| try { |
| raw.prepare('DELETE FROM runtime_events WHERE event_seq = 2').run(); |
| } finally { |
| raw.close(); |
| } |
| |
| const reopened = createSqliteRuntimeStore(dbPath); |
| try { |
| await assert.rejects( |
| reopened.readImmutableRuntimePrefix({ |
| sessionId: 'session-1', |
| runId: 'run-1', |
| }), |
| /event_seq gap/, |
| ); |
| await assert.rejects( |
| reopened.readImmutableRuntimePrefixProof( |
| { sessionId: 'session-1', runId: 'run-1' }, |
| PREFIX_PROOF_TEST_BUDGET, |
| ), |
| /event_seq gap/, |
| ); |
| } finally { |
| reopened.close(); |
| } |
| }); |
| }); |
| |
| it('replaces text and tool partial snapshots when their durable final arrives', async () => { |
| await withStore(async (store, dbPath) => { |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'text-partial', |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'working' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'tool-partial', |
| partial: true, |
| role: 'tool', |
| author: 'tool', |
| content: undefined, |
| refs: { toolCallId: 'provider-call-1' }, |
| }), |
| ); |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionCallEvent({ |
| id: 'text-final', |
| ts: 2, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: 'done' }, |
| refs: { providerEventId: 'message-1' }, |
| }), |
| ); |
| await store.appendRuntimeEvent('session-1', 'run-1', functionCallEvent()); |
| await store.appendRuntimeEvent( |
| 'session-1', |
| 'run-1', |
| functionResponseEvent({ |
| refs: { toolCallId: 'provider-call-1' }, |
| }), |
| ); |
| |
| assert.deepEqual( |
| (await store.readRuntimeEvents('session-1', 'run-1')).map((event) => event.id), |
| ['text-final', 'call-event-1', 'response-event-1'], |
| ); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3); |
| const inspect = new DatabaseSync(dbPath); |
| try { |
| assert.equal( |
| ( |
| inspect.prepare('SELECT count(*) AS count FROM runtime_partial_segments').get() as { |
| count: number; |
| } |
| ).count, |
| 0, |
| ); |
| } finally { |
| inspect.close(); |
| } |
| }); |
| }); |
| |
| it('uses the immutable SQLite event as the steering-message recovery proof', async () => { |
| await withStore(async (store) => { |
| const steering = functionCallEvent({ |
| id: 'steering-event-1', |
| content: { kind: 'text', text: 'steer', steering: true }, |
| refs: { providerEventId: 'message-steering' }, |
| }); |
| |
| await store.appendRuntimeEvent('session-1', 'run-1', steering, { durable: true }); |
| await store.appendRuntimeEvent('session-1', 'run-1', steering, { durable: true }); |
| |
| assert.deepEqual( |
| await store.readImmutableSteeringMessageProof('session-1', 'message-steering'), |
| { event: steering }, |
| ); |
| await store.repairImmutableSteeringMessageProofsForRecovery('session-1'); |
| await assert.rejects( |
| store.appendRuntimeEvent( |
| 'session-1', |
| 'run-2', |
| functionCallEvent({ |
| id: 'steering-event-conflict', |
| invocationId: 'invocation-2', |
| runId: 'run-2', |
| turnId: 'turn-2', |
| content: { kind: 'text', text: 'different', steering: true }, |
| refs: { providerEventId: 'message-steering' }, |
| }), |
| ), |
| /Immutable steering message identity conflict: message-steering/, |
| ); |
| assert.deepEqual(await store.readImmutableRuntimeEvents('session-1', 'run-2'), []); |
| }); |
| }); |
| }); |
| |
| type Store = ReturnType<typeof createSqliteRuntimeStore>; |
| |
| async function withStore( |
| run: ( |
| store: Store, |
| dbPath: string, |
| setFailpoint: (point: SqliteRuntimeStoreFailpoint | undefined) => void, |
| ) => Promise<void>, |
| ): Promise<void> { |
| const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-')); |
| const dbPath = join(root, 'runtime.sqlite'); |
| let failpoint: SqliteRuntimeStoreFailpoint | undefined; |
| const store = createSqliteRuntimeStore(dbPath, { |
| failpoint: (point) => { |
| if (failpoint === point) throw new Error(`sqlite runtime failpoint: ${point}`); |
| }, |
| }); |
| try { |
| await run(store, dbPath, (point) => { |
| failpoint = point; |
| }); |
| } finally { |
| store.close(); |
| await rm(root, { recursive: true, force: true }); |
| } |
| } |
| |
| function continuationClaim( |
| input: Parameters<typeof continuationClaimForBoundary>[1] = {}, |
| ): ContinuationClaimV1 { |
| const boundary = createRuntimeBoundaryCursor([runtimePrefixSegment(continuationSourcePrefix())]); |
| return continuationClaimForBoundary(boundary, input); |
| } |
| |
| function continuationClaimForBoundary( |
| boundary: ContinuationClaimV1['boundary'], |
| input: { |
| claimId?: string; |
| claimedAt?: number; |
| target?: ContinuationClaimV1['target']; |
| } = {}, |
| ): ContinuationClaimV1 { |
| const source = boundary.segments.at(-1)!; |
| const target = |
| input.target ?? |
| ({ |
| sessionId: 'session-1', |
| invocationId: 'invocation-2', |
| runId: 'run-2', |
| turnId: 'turn-2', |
| } satisfies ContinuationClaimV1['target']); |
| const claimId = input.claimId ?? 'claim-1'; |
| const claimedAt = input.claimedAt ?? 10; |
| return { |
| protocol: 'continuation_claim_v1', |
| claimId, |
| boundaryDigest: boundary.manifestDigest, |
| boundary, |
| providerProjectionVersion: 1, |
| providerReplayDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
| target, |
| targetOpening: { |
| kind: 'invocation_opened', |
| protocol: 'invocation_opened_v1', |
| route: { |
| provenance: 'unknown', |
| backendKind: 'fake', |
| llmConnectionSlug: 'connection-1', |
| modelId: 'model-1', |
| }, |
| configuration: { |
| cwd: '/workspace/repo', |
| permissionMode: 'ask', |
| collaborationMode: 'agent', |
| orchestrationMode: 'default', |
| orchestrationSource: 'session', |
| toolMode: DEFAULT_TOOL_MODE, |
| agentSwarmAuthorization: 'none', |
| }, |
| root: { kind: 'user' }, |
| source: { |
| kind: 'continuation', |
| sourceInvocationId: source.identity.invocationId, |
| sourceRunId: source.identity.runId, |
| sourceTurnId: source.identity.turnId, |
| sourceRuntimeEventHighWater: source.position.lastEventSeq, |
| claimId, |
| boundaryDigest: boundary.manifestDigest, |
| }, |
| lineage: { |
| parentRunId: source.identity.runId, |
| parentTurnId: source.identity.turnId, |
| }, |
| }, |
| claimedAt, |
| }; |
| } |
| |
| function continuationSourcePrefix(): ImmutableRuntimePrefixV1 { |
| return buildImmutableRuntimePrefix( |
| { |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| }, |
| [ |
| ...activeContinuationSourcePrefix().events.map((event, index) => ({ |
| eventSeq: index + 1, |
| event, |
| })), |
| { |
| eventSeq: 2, |
| event: functionCallEvent({ |
| id: 'source-terminal-1', |
| ts: 2, |
| content: undefined, |
| role: 'system', |
| author: 'system', |
| status: 'failed', |
| actions: { endInvocation: true }, |
| }), |
| }, |
| ], |
| ); |
| } |
| |
| function activeContinuationSourcePrefix(): ImmutableRuntimePrefixV1 { |
| return buildImmutableRuntimePrefix( |
| { |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| }, |
| [ |
| { |
| eventSeq: 1, |
| event: functionCallEvent({ |
| id: 'source-user-1', |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: 'continue this interrupted Run' }, |
| }), |
| }, |
| ], |
| ); |
| } |
| |
| async function persistImmutablePrefix( |
| store: Store, |
| prefix: ImmutableRuntimePrefixV1, |
| ): Promise<void> { |
| for (const runtimeEvent of prefix.events) { |
| await store.appendRuntimeEvent(prefix.identity.sessionId, prefix.identity.runId, runtimeEvent); |
| } |
| } |
| |
| function continuationStartEvent( |
| claim: ContinuationClaimV1, |
| overrides: { |
| id?: string; |
| provenance?: 'runtime_admission' | 'claim_repair'; |
| toolBoundaryProtocol?: 't1_after_preflight_v1'; |
| } = {}, |
| ): RuntimeEvent { |
| const source = claim.boundary.segments.at(-1)!; |
| return { |
| id: overrides.id ?? 'continuation-start-1', |
| ...claim.target, |
| ts: 12, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| modelVisibility: 'hidden', |
| content: claim.targetOpening, |
| actions: { |
| ...(overrides.toolBoundaryProtocol |
| ? { runtimeProtocol: { toolBoundary: overrides.toolBoundaryProtocol } } |
| : {}), |
| continuationStart: { |
| protocol: 'continuation_start_v2', |
| provenance: overrides.provenance ?? 'runtime_admission', |
| claimId: claim.claimId, |
| boundaryDigest: claim.boundaryDigest, |
| immediateSource: { |
| sessionId: source.identity.sessionId, |
| invocationId: source.identity.invocationId, |
| runId: source.identity.runId, |
| turnId: source.identity.turnId, |
| highWater: source.position.lastEventSeq, |
| prefixDigest: source.prefixDigest, |
| }, |
| replayManifestDigest: claim.boundary.manifestDigest, |
| providerProjectionVersion: claim.providerProjectionVersion, |
| providerReplayDigest: claim.providerReplayDigest, |
| }, |
| }, |
| }; |
| } |
| |
| /** A DatabaseSync that records what each statement was actually run with. */ |
| function watchStatements( |
| db: DatabaseSync, |
| executed: { sql: string; bind: unknown[] }[], |
| ): DatabaseSync { |
| return { |
| prepare(sql: string) { |
| const statement = db.prepare(sql); |
| const record = |
| <T>(call: (...bind: unknown[]) => T) => |
| (...bind: unknown[]) => { |
| executed.push({ sql, bind }); |
| return call(...bind); |
| }; |
| return { |
| all: record((...bind) => statement.all(...(bind as []))), |
| get: record((...bind) => statement.get(...(bind as []))), |
| iterate: record((...bind) => statement.iterate(...(bind as []))), |
| }; |
| }, |
| } as unknown as DatabaseSync; |
| } |
| |
| async function appendSettledTurn(store: Store, index: number): Promise<void> { |
| const run = { |
| sessionId: 'session-1', |
| invocationId: `invocation-${index}`, |
| runId: `run-${index}`, |
| turnId: `turn-${index}`, |
| }; |
| await store.appendRuntimeEvent( |
| run.sessionId, |
| run.runId, |
| buildInvocationOpenedEvent({ |
| id: `opened-${index}`, |
| run, |
| openedAt: index * 10, |
| opening: { |
| kind: 'invocation_opened', |
| protocol: 'invocation_opened_v1', |
| route: { |
| provenance: 'runtime', |
| backendKind: 'fake', |
| llmConnectionId: 'fake-connection', |
| llmConnectionSlug: 'fake', |
| modelId: 'fake-model', |
| }, |
| configuration: { |
| cwd: '/tmp', |
| permissionMode: 'ask', |
| collaborationMode: 'agent', |
| orchestrationMode: 'default', |
| orchestrationSource: 'session', |
| toolMode: DEFAULT_TOOL_MODE, |
| }, |
| root: { kind: 'user' }, |
| source: { kind: 'fresh' }, |
| }, |
| }), |
| ); |
| await store.appendRuntimeEvent(run.sessionId, run.runId, { |
| id: `prompt-${index}`, |
| ...run, |
| ts: index * 10 + 1, |
| partial: false, |
| role: 'user', |
| author: 'user', |
| content: { kind: 'text', text: `turn ${index}` }, |
| }); |
| await store.appendRuntimeEvent(run.sessionId, run.runId, { |
| id: `terminal-${index}`, |
| ...run, |
| ts: index * 10 + 2, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| status: 'completed', |
| actions: { endInvocation: true }, |
| }); |
| } |
| |
| function textEvent(id: string): RuntimeEvent { |
| return functionCallEvent({ id, content: { kind: 'text', text: id } }); |
| } |
| |
| function functionCallEvent(overrides: Partial<RuntimeEvent> = {}): RuntimeEvent { |
| return { |
| id: 'call-event-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| sessionId: 'session-1', |
| turnId: 'turn-1', |
| ts: 1, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: 'provider-call-1', |
| name: 'Read', |
| args: { path: '/workspace/repo/README.md' }, |
| }, |
| ...overrides, |
| }; |
| } |
| |
| function functionResponseEvent(overrides: Partial<RuntimeEvent> = {}): RuntimeEvent { |
| return { |
| id: 'response-event-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| sessionId: 'session-1', |
| turnId: 'turn-1', |
| ts: 2, |
| partial: false, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: 'provider-call-1', |
| name: 'Read', |
| result: 'contents', |
| }, |
| refs: { operationId: 'operation-1', toolCallId: 'provider-call-1' }, |
| ...overrides, |
| }; |
| } |
| |
| function toolDispatchEvent(overrides: Partial<RuntimeEvent> = {}): RuntimeEvent { |
| return { |
| id: 'dispatch-event-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| sessionId: 'session-1', |
| turnId: 'turn-1', |
| ts: 10, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'operation-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| }, |
| }, |
| refs: { operationId: 'operation-1', toolCallId: 'provider-call-1' }, |
| ...overrides, |
| }; |
| } |
| |
| function commitPrepared(store: Store, options: { resultProjectionVersion?: 1 } = {}) { |
| return store.commitToolPrepared({ |
| operationId: 'operation-1', |
| journalEventId: 'operation-1_prepared', |
| runtimeEvent: functionCallEvent(), |
| dispatchRuntimeEvent: toolDispatchEvent({ |
| actions: { |
| toolDispatch: { |
| protocol: 't1_after_preflight_v1', |
| operationId: 'operation-1', |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| ...(options.resultProjectionVersion !== undefined |
| ? { resultProjectionVersion: options.resultProjectionVersion } |
| : {}), |
| }, |
| }, |
| }), |
| providerToolCallId: 'provider-call-1', |
| toolName: 'Read', |
| canonicalArgsHash: READ_ARGS_HASH, |
| recoveryMode: 'replay_safe', |
| committedAt: 10, |
| }); |
| } |
| |
| const READ_ARGS_HASH = canonicalToolArgsHash('Read', { |
| path: '/workspace/repo/README.md', |
| }); |
| const DIFFERENT_READ_ARGS_HASH = canonicalToolArgsHash('Read', { |
| path: '/workspace/repo/OTHER.md', |
| }); |