Skip to content

Commit 8cd09af

Browse files
committed
refactor(runtime): hold the test doubles to the seal, and name corruption
Two terminal events was classified as `ambiguous`. Nothing is ambiguous about it: a store seals a run on its first terminal, so a second one means the ledger was written around the seal. It is `corrupt`, and the strict recovery error now says what it found rather than that it was unsure. The seal itself was an obligation only one double honoured, which is what let that state look reachable in the first place. It is stated once now and used by every RuntimeEventStore double, so a double cannot drift from the SQLite store or from the interface. Six fixtures were appending to a finished run through the API that forbids it, using append as a way to write into the ledger rather than to test the seal. They now seed the double directly, which is where a ledger shape no supported store can produce belongs. Generated-by: Claude Code
1 parent 52ad1b3 commit 8cd09af

6 files changed

Lines changed: 88 additions & 11 deletions

File tree

packages/runtime/src/__tests__/agent-run-inspect.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from '@maka/core/runtime-invocation';
3030
import { inspectAgentRunReadModel } from '../agent-run-inspect.js';
3131
import { testInvocationOpening } from './invocation-fixture.js';
32+
import { assertDoubleRunNotSealed } from './runtime-event-store-seal.js';
3233

3334
const sessionId = 'session-1';
3435
const invocationId = 'inv-1';
@@ -186,6 +187,7 @@ class MemoryAgentRunStore implements AgentRunStore, RuntimeEventStore {
186187

187188
async appendRuntimeEvent(sessionId: string, runId: string, event: RuntimeEvent): Promise<void> {
188189
const eventKey = key(sessionId, runId);
190+
assertDoubleRunNotSealed(this.runtimeEvents.get(eventKey) ?? [], event);
189191
this.runtimeEvents.set(eventKey, [
190192
...(this.runtimeEvents.get(eventKey) ?? []),
191193
copyRuntimeEvent(event),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
21+
import type { RuntimeEvent } from '@maka/core/runtime-event';
22+
import { RunSealedError } from '@maka/core/runtime-event-store';
23+
24+
/**
25+
* The seal every `RuntimeEventStore` owes its callers, for the doubles.
26+
*
27+
* `RuntimeEventStore` requires an implementation to refuse any new event on a
28+
* run that already holds a terminal one. A double that skips it manufactures a
29+
* ledger no supported store can produce, and a test built on that ledger proves
30+
* nothing about production. Stated here once so the doubles cannot drift apart
31+
* from each other or from the SQLite store.
32+
*
33+
* A test that genuinely needs a corrupt ledger should assemble it underneath the
34+
* store rather than appending through it.
35+
*/
36+
export function assertDoubleRunNotSealed(
37+
storedEvents: readonly RuntimeEvent[],
38+
incoming: RuntimeEvent,
39+
): void {
40+
// An exact-id replay is idempotent: the event is already inside the seal.
41+
if (storedEvents.some((event) => event.id === incoming.id)) return;
42+
if (storedEvents.some(isTerminalRuntimeEvent)) throw new RunSealedError(incoming.runId);
43+
}

packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { RuntimeReadModel } from '../runtime-read-model.js';
6363
import { RuntimeKernel } from '../runtime-kernel.js';
6464
import type { RuntimeInteractionAuthority } from '../interaction-authority.js';
6565
import { testInvocationOpening } from './invocation-fixture.js';
66+
import { assertDoubleRunNotSealed } from './runtime-event-store-seal.js';
6667

6768
describe('SessionManager terminal ledger invariants', () => {
6869
test('coalesces one partial stream and flushes it before the final model event', async () => {
@@ -1042,7 +1043,7 @@ describe('SessionManager terminal ledger invariants', () => {
10421043
}),
10431044
]);
10441045

1045-
assert.strictEqual(result.kind, 'ambiguous');
1046+
assert.strictEqual(result.kind, 'corrupt');
10461047
assert.deepStrictEqual(
10471048
result.terminalEvents.map((event) => event.id),
10481049
['rt-completed', 'rt-failed'],
@@ -2394,6 +2395,7 @@ class BatchingRuntimeEventStore implements RuntimeEventStore {
23942395
constructor(private readonly failPartialBatch = false) {}
23952396

23962397
async appendRuntimeEvent(_sessionId: string, _runId: string, event: RuntimeEvent): Promise<void> {
2398+
assertDoubleRunNotSealed(this.events, event);
23972399
this.order.push(`append:${event.id}`);
23982400
this.events.push(clone(event));
23992401
}

packages/runtime/src/__tests__/session-manager.test.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import type {
9191
import { PlanConflictError, emptyPlanSessionState, type PlanStore } from '@maka/core/plan';
9292
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
9393
import { createTestAiSdkBackend } from './execution-boundary-test-helpers.js';
94+
import { assertDoubleRunNotSealed } from './runtime-event-store-seal.js';
9495
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
9596
import { z } from 'zod';
9697
import { AiSdkBackend } from '../ai-sdk-backend.js';
@@ -6011,7 +6012,7 @@ describe('SessionManager permission mode updates', () => {
60116012

60126013
const targetRunId = firstPlan.continuation.runId;
60136014
const targetRun = await readInvocation(runStore, session.id, targetRunId);
6014-
await runStore.appendRuntimeEvent(
6015+
runStore.seedRuntimeEvent(
60156016
session.id,
60166017
targetRunId,
60176018
runtimeEvent({
@@ -6708,7 +6709,7 @@ describe('SessionManager permission mode updates', () => {
67086709
});
67096710
if (!plan.continuation) throw new Error('expected continuation');
67106711

6711-
await runStore.appendRuntimeEvent(
6712+
runStore.seedRuntimeEvent(
67126713
session.id,
67136714
sourceRunId,
67146715
runtimeEvent({
@@ -10053,7 +10054,7 @@ describe('SessionManager permission mode updates', () => {
1005310054
ts: 120 + index,
1005410055
}),
1005510056
);
10056-
await runStore.appendRuntimeEvent(
10057+
runStore.seedRuntimeEvent(
1005710058
session.id,
1005810059
'child-run',
1005910060
runtimeEvent({
@@ -10122,7 +10123,7 @@ describe('SessionManager permission mode updates', () => {
1012210123
permissionMode: 'explore',
1012310124
}),
1012410125
);
10125-
await runStore.appendRuntimeEvent(
10126+
runStore.seedRuntimeEvent(
1012610127
session.id,
1012710128
'child-run',
1012810129
runtimeEvent({
@@ -10136,7 +10137,7 @@ describe('SessionManager permission mode updates', () => {
1013610137
content: { kind: 'text', text: 'x'.repeat(64 * 1024) },
1013710138
}),
1013810139
);
10139-
await runStore.appendRuntimeEvent(
10140+
runStore.seedRuntimeEvent(
1014010141
session.id,
1014110142
'child-run',
1014210143
runtimeEvent({
@@ -10467,7 +10468,7 @@ describe('SessionManager permission mode updates', () => {
1046710468
}
1046810469

1046910470
const [run] = await runStore.listSessionInvocations(session.id);
10470-
await runStore.appendRuntimeEvent(
10471+
runStore.seedRuntimeEvent(
1047110472
session.id,
1047210473
run!.runId,
1047310474
runtimeEvent({
@@ -13185,6 +13186,17 @@ class MemoryAgentRunStore
1318513186
this.options.failRuntimeEventAppendAfter = undefined;
1318613187
throw new Error('runtime event append failed');
1318713188
}
13189+
assertDoubleRunNotSealed(this.runtimeEvents.get(key(sessionId, runId)) ?? [], event);
13190+
this.seedRuntimeEvent(sessionId, runId, event);
13191+
}
13192+
13193+
/**
13194+
* Put an event into the ledger underneath the seal.
13195+
*
13196+
* A test that needs a ledger shape the store would refuse to write has to
13197+
* assemble it below the store, not through the API whose contract forbids it.
13198+
*/
13199+
seedRuntimeEvent(sessionId: string, runId: string, event: RuntimeEvent): void {
1318813200
const eventKey = key(sessionId, runId);
1318913201
this.runtimeEvents.set(eventKey, [
1319013202
...(this.runtimeEvents.get(eventKey) ?? []),
@@ -13566,6 +13578,17 @@ class MemoryRuntimeEventStore implements RuntimeEventStore {
1356613578

1356713579
async appendRuntimeEvent(sessionId: string, runId: string, event: RuntimeEvent): Promise<void> {
1356813580
if (this.options.failRuntimeEventAppends) throw new Error('runtime event append failed');
13581+
assertDoubleRunNotSealed(this.runtimeEvents.get(key(sessionId, runId)) ?? [], event);
13582+
this.seedRuntimeEvent(sessionId, runId, event);
13583+
}
13584+
13585+
/**
13586+
* Put an event into the ledger underneath the seal.
13587+
*
13588+
* A test that needs a ledger shape the store would refuse to write has to
13589+
* assemble it below the store, not through the API whose contract forbids it.
13590+
*/
13591+
seedRuntimeEvent(sessionId: string, runId: string, event: RuntimeEvent): void {
1356913592
const eventKey = key(sessionId, runId);
1357013593
this.runtimeEvents.set(eventKey, [
1357113594
...(this.runtimeEvents.get(eventKey) ?? []),

packages/runtime/src/session-manager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4592,9 +4592,11 @@ export class SessionManager {
45924592
}
45934593
}
45944594
const terminalLedger = classifyTerminalRuntimeLedger(run, inspected.runtimeEvents);
4595-
if (terminalLedger.kind === 'ambiguous') {
4595+
if (terminalLedger.kind === 'corrupt') {
45964596
if (policy.kind === 'strict') {
4597-
throw new Error(`RuntimeEvent ledger has ambiguous terminal facts for run ${run.runId}`);
4597+
throw new Error(
4598+
`RuntimeEvent ledger has more than one terminal event for run ${run.runId}`,
4599+
);
45984600
}
45994601
continue;
46004602
}

packages/runtime/src/terminal-run-commit.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ export type TerminalRuntimeLedgerClassification =
4747
terminalEvents: readonly RuntimeEvent[];
4848
}
4949
| {
50-
kind: 'ambiguous';
50+
/**
51+
* More than one terminal event. Nothing ambiguous about it: a store seals
52+
* a run on its first terminal, so a second one means the ledger was
53+
* written around that seal and is corrupt.
54+
*/
55+
kind: 'corrupt';
5156
terminalEvents: readonly RuntimeEvent[];
5257
};
5358

@@ -60,7 +65,7 @@ export function classifyTerminalRuntimeLedger(
6065
return { kind: 'none', terminalEvents };
6166
}
6267
if (terminalEvents.length > 1) {
63-
return { kind: 'ambiguous', terminalEvents };
68+
return { kind: 'corrupt', terminalEvents };
6469
}
6570

6671
const fact = classifyRuntimeEventTerminalFact(run, events).fact;

0 commit comments

Comments
 (0)