blob: 45a575c8eccbc9a3c67f08ea567987d1dd2bb24a [file]
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { setTimeout as timerDelay } from 'node:timers/promises';
import { createHash } from 'node:crypto';
import {
applySandboxBoundaryExpansion,
createGenesisExecutionBoundary,
createWorkspaceWritePermissionProfile,
DEEP_RESEARCH_SESSION_LABEL,
SIDE_CONVERSATION_SESSION_LABEL,
RUNTIME_CONTINUATION_AUTHORITY_V1,
deriveTurnRecords,
isSandboxBoundaryRestartClosure,
isSessionInlineRun,
isTerminalRuntimeEvent,
} from '@maka/core';
import { canonicalToolArgsHash } from '@maka/core';
import { buildImmutableRuntimePrefix, decodeContinuationClaim } from '@maka/core';
import type {
CreateSandboxBoundaryRequest,
SandboxBoundaryRequest,
SandboxBoundaryResponse,
SandboxBoundarySettlement,
SettleSandboxBoundaryRequest,
} from '@maka/core';
import type {
CreateSessionInput,
ExecutionBoundary,
QueueEnqueueOutcome,
AgentGraphIntentClaim,
AgentGraphIntentClaimStore,
AgentGraphOperatorProvisionRequest,
AgentGraphOperatorProvisionResult,
AgentRunEvent,
EmittedAgentRunEvent,
AgentRunHeader,
AgentRunStore,
ArtifactRecord,
ContinuationClaimV1,
RuntimeBoundaryDigest,
RuntimeContinuationAuthorityStore,
RuntimeEvent,
RuntimeEventStore,
RootExecutionDescriptor,
SessionEvent,
SessionHeader,
SessionListFilter,
SessionSummary,
ShellRunSnapshotResult,
StoredMessage,
TurnRecord,
} from '@maka/core';
import type { BackendSendInput, BackendStopMode } from '@maka/core';
import { PlanConflictError, emptyPlanSessionState, type PlanStore } from '@maka/core';
import { expect } from '../test-helpers.js';
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
import { createTestAiSdkBackend } from './execution-boundary-test-helpers.js';
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
import { z } from 'zod';
import { AiSdkBackend } from '../ai-sdk-backend.js';
import {
BackendRegistry,
SessionConfigurationRevisionConflictError,
SessionConfigurationTransitionError,
SessionManager,
headerToSummary,
type BackendFactoryContext,
type SessionConfigurationStoreUpdate,
type SessionStore,
type VersionedSessionHeader,
} from '../session-manager.js';
import {
RuntimeContextCompactError,
RuntimeKernel,
type RuntimeKernelLike,
} from '../runtime-kernel.js';
import { FAKE_ASK_USER_QUESTION_PROMPT, FakeBackend } from '../fake-backend.js';
import { RuntimeReadModel, RuntimeReadModelError } from '../runtime-read-model.js';
import type { AgentBackend } from '@maka/core';
import type { MakaTool } from '../tool-runtime.js';
import type { ShellRunProcessManager } from '../shell-run-manager.js';
import type { InvocationResult } from '../invocation-context.js';
import type { RuntimeCommitSink } from '../runtime-commit-sink.js';
import type { ActiveFullCompactBlock } from '../active-full-compact.js';
import {
buildHistoryCompactCheckpoint,
type HistoryCompactCheckpoint,
} from '../history-compact-checkpoint.js';
import { buildLlmHistorySummarizer } from '../history-compact-summarizer.js';
import { decodeModelCallAttempt, type ModelCallAttempt } from '@maka/core';
import {
AGENT_WORKSPACE_WORKTREE,
IMPLEMENTATION_AGENT_DEFINITION,
IMPLEMENTATION_AGENT_ID,
LOCAL_READ_AGENT_DEFINITION,
LOCAL_READ_AGENT_ID,
LOCAL_READ_AGENT_PROFILE,
WEB_RESEARCH_AGENT_DEFINITION,
WEB_RESEARCH_AGENT_ID,
} from '../agent-catalog.js';
import {
RuntimeMessageAuthorityInvariantError,
type RuntimeHostedRootAuthority,
type RuntimeMessageRunIdentity,
} from '../message-authority.js';
import {
RuntimeInteractionInvariantError,
type CanonicalPermissionOutcomeReader,
type CanonicalPermissionOutcomeRecord,
type RuntimeInteractionAuthority,
type RuntimeInteractionRunIdentity,
type RuntimeUserQuestionContinuation,
} from '../interaction-authority.js';
import {
claimAgentGraphRunnableIntent,
fingerprintAgentGraphRunnableIntent,
} from '../stream-graph-admission.js';
import type { AgentGraphRunnableIntent } from '../stream-graph-readiness.js';
/**
* "A turn is running" is a fact about the live process, so the read model takes
* it from the run rather than from the persisted status. The status cannot
* serve: it is written only at the END of `AgentRun.begin`, it reads the same
* before a turn starts and after it ends, and a crash between a turn's end and
* its status write leaves `running` in storage forever.
*/
describe('SessionManager running-turn projection', () => {
test('names the turn a session is running, without persisting it', async () => {
const store = new MemorySessionStore();
const runningTurnBySession = new Map<string, string[]>();
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
runtimeKernel: {
runningTurnIds: (sessionId: string) => runningTurnBySession.get(sessionId) ?? [],
} as never,
});
const idle = await manager.createSession(makeInput({ name: 'Idle' }));
const busy = await manager.createSession(makeInput({ name: 'Busy' }));
runningTurnBySession.set(busy.id, ['turn-live']);
const listed = await manager.listSessions();
const byId = new Map(listed.map((session) => [session.id, session]));
expect(byId.get(busy.id)?.runningTurnIds).toEqual(['turn-live']);
expect(byId.get(idle.id)?.runningTurnIds).toEqual(undefined);
// Nothing about it reached storage, which is what makes a restart honest.
expect(
((await store.readHeader(busy.id)) as unknown as Record<string, unknown>).runningTurnId,
).toEqual(undefined);
// The run ends; the very next list stops naming it, with no status write
// and no crash-recovery pass in between.
runningTurnBySession.delete(busy.id);
const after = await manager.listSessions();
expect(after.find((session) => session.id === busy.id)?.runningTurnIds).toEqual(undefined);
});
});
describe('SessionManager Plan control boundaries', () => {
test('an exact approval retry completes Session side effects after a partial failure', async () => {
const store = new MemorySessionStore();
const runtimeKernel = new DelegatingRuntimeKernel();
const sessionId = 'session-1';
let receipt: object | undefined;
let committedApprovals = 0;
const planStore = {
readOperationReceipt: async () => receipt,
approveProposal: async () => {
if (!receipt) {
committedApprovals += 1;
receipt = { id: 'approve-operation', type: 'plan_approved' };
}
return {
event: receipt,
state: { ...emptyPlanSessionState(sessionId), storeVersion: 1 },
};
},
} as unknown as PlanStore;
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
planStore,
runtimeKernel,
});
await manager.createSession(makeInput({ collaborationMode: 'plan' }));
const input = {
sessionId,
proposalId: 'proposal-1',
expectedRevision: 1,
expectedStoreVersion: 1,
operationId: 'approve-operation',
};
store.failUpdateHeaderFor.add(sessionId);
await assert.rejects(manager.approvePlan(input), /Cannot update header/);
store.failUpdateHeaderFor.delete(sessionId);
runtimeKernel.activeRuns = true;
await manager.approvePlan(input);
expect(committedApprovals).toBe(1);
expect((await store.readHeader(sessionId)).collaborationMode).toBe('agent');
expect(runtimeKernel.disposed).toEqual([sessionId]);
});
test('does not commit a same-mode Plan revision before backend disposal succeeds', async () => {
const store = new MemorySessionStore();
const runtimeKernel = new DelegatingRuntimeKernel();
const sessionId = 'session-1';
let receipt: object | undefined;
let committedRevisions = 0;
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
runtimeKernel,
planStore: {
readOperationReceipt: async () => receipt,
requestRevision: async () => {
committedRevisions += 1;
receipt = { id: 'revision-operation', type: 'plan_revision_requested' };
return {
event: receipt,
state: { ...emptyPlanSessionState(sessionId), storeVersion: 1 },
};
},
} as unknown as PlanStore,
});
await manager.createSession(makeInput({ collaborationMode: 'plan' }));
runtimeKernel.failNextDispose = true;
await assert.rejects(
manager.requestPlanRevision(sessionId, 'proposal-1', 'revision-operation'),
/backend disposal failed/,
);
expect(committedRevisions).toBe(0);
await manager.requestPlanRevision(sessionId, 'proposal-1', 'revision-operation');
expect(committedRevisions).toBe(1);
expect(runtimeKernel.disposed).toEqual([sessionId]);
});
test('does not commit Plan cancellation before backend disposal succeeds', async () => {
const store = new MemorySessionStore();
const runtimeKernel = new DelegatingRuntimeKernel();
const sessionId = 'session-1';
const planState = {
...emptyPlanSessionState(sessionId),
storeVersion: 2,
executions: [
{
executionId: 'execution-1',
planId: 'plan-1',
proposalId: 'proposal-1',
sessionId,
status: 'interrupted' as const,
steps: [],
startedAt: 1,
updatedAt: 2,
interruptedAt: 2,
interruptionReason: 'Host restart',
},
],
};
let receipt: object | undefined;
let committedCancellations = 0;
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
runtimeKernel,
planStore: {
readState: async () => structuredClone(planState),
readOperationReceipt: async () => receipt,
cancelExecution: async () => {
committedCancellations += 1;
receipt = { id: 'cancel-operation', type: 'plan_execution_cancelled' };
return { event: receipt, state: { ...planState, storeVersion: 3 } };
},
} as unknown as PlanStore,
});
await manager.createSession(makeInput());
runtimeKernel.failNextDispose = true;
await assert.rejects(
manager.cancelPlanExecution(sessionId, 'execution-1', 'cancel-operation'),
/backend disposal failed/,
);
expect(committedCancellations).toBe(0);
await manager.cancelPlanExecution(sessionId, 'execution-1', 'cancel-operation');
expect(committedCancellations).toBe(1);
expect(runtimeKernel.disposed).toEqual([sessionId]);
});
test('classifies an ineligible execution cancellation as a domain conflict', async () => {
const store = new MemorySessionStore();
const sessionId = 'session-1';
const planState = {
...emptyPlanSessionState(sessionId),
storeVersion: 2,
activeExecutionId: 'execution-1',
executions: [
{
executionId: 'execution-1',
planId: 'plan-1',
proposalId: 'proposal-1',
sessionId,
status: 'active' as const,
steps: [],
startedAt: 1,
updatedAt: 1,
},
],
};
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
planStore: {
readState: async () => structuredClone(planState),
readOperationReceipt: async () => undefined,
} as unknown as PlanStore,
});
await manager.createSession(makeInput());
await assert.rejects(
manager.cancelPlanExecution(sessionId, 'execution-1', 'cancel-operation'),
(error: unknown) => {
assert.ok(error instanceof PlanConflictError);
assert.match(error.message, /interrupted/);
return true;
},
);
});
test('rejects Plan mode for a linked child Session', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
planStore: {
readState: async (sessionId: string) => emptyPlanSessionState(sessionId),
} as unknown as PlanStore,
});
const child = await manager.createSession(
makeInput({
subagentParent: {
kind: 'subagent',
parentSessionId: 'parent-session',
spawnedBy: {
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
toolCallId: 'tool-call',
},
lifecycle: 'foreground',
},
}),
);
await assert.rejects(
manager.transitionSessionConfiguration(child.id, {
expectedRevision: 1,
configuration: {
backend: child.backend,
llmConnectionSlug: child.llmConnectionSlug,
connectionLocked: true,
model: child.model,
thinkingLevel: child.thinkingLevel,
permissionMode: child.permissionMode,
collaborationMode: 'plan',
orchestrationMode: child.orchestrationMode ?? 'default',
},
}),
(error: unknown) => {
assert.ok(error instanceof SessionConfigurationTransitionError);
assert.equal(error.code, 'operation_unavailable');
return true;
},
);
assert.equal((await store.readHeader(child.id)).collaborationMode, 'agent');
});
});
describe('SessionManager child-session read model', () => {
test('lists typed child sessions without treating branches as children', async () => {
const store = new MemorySessionStore();
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(1),
});
const parent = await manager.createSession(makeInput({ name: 'Parent' }));
const child = await manager.createSession(
makeInput({
name: 'Child',
subagentParent: {
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
toolCallId: 'tool-call',
},
lifecycle: 'foreground',
},
}),
);
await manager.createSession(
makeInput({
name: 'Branch',
parentSessionId: parent.id,
branchOfTurnId: 'parent-turn',
}),
);
expect((await manager.listChildSessions(parent.id)).map((session) => session.id)).toEqual([
child.id,
]);
});
});
describe('SessionManager graph operator provisioning', () => {
test('provisions a graph operator before its active supervisor turn returns', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const parentGate = makeGate();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx, parentGate));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
subagentCatalog: {
list: async () => [],
resolve: async (id) => ({
id,
name: 'Fast graph reader',
description: 'Cheap graph scans',
profile: 'local_read',
connectionSlug: 'worker-connection',
model: 'worker-model',
thinkingLevel: 'low',
enabled: true,
}),
},
newId: nextId(),
now: nextNow(10),
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'supervisor-turn', text: 'schedule graph work' })
[Symbol.asyncIterator]();
await parentTurn.next();
const sourceRun = (await runStore.listSessionRuns(parent.id))[0];
if (!sourceRun) throw new Error('Supervisor Run was not recorded');
let provisionSettled = false;
const provision = manager
.provisionAgentGraphOperator({
graphId: 'graph-active-supervisor',
workId: `graph_work_${'8'.repeat(32)}`,
subagentId: 'fast-reader',
operatorId: `graph_operator_${'9'.repeat(32)}`,
source: {
sessionId: parent.id,
runId: sourceRun.runId,
turnId: sourceRun.turnId,
toolCallId: 'schedule-tool',
},
edges: [],
expectedScheduleRevision: 1,
})
.finally(() => {
provisionSettled = true;
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(provisionSettled).toBe(true);
const provisioned = await provision;
expect(provisioned.created).toBe(true);
expect(provisioned.header.name).toBe('Fast graph reader');
expect(provisioned.header.llmConnectionSlug).toBe('worker-connection');
expect(provisioned.header.model).toBe('worker-model');
expect(provisioned.header.thinkingLevel).toBe('low');
expect(provisioned.header.subagentRuntime?.presetId).toBe('fast-reader');
expect(provisioned.provision.agentId).toBe(LOCAL_READ_AGENT_ID);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('serializes graph operator provisioning with parent permission narrowing', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(20),
shellRuns: {
async terminateSession() {
return undefined;
},
async commitSessionClose() {},
rollbackSessionClose() {},
resumeSession() {},
} as never,
});
const parent = await manager.createSession(makeInput({ permissionMode: 'bypass' }));
await runStore.createRun(
makeRunHeader({
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
}),
);
const provisionStarted = makeGate();
const releaseProvision = makeGate();
store.nextGraphOperatorProvisionGate = {
started: provisionStarted,
release: releaseProvision,
};
const provision = manager.provisionAgentGraphOperator({
graphId: 'graph-config-fence',
workId: `graph_work_${'a'.repeat(32)}`,
agentId: LOCAL_READ_AGENT_ID,
operatorId: `graph_operator_${'b'.repeat(32)}`,
source: {
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
toolCallId: 'schedule-tool',
},
edges: [],
expectedScheduleRevision: 1,
});
await provisionStarted.promise;
let transitionSettled = false;
const transition = manager
.transitionSessionConfiguration(parent.id, {
expectedRevision: 1,
configuration: {
backend: parent.backend,
llmConnectionSlug: parent.llmConnectionSlug,
connectionLocked: true,
model: parent.model,
thinkingLevel: parent.thinkingLevel,
permissionMode: 'ask',
collaborationMode: parent.collaborationMode ?? 'agent',
orchestrationMode: parent.orchestrationMode ?? 'default',
},
})
.then(
(value) => ({ ok: true as const, value }),
(error: unknown) => ({ ok: false as const, error }),
)
.finally(() => {
transitionSettled = true;
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(transitionSettled).toBe(false);
releaseProvision.release();
const provisioned = await provision;
const transitionResult = await transition;
expect(transitionResult.ok).toBe(false);
if (transitionResult.ok) throw new Error('Configuration transition unexpectedly committed');
assert.ok(transitionResult.error instanceof SessionConfigurationTransitionError);
expect(transitionResult.error.code).toBe('operation_conflict');
expect((await store.readHeader(parent.id)).permissionMode).toBe('bypass');
expect(provisioned.header.permissionMode).toBe('bypass');
});
test('snapshots a catalog agent into a metadata-only child with reserved activation ids', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(30),
});
const parent = await manager.createSession(
makeInput({
cwd: '/tmp/graph-project',
llmConnectionSlug: 'graph-connection',
model: 'graph-model',
thinkingLevel: 'medium',
permissionMode: 'ask',
}),
);
await runStore.createRun(
makeRunHeader({
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
cwd: '/tmp/graph-project',
}),
);
const result = await manager.provisionAgentGraphOperator({
graphId: 'graph-1',
workId: `graph_work_${'1'.repeat(32)}`,
agentId: LOCAL_READ_AGENT_ID,
operatorId: `graph_operator_${'2'.repeat(32)}`,
source: {
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
toolCallId: 'schedule-tool',
},
edges: [
{
edgeId: `graph_edge_${'3'.repeat(32)}`,
fromOperatorId: 'writer',
toOperatorId: `graph_operator_${'2'.repeat(32)}`,
},
],
expectedScheduleRevision: 1,
});
expect(result.created).toBe(true);
expect(result.header.subagentParent?.graph).toEqual({
graphId: 'graph-1',
workId: `graph_work_${'1'.repeat(32)}`,
operatorId: `graph_operator_${'2'.repeat(32)}`,
});
expect(result.header.subagentRuntime?.agentId).toBe(LOCAL_READ_AGENT_ID);
expect(result.header.permissionMode).toBe('explore');
expect(result.provision.initialTurnId).toBe(result.header.subagentSpawn?.initialTurnId);
expect(result.provision.initialRunId).toBe(result.header.subagentSpawn?.initialRunId);
expect(await runStore.listSessionRuns(result.header.id)).toEqual([]);
});
test('reads a graph activation through the committed-result data plane', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(60),
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await runStore.createRun(
makeRunHeader({
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
}),
);
const provisioned = await manager.provisionAgentGraphOperator({
graphId: 'graph-result',
workId: `graph_work_${'6'.repeat(32)}`,
agentId: LOCAL_READ_AGENT_ID,
operatorId: `graph_operator_${'7'.repeat(32)}`,
source: {
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
toolCallId: 'schedule-tool',
},
edges: [],
expectedScheduleRevision: 1,
});
const run = makeRunHeader({
sessionId: provisioned.header.id,
runId: provisioned.provision.initialRunId,
turnId: provisioned.provision.initialTurnId,
status: 'completed',
createdAt: 70,
updatedAt: 80,
completedAt: 80,
});
const committedText = 'committed child answer '.repeat(200);
await seedRuntimeRun(runStore, run, [
runtimeEvent({
id: 'graph-child-answer',
sessionId: run.sessionId,
runId: run.runId,
turnId: run.turnId,
ts: 75,
role: 'model',
author: 'agent',
content: { kind: 'text', text: committedText },
}),
runtimeEvent({
id: 'graph-child-complete',
sessionId: run.sessionId,
runId: run.runId,
turnId: run.turnId,
ts: 80,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
]);
const output = await manager.readChildAgentOutput(parent.id, {
execution: {
kind: 'child_session',
sessionId: provisioned.header.id,
currentRunId: run.runId,
},
view: 'result',
maxBytes: 1024,
});
expect(output.events).toEqual([]);
expect(output.runtimeEvents).toEqual([]);
expect(output.artifacts).toEqual([]);
expect(output.result).toMatchObject({
schemaVersion: 1,
status: 'completed',
graph: {
graphId: 'graph-result',
workId: `graph_work_${'6'.repeat(32)}`,
operatorId: `graph_operator_${'7'.repeat(32)}`,
},
sourceRuntimeEventId: 'graph-child-answer',
terminalRuntimeEventId: 'graph-child-complete',
textTruncated: true,
});
expect(output.result?.text?.startsWith('committed child answer')).toBe(true);
expect((output.result?.text?.length ?? 0) < committedText.length).toBe(true);
expect(output.result?.resultRecordId).toMatch(/^graph_record_/);
expect(output.result?.terminalRecordId).toMatch(/^graph_record_/);
expect(output.result?.resultRecordId === output.result?.terminalRecordId).toBe(false);
expect(output.budget).toMatchObject({
view: 'result',
maxBytes: 1024,
});
expect(output.budget.projectedBytes <= output.budget.maxBytes).toBe(true);
});
test('keeps four large graph branches and a replacement off the supervisor data plane', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(90),
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await runStore.createRun(
makeRunHeader({
sessionId: parent.id,
runId: 'large-supervisor-run',
turnId: 'large-supervisor-turn',
}),
);
const rawPayloadBytes = 500 * 1024;
const oversizedPayload = 'x'.repeat(rawPayloadBytes);
const outputs = [];
for (let index = 0; index < 5; index += 1) {
const identity = String(index + 1).repeat(32);
const provisioned = await manager.provisionAgentGraphOperator({
graphId: 'graph-large-output',
workId: `graph_work_${identity}`,
agentId: LOCAL_READ_AGENT_ID,
operatorId: `graph_operator_${identity}`,
source: {
sessionId: parent.id,
runId: 'large-supervisor-run',
turnId: 'large-supervisor-turn',
toolCallId: `schedule-large-${index}`,
},
edges: [],
expectedScheduleRevision: index + 1,
});
const failed = index === 2;
const run = makeRunHeader({
sessionId: provisioned.header.id,
runId: provisioned.provision.initialRunId,
turnId: provisioned.provision.initialTurnId,
status: failed ? 'failed' : 'completed',
createdAt: 100 + index * 10,
updatedAt: 109 + index * 10,
completedAt: 109 + index * 10,
...(failed ? { failureClass: 'branch_failed' } : {}),
});
await seedRuntimeRun(runStore, run, [
runtimeEvent({
id: `large-tool-result-${index}`,
sessionId: run.sessionId,
runId: run.runId,
turnId: run.turnId,
ts: 105 + index * 10,
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: `large-tool-${index}`,
name: 'Read',
result: oversizedPayload,
isError: false,
},
}),
runtimeEvent({
id: `large-final-${index}`,
sessionId: run.sessionId,
runId: run.runId,
turnId: run.turnId,
ts: 108 + index * 10,
role: 'model',
author: 'agent',
content: {
kind: 'text',
text: failed
? 'Branch failed after collecting evidence.'
: index === 4
? 'Replacement branch completed with a verified answer.'
: `Branch ${index + 1} completed with a verified answer.`,
},
}),
runtimeEvent({
id: `large-terminal-${index}`,
sessionId: run.sessionId,
runId: run.runId,
turnId: run.turnId,
ts: 109 + index * 10,
role: 'system',
author: 'system',
status: failed ? 'failed' : 'completed',
actions: { endInvocation: true },
}),
]);
outputs.push(
await manager.readChildAgentOutput(parent.id, {
execution: {
kind: 'child_session',
sessionId: run.sessionId,
currentRunId: run.runId,
},
view: 'result',
maxBytes: 32 * 1024,
}),
);
}
const serializedOutputs = JSON.stringify(outputs);
expect(Buffer.byteLength(oversizedPayload, 'utf8') * outputs.length >= 2_500 * 1024).toBe(true);
expect(Buffer.byteLength(serializedOutputs, 'utf8') < 64 * 1024).toBe(true);
expect(serializedOutputs.includes(oversizedPayload.slice(0, 1024))).toBe(false);
expect(outputs.every((output) => output.runtimeEvents.length === 0)).toBe(true);
expect(outputs.every((output) => output.budget.projectedBytes <= 32 * 1024)).toBe(true);
expect(outputs[2]?.result?.status).toBe('failed');
expect(outputs[2]?.result?.failureClass).toBe('branch_failed');
expect(outputs[4]?.result?.text).toBe('Replacement branch completed with a verified answer.');
});
test('does not advertise implementation when the current project cannot use worktrees', async () => {
const store = new MemorySessionStore();
const checkedSources: unknown[] = [];
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
childTools: IMPLEMENTATION_AGENT_DEFINITION.tools.map(testTool),
worktreeChildExecutor: {
isAvailable: async (input) => {
checkedSources.push(input);
return false;
},
provision: async () => {
throw new Error('unavailable executor must not provision');
},
ensure: async () => {},
capturePatch: async () => new Uint8Array(),
recover: async () => {},
retire: async () => {},
},
newId: nextId(),
now: nextNow(40),
});
const parent = await manager.createSession(
makeInput({ cwd: '/tmp/plain-folder', projectId: 'plain-project' }),
);
const implementation = (await manager.listChildAgents(parent.id)).definitions.find(
(definition) => definition.id === IMPLEMENTATION_AGENT_ID,
);
expect(checkedSources).toEqual([
{ sourceCwd: '/tmp/plain-folder', sourceProjectId: 'plain-project' },
]);
expect(implementation?.availability).toEqual({
status: 'unavailable',
reason: 'workspace_isolation_unavailable',
workspace: AGENT_WORKSPACE_WORKTREE,
requiredRuntime: 'worktree_child_executor',
});
});
test('binds implementation operators to a durable project worktree', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const provisioned: unknown[] = [];
const binding = {
schemaVersion: 1 as const,
kind: 'git_worktree' as const,
leaseId: `subagent_worktree_${'a'.repeat(32)}`,
gitCommonDir: '/tmp/project/.git',
worktreePath: '/tmp/worktrees/implementation-a',
branch: `maka/subagent/${'a'.repeat(32)}`,
baseCommit: 'b'.repeat(40),
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
childTools: IMPLEMENTATION_AGENT_DEFINITION.tools.map(testTool),
worktreeChildExecutor: {
isAvailable: async () => true,
provision: async (input) => {
provisioned.push(input);
return {
...binding,
leaseId: input.leaseId,
worktreePath: `/tmp/worktrees/${input.leaseId}`,
branch: `maka/subagent/${input.leaseId.slice('subagent_worktree_'.length)}`,
};
},
ensure: async () => {},
capturePatch: async () => new Uint8Array(),
recover: async () => {},
retire: async () => {},
},
listArtifactsForTurn: async () => [],
publishChildWorkspacePatch: async () => {
throw new Error('Patch publication is not expected during provisioning');
},
assertChildWorkspaceQuiescent: async () => {},
newId: nextId(),
now: nextNow(40),
});
const parent = await manager.createSession(
makeInput({
cwd: '/tmp/project',
projectId: 'project-1',
permissionMode: 'ask',
}),
);
await runStore.createRun(
makeRunHeader({
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
cwd: '/tmp/project',
}),
);
const result = await manager.provisionAgentGraphOperator({
graphId: 'graph-worktree',
workId: `graph_work_${'4'.repeat(32)}`,
agentId: IMPLEMENTATION_AGENT_ID,
operatorId: `graph_operator_${'5'.repeat(32)}`,
source: {
sessionId: parent.id,
runId: 'supervisor-run',
turnId: 'supervisor-turn',
toolCallId: 'schedule-tool',
},
edges: [],
expectedScheduleRevision: 1,
});
expect(provisioned).toHaveLength(1);
expect(result.header.projectId).toBe('project-1');
expect(result.header.permissionMode).toBe('execute');
expect(
result.header.subagentRuntime
? 'permissionCeiling' in result.header.subagentRuntime
: undefined,
).toBe(false);
expect(result.header.cwd).toBe(result.header.subagentWorkspace?.worktreePath);
expect(result.header.subagentWorkspace?.kind).toBe('git_worktree');
expect(result.header.subagentWorkspace?.branch).toMatch(/^maka\/subagent\//);
expect(headerToSummary(result.header).subagentWorkspace).toEqual(
result.header.subagentWorkspace,
);
});
test('recovers only the latest unpublished implementation patch idempotently', async () => {
const store = new MemorySessionStore();
const runStore = new ReverseOrderedAgentRunStore();
const binding = {
schemaVersion: 1 as const,
kind: 'git_worktree' as const,
leaseId: `subagent_worktree_${'d'.repeat(32)}`,
gitCommonDir: '/tmp/project/.git',
worktreePath: '/tmp/worktrees/implementation-recovery',
branch: `maka/subagent/${'d'.repeat(32)}`,
baseCommit: 'e'.repeat(40),
};
const parent = await store.create(makeInput({ cwd: '/tmp/project' }));
const { header: child } = await store.createSubagent(
makeInput({
cwd: binding.worktreePath,
permissionMode: 'execute',
subagentParent: {
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
toolCallId: 'implementation-spawn',
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: IMPLEMENTATION_AGENT_DEFINITION.definitionVersion,
agentId: IMPLEMENTATION_AGENT_ID,
agentName: IMPLEMENTATION_AGENT_DEFINITION.name,
profile: 'implementation',
systemPrompt: IMPLEMENTATION_AGENT_DEFINITION.systemPrompt,
toolNames: [...IMPLEMENTATION_AGENT_DEFINITION.tools],
categoryPolicy: {},
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint: 'f'.repeat(64),
initialTurnId: 'child-turn',
initialRunId: 'child-run',
},
subagentWorkspace: binding,
}),
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: child.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
completedAt: 20,
updatedAt: 20,
cwd: binding.worktreePath,
permissionMode: 'execute',
agentId: IMPLEMENTATION_AGENT_ID,
agentName: IMPLEMENTATION_AGENT_DEFINITION.name,
}),
[
runtimeEvent({
id: 'child-complete',
sessionId: child.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 20,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const artifacts = new Map<string, ArtifactRecord[]>();
let captures = 0;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
worktreeChildExecutor: {
isAvailable: async () => true,
provision: async () => binding,
ensure: async () => {},
capturePatch: async () => {
captures += 1;
return new TextEncoder().encode('terminal patch');
},
recover: async () => {},
retire: async () => {},
},
listArtifactsForTurn: async (sessionId, turnId) =>
artifacts.get(`${sessionId}:${turnId}`) ?? [],
publishChildWorkspacePatch: async ({ sessionId, turnId, patch }) => {
const record: ArtifactRecord = {
id: 'recovered-writeback',
sessionId,
turnId,
createdAt: 30,
name: 'workspace.patch',
kind: 'diff',
relativePath: `${sessionId}/recovered-writeback-workspace.patch`,
sizeBytes: patch.byteLength,
mimeType: 'text/x-diff; charset=utf-8',
source: 'subagent_writeback',
status: 'live',
};
artifacts.set(`${sessionId}:${turnId}`, [record]);
return record;
},
assertChildWorkspaceQuiescent: async () => {},
newId: nextId(),
now: nextNow(40),
});
await manager.recoverChildWorkspacePatches([parent.id, child.id]);
await manager.recoverChildWorkspacePatches([child.id]);
expect(captures).toBe(1);
expect(artifacts.get(`${child.id}:child-turn`)?.[0]?.source).toBe('subagent_writeback');
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: child.id,
runId: 'newer-child-run',
turnId: 'newer-child-turn',
status: 'completed',
createdAt: 50,
completedAt: 60,
updatedAt: 60,
cwd: binding.worktreePath,
permissionMode: 'execute',
agentId: IMPLEMENTATION_AGENT_ID,
agentName: IMPLEMENTATION_AGENT_DEFINITION.name,
resumedFromRunId: 'child-run',
}),
[
runtimeEvent({
id: 'newer-child-complete',
sessionId: child.id,
runId: 'newer-child-run',
turnId: 'newer-child-turn',
ts: 60,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const oldArtifact = artifacts.get(`${child.id}:child-turn`)?.[0];
if (!oldArtifact) throw new Error('Recovered patch Artifact is missing');
artifacts.set(`${child.id}:newer-child-turn`, [
{
...oldArtifact,
id: 'newer-writeback',
turnId: 'newer-child-turn',
relativePath: `${child.id}/newer-writeback-workspace.patch`,
},
]);
artifacts.delete(`${child.id}:child-turn`);
await expectRejects(
manager.readChildAgentOutput(parent.id, {
execution: {
kind: 'child_session',
sessionId: child.id,
currentRunId: 'child-run',
},
}),
/cannot reconstruct the historical workspace patch/,
);
expect(captures).toBe(1);
});
});
describe('SessionManager claimed graph intent execution', () => {
test('fails closed without a trusted hosted graph execution capability', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendBuilds = 0;
backends.register('fake', (ctx) => {
backendBuilds += 1;
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: hostedRootAuthority(),
newId: nextId(),
now: nextNow(20),
});
const parent = await manager.createSession(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim({ targetSessionId: child.id }, 'must not start');
await expectRejects(
manager.runClaimedAgentGraphIntent(graphExecutionInput(claim, 'must not start')),
/requires its trusted graph execution capability/,
);
expect(await runStore.listSessionRuns(child.id)).toEqual([]);
expect(await store.readMessages(child.id)).toEqual([]);
expect(backendBuilds).toBe(0);
});
test('hosted execution reads the trusted claim and delegates the exact root descriptor', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const executions: Parameters<RuntimeHostedRootAuthority['executeRoot']>[0][] = [];
const authority = hostedRootAuthority();
authority.executeRoot = async (input) => {
executions.push(input);
for await (const event of input.start({
runId: input.runId,
userMessageId: input.userMessageId,
onRunStarted: () => input.onReady?.(),
})) {
input.onEvent?.(event);
}
};
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'hosted-graph-turn',
targetRunId: 'hosted-graph-run',
},
'canonical hosted graph prompt',
);
const prompt = 'canonical hosted graph prompt';
const trustedExecution = graphExecutionInput(claim, prompt);
let trustedReads = 0;
const trustedClaimStore = trustedExecution.claimStore;
const callerClaimStore: AgentGraphIntentClaimStore = {
async claimAgentGraphIntent() {
throw new Error('caller store must not be used');
},
async readAgentGraphIntentClaim() {
throw new Error('caller store must not be used');
},
async listAgentGraphIntentClaims() {
throw new Error('caller store must not be used');
},
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: authority,
hostedAgentGraphExecution: {
async readAgentGraphIntentClaim(graphId, intentId) {
trustedReads += 1;
return trustedClaimStore.readAgentGraphIntentClaim(graphId, intentId);
},
async readRootTurnAdmissionIdentity(sessionId, turnId) {
const admission = await runStore.readRootTurnAdmission(sessionId, turnId);
return admission
? { runId: admission.runId, userMessageId: admission.userMessageId }
: undefined;
},
},
newId: nextId(),
now: nextNow(30),
});
const result = await manager.runClaimedAgentGraphIntent({
...trustedExecution,
claimStore: callerClaimStore,
});
expect(result.status).toBe('completed');
expect(trustedReads).toBe(1);
expect(executions).toHaveLength(1);
expect(executions[0]).toMatchObject({
sessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
userMessageId: 'id-1',
execution: {
kind: 'claimed_agent_graph_intent',
claim,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
},
content: { text: prompt },
});
expect(
(await store.readMessages(child.id)).find(
(message) => message.type === 'user' && message.turnId === claim.targetTurnId,
),
).toMatchObject({ id: 'id-1', text: prompt });
});
test('hosted execution rejects prompt drift from the durable graph claim before admission', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendBuilds = 0;
backends.register('fake', (ctx) => {
backendBuilds += 1;
return new TestBackend(ctx);
});
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const proposedClaim = graphIntentClaim({
targetSessionId: child.id,
targetTurnId: 'prompt-bound-turn',
targetRunId: 'prompt-bound-run',
});
const intent = graphRunnableIntentForClaim(proposedClaim);
let persistedClaim: AgentGraphIntentClaim | undefined;
const durableClaims: AgentGraphIntentClaimStore = {
async claimAgentGraphIntent(request) {
if (persistedClaim) return { claim: persistedClaim, created: false };
persistedClaim = { ...request, claimedAt: 31 };
return { claim: persistedClaim, created: true };
},
async readAgentGraphIntentClaim(graphId, intentId) {
return persistedClaim?.graphId === graphId && persistedClaim.intentId === intentId
? persistedClaim
: undefined;
},
async listAgentGraphIntentClaims(graphId) {
return persistedClaim && (!graphId || persistedClaim.graphId === graphId)
? [persistedClaim]
: [];
},
};
const admitted = await claimAgentGraphRunnableIntent({
intent,
store: durableClaims,
newId: nextId(),
targetTurnId: proposedClaim.targetTurnId,
targetRunId: proposedClaim.targetRunId,
executionInput: { prompt: 'durably claimed prompt A' },
});
let hostedExecutions = 0;
const authority = hostedRootAuthority();
authority.executeRoot = async () => {
hostedExecutions += 1;
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: authority,
hostedAgentGraphExecution: hostedGraphExecutionCapability(durableClaims, runStore),
newId: nextId(),
now: nextNow(31),
});
await expectRejects(
manager.runClaimedAgentGraphIntent({
claimStore: durableClaims,
intent,
graphId: admitted.claim.graphId,
intentId: admitted.claim.intentId,
prompt: 'drifted prompt B',
}),
/does not match its durable claim/,
);
expect(hostedExecutions).toBe(0);
expect(backendBuilds).toBe(0);
expect(await runStore.listSessionRuns(child.id)).toEqual([]);
expect(await store.readMessages(child.id)).toEqual([]);
expect(
await runStore.readRootTurnAdmission(child.id, proposedClaim.targetTurnId),
).toBeUndefined();
});
test('hosted retry reuses an admitted user message identity before a Run exists', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'admitted-graph-turn',
targetRunId: 'admitted-graph-run',
},
'resume admitted graph root',
);
const descriptor: RootExecutionDescriptor = {
kind: 'claimed_agent_graph_intent',
claim,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
};
runStore.seedRootTurnAdmission(child.id, claim.targetTurnId, {
runId: claim.targetRunId,
userMessageId: 'durable-graph-user-message',
execution: descriptor,
});
let newIdCalls = 0;
let newIdCallsAtExecution = -1;
const authority = hostedRootAuthority();
authority.executeRoot = async (input) => {
newIdCallsAtExecution = newIdCalls;
expect(input.userMessageId).toBe('durable-graph-user-message');
for await (const event of input.start({
runId: input.runId,
userMessageId: input.userMessageId,
onRunStarted: () => input.onReady?.(),
})) {
input.onEvent?.(event);
}
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: authority,
hostedAgentGraphExecution: hostedGraphExecutionCapability(
graphExecutionInput(claim, '').claimStore,
runStore,
),
newId: () => `retry-id-${++newIdCalls}`,
now: nextNow(33),
});
const result = await manager.runClaimedAgentGraphIntent(
graphExecutionInput(claim, 'resume admitted graph root'),
);
expect(result.status).toBe('completed');
expect(newIdCallsAtExecution).toBe(0);
expect(
(await store.readMessages(child.id)).find(
(message) => message.type === 'user' && message.turnId === claim.targetTurnId,
),
).toMatchObject({
id: 'durable-graph-user-message',
text: 'resume admitted graph root',
});
});
test('hosted retry rejects an existing Run without its durable RootTurn admission', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendBuilds = 0;
backends.register('fake', (ctx) => {
backendBuilds += 1;
return new TestBackend(ctx);
});
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'orphaned-hosted-turn',
targetRunId: 'orphaned-hosted-run',
},
'must not be backfilled',
);
await runStore.createRun(
makeRunHeader({
sessionId: child.id,
runId: claim.targetRunId,
turnId: claim.targetTurnId,
status: 'completed',
completedAt: 34,
permissionMode: 'explore',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
}),
);
let hostedExecutions = 0;
const authority = hostedRootAuthority();
authority.executeRoot = async () => {
hostedExecutions += 1;
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: authority,
hostedAgentGraphExecution: hostedGraphExecutionCapability(
graphExecutionInput(claim, '').claimStore,
runStore,
),
newId: nextId(),
now: nextNow(34),
});
await expectRejects(
manager.runClaimedAgentGraphIntent(graphExecutionInput(claim, 'must not be backfilled')),
/missing its durable RootTurn admission/,
);
expect(hostedExecutions).toBe(0);
expect(backendBuilds).toBe(0);
expect(await store.readMessages(child.id)).toEqual([]);
expect((await runStore.readRun(child.id, claim.targetRunId)).status).toBe('completed');
});
test('hosted explicit abort stops only the exact claimed root identity', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendGate = makeGate();
const ready = makeGate();
backends.register('fake', (ctx) => new TestBackend(ctx, backendGate));
const stoppedRoots: RuntimeMessageRunIdentity[] = [];
let stoppedSessions = 0;
const authority = hostedRootAuthority();
authority.stopRoot = async (identity) => {
stoppedRoots.push(identity);
backendGate.release();
};
authority.stopSession = async () => {
stoppedSessions += 1;
};
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'aborted-hosted-turn',
targetRunId: 'aborted-hosted-run',
},
'abort this exact root',
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
messageAuthority: authority,
hostedAgentGraphExecution: hostedGraphExecutionCapability(
graphExecutionInput(claim, '').claimStore,
runStore,
),
newId: nextId(),
now: nextNow(35),
});
const abort = new AbortController();
const execution = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'abort this exact root'),
abortSignal: abort.signal,
onReady: () => ready.release(),
});
await ready.promise;
abort.abort();
await execution;
expect(stoppedRoots).toEqual([
{
sessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
},
]);
expect(stoppedSessions).toBe(0);
});
test('recovers a pending hosted graph admission without source lineage', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(38),
});
const parent = await store.create(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim({
targetSessionId: child.id,
targetTurnId: 'recovered-graph-turn',
targetRunId: 'recovered-graph-run',
});
await manager.closePendingHostedAdmission({
sessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
admittedAt: 37,
execution: {
kind: 'claimed_agent_graph_intent',
claim,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
},
});
const run = await runStore.readRun(child.id, claim.targetRunId);
expect(run).toMatchObject({
status: 'failed',
failureClass: 'app_restarted',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
});
expect(run.workspaceIdentity).toBe(undefined);
expect(run.resumedFromRunId).toBe(undefined);
expect(run.retriedFromRunId).toBe(undefined);
const terminalEvents = (await runStore.readRuntimeEvents(child.id, claim.targetRunId)).filter(
(event) => event.status === 'failed',
);
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.actions?.stateDelta).toMatchObject({
recovered: true,
recoveryReason: 'child_internal_admission_without_run',
executionKind: 'claimed_agent_graph_intent',
failureClass: 'app_restarted',
});
expect(terminalEvents[0]?.actions?.stateDelta?.sourceRunId).toBe(undefined);
});
test('runs the exact claimed session-inline activation and durably deduplicates retries', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendsBySession = new Map<string, TestBackend>();
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendsBySession.set(ctx.sessionId, backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(40),
});
const parent = await manager.createSession(makeInput({ name: 'Supervisor' }));
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'graph-turn',
targetRunId: 'graph-run',
},
'summarize the routed records',
);
const ready: unknown[] = [];
const result = await manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'summarize the routed records'),
onReady: (input) => {
ready.push(input);
},
});
expect(result).toMatchObject({
claimId: claim.claimId,
graphId: claim.graphId,
intentId: claim.intentId,
operatorId: claim.targetOperatorId,
childSessionId: child.id,
turnId: 'graph-turn',
runId: 'graph-run',
agentId: LOCAL_READ_AGENT_ID,
status: 'completed',
summary: 'ok',
});
expect(ready).toEqual([
{
claimId: claim.claimId,
graphId: claim.graphId,
intentId: claim.intentId,
operatorId: claim.targetOperatorId,
childSessionId: child.id,
turnId: 'graph-turn',
runId: 'graph-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
},
]);
const run = await runStore.readRun(child.id, 'graph-run');
expect(isSessionInlineRun(run)).toBe(true);
expect(run.parentRunId).toBe(undefined);
expect(run.turnId).toBe('graph-turn');
expect(
(await store.readMessages(child.id)).find(
(message) => message.type === 'user' && message.turnId === 'graph-turn',
),
).toMatchObject({ text: 'summarize the routed records' });
await store.archive(child.id);
const retry = await manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'summarize the routed records'),
});
expect(retry).toMatchObject({
claimId: result.claimId,
childSessionId: result.childSessionId,
turnId: result.turnId,
runId: result.runId,
status: 'completed',
summary: 'ok',
});
expect((await runStore.listSessionRuns(child.id)).length).toBe(1);
expect(backendsBySession.get(child.id)?.sendInputs.length).toBe(1);
await expectRejects(
manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'perform different work'),
}),
/does not match its durable claim/,
);
});
test('joins concurrent execution of one claim and rejects in-flight input drift', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const childGate = makeGate();
const started = makeGate();
let childBackend: TestBackend | undefined;
backends.register('fake', (ctx) => {
childBackend = new TestBackend(ctx, childGate);
return childBackend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(60),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim({ targetSessionId: child.id }, 'one activation');
const first = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'one activation'),
onReady: () => started.release(),
});
await started.promise;
const joined = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'one activation'),
});
await expectRejects(
manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'drifted activation'),
}),
/does not match its durable claim/,
);
childGate.release();
const [firstResult, joinedResult] = await Promise.all([first, joined]);
expect(joinedResult).toEqual(firstResult);
expect(childBackend?.sendInputs.length).toBe(1);
expect((await runStore.listSessionRuns(child.id)).length).toBe(1);
});
test('serializes different claims per child session without letting a queued abort stop active work', async () => {
const { store, runStore, manager, child, backend, activeGate, first, claims } =
await createQueuedGraphScenario();
const [firstClaim, secondClaim, thirdClaim] = claims;
const queuedAbort = new AbortController();
let secondReadyCount = 0;
const second = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(secondClaim, 'queued activation'),
abortSignal: queuedAbort.signal,
onReady: () => {
secondReadyCount += 1;
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(secondReadyCount).toBe(0);
expect(backend?.sendInputs.length).toBe(1);
queuedAbort.abort();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(backend?.stopCalls).toBe(0);
expect((await runStore.listSessionRuns(child.id)).length).toBe(1);
activeGate.release();
expect((await first).status).toBe('completed');
await expectRejects(second, /cancelled before runtime admission/);
expect(secondReadyCount).toBe(0);
expect(backend?.stopCalls).toBe(0);
expect(backend?.sendInputs.length).toBe(1);
const third = await manager.runClaimedAgentGraphIntent(
graphExecutionInput(thirdClaim, 'third activation'),
);
expect(third.status).toBe('completed');
expect(backend?.sendInputs.length).toBe(2);
expect((await runStore.listSessionRuns(child.id)).length).toBe(2);
});
test('evaluates execution admission only after a claimed child-session slot is available', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const firstGate = makeGate();
const firstReady = makeGate();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx, firstGate);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(75),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
const firstClaim = graphIntentClaim({ targetSessionId: child.id }, 'first activation');
const queuedClaim = graphIntentClaim(
{
claimId: `graph_claim_${'7'.repeat(32)}`,
intentId: `graph_intent_${'8'.repeat(32)}`,
targetSessionId: child.id,
targetTurnId: 'queued-turn',
targetRunId: 'queued-run',
},
'cancelled queued activation',
);
const first = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(firstClaim, 'first activation'),
onReady: () => firstReady.release(),
});
await firstReady.promise;
let admissionChecks = 0;
const queued = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(queuedClaim, 'cancelled queued activation'),
async admitExecution() {
admissionChecks += 1;
return 'cancelled';
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(admissionChecks).toBe(0);
firstGate.release();
expect((await first).status).toBe('completed');
await expectRejects(queued, /cancelled before runtime admission/);
expect(admissionChecks).toBe(1);
expect(backend?.sendInputs.length).toBe(1);
expect((await runStore.listSessionRuns(child.id)).length).toBe(1);
});
test('keeps a stop pending across graph admission with an idle cached backend', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const admissionStarted = makeGate();
const releaseAdmission = makeGate();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(77),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
await drain(
manager.sendMessage(child.id, {
turnId: 'completed-before-admission',
text: 'warm the cached operator backend',
}),
);
expect(backend?.sendInputs).toHaveLength(1);
const claim = graphIntentClaim(
{ targetSessionId: child.id },
'activation stopped during admission',
);
const execution = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'activation stopped during admission'),
async admitExecution() {
admissionStarted.release();
await releaseAdmission.promise;
return 'executing';
},
});
await admissionStarted.promise;
let stopSettled = false;
const stop = manager.stopSession(child.id, { source: 'graph_supervisor' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
expect(backend?.sendInputs).toHaveLength(1);
releaseAdmission.release();
await stop;
const result = await execution;
expect(result.status).toBe('cancelled');
expect(backend?.stopCalls).toBe(1);
expect(backend?.sendInputs).toHaveLength(1);
expect((await runStore.readRun(child.id, claim.targetRunId)).status).toBe('cancelled');
});
test('runtime stop settles queued graph claims without letting their slots pass the active claim', async () => {
const firstAbort = new AbortController();
const { store, runStore, manager, child, backend, stopStarted, first, claims } =
await createQueuedGraphScenario(firstAbort.signal);
const [firstClaim, secondClaim, thirdClaim] = claims;
let queuedReady = 0;
const second = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(secondClaim, 'queued activation'),
onReady: () => {
queuedReady += 1;
},
});
const third = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(thirdClaim, 'third activation'),
onReady: () => {
queuedReady += 1;
},
});
await new Promise<void>((resolve) => setImmediate(resolve));
firstAbort.abort();
await stopStarted.promise;
const bound = new AbortController();
let results: Awaited<ReturnType<typeof Promise.allSettled>>;
try {
results = await Promise.race([
Promise.allSettled([first, second, third]),
timerDelay(2_000, undefined, { signal: bound.signal }).then(() => {
throw new Error('graph stop composition did not settle within the bound');
}),
]);
} finally {
bound.abort();
}
expect(results[0]?.status).toBe('fulfilled');
expect(results[1]?.status).toBe('rejected');
expect(results[2]?.status).toBe('rejected');
expect(queuedReady).toBe(0);
expect(backend?.sendInputs.map((input) => input.turnId)).toEqual([firstClaim.targetTurnId]);
expect((await runStore.listSessionRuns(child.id)).map((run) => run.turnId)).toEqual([
firstClaim.targetTurnId,
]);
expect(
(await store.readMessages(child.id)).filter(
(message) =>
'turnId' in message &&
(message.turnId === secondClaim.targetTurnId ||
message.turnId === thirdClaim.targetTurnId),
),
).toEqual([]);
});
test('recovers an existing nonterminal claimed run without invoking the backend again', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendBuilds = 0;
backends.register('fake', (ctx) => {
backendBuilds += 1;
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(80),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim({ targetSessionId: child.id }, 'interrupted turn');
await seedRunningTurn(store, child.id, claim.targetTurnId);
await seedRun(
runStore,
makeRunHeader({
sessionId: child.id,
runId: claim.targetRunId,
turnId: claim.targetTurnId,
status: 'running',
permissionMode: 'explore',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
}),
[
makeRunEvent({
sessionId: child.id,
runId: claim.targetRunId,
turnId: claim.targetTurnId,
type: 'run_started',
ts: 81,
}),
],
);
const recovered = await manager.runClaimedAgentGraphIntent({
...graphExecutionInput(claim, 'interrupted turn'),
});
expect(recovered.status).toBe('failed');
expect(recovered.failureClass).toBe('app_restarted');
expect(backendBuilds).toBe(0);
expect((await runStore.listSessionRuns(child.id)).length).toBe(1);
});
test('target Session stop owns a claimed graph execution before its first runtime preflight', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(90),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim(
{
targetSessionId: child.id,
targetTurnId: 'stopped-graph-turn',
targetRunId: 'stopped-graph-run',
},
'must stop before provider dispatch',
);
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const executing = manager.runClaimedAgentGraphIntent(
graphExecutionInput(claim, 'must stop before provider dispatch'),
);
await readStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(child.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseRead.release();
const [result] = await Promise.all([executing, stopping]);
expect(result.status).toBe('cancelled');
expect(backend?.sendInputs).toEqual([]);
expect((await runStore.readRun(child.id, claim.targetRunId)).status).toBe('cancelled');
expect((await store.readHeader(child.id)).status === 'blocked').toBe(false);
});
test('fails closed before runtime when claim routing collides or targets a main session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendBuilds = 0;
backends.register('fake', (ctx) => {
backendBuilds += 1;
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(100),
});
const parent = await manager.createSession(makeInput());
const unclaimedIntent = graphRunnableIntentForClaim(
graphIntentClaim({
graphId: 'graph-unclaimed',
intentId: `graph_intent_${'f'.repeat(32)}`,
}),
);
await expectRejects(
manager.runClaimedAgentGraphIntent({
claimStore: {
async claimAgentGraphIntent() {
throw new Error('read-only');
},
async readAgentGraphIntentClaim() {
return undefined;
},
async listAgentGraphIntentClaims() {
return [];
},
},
intent: unclaimedIntent,
graphId: 'graph-unclaimed',
intentId: `graph_intent_${'f'.repeat(32)}`,
prompt: 'must not run',
}),
/has not been claimed/,
);
const mainSessionClaim = graphIntentClaim({ targetSessionId: parent.id }, 'must not run');
await expectRejects(
manager.runClaimedAgentGraphIntent(graphExecutionInput(mainSessionClaim, 'must not run')),
/target must be a linked child session/,
);
const child = await createGraphOperatorSession(store, parent.id);
const claim = graphIntentClaim({ targetSessionId: child.id }, 'must not run');
await runStore.createRun(
makeRunHeader({
sessionId: child.id,
runId: 'different-run',
turnId: claim.targetTurnId,
status: 'completed',
permissionMode: 'explore',
completedAt: 101,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
}),
);
await expectRejects(
manager.runClaimedAgentGraphIntent(graphExecutionInput(claim, 'must not run')),
/already owned by run different-run/,
);
const archivedChild = await createGraphOperatorSession(store, parent.id);
await store.archive(archivedChild.id);
const archivedClaim = graphIntentClaim(
{
claimId: `graph_claim_${'3'.repeat(32)}`,
intentId: `graph_intent_${'4'.repeat(32)}`,
targetSessionId: archivedChild.id,
targetTurnId: 'archived-turn',
targetRunId: 'archived-run',
},
'must not revive archived work',
);
await expectRejects(
manager.runClaimedAgentGraphIntent(
graphExecutionInput(archivedClaim, 'must not revive archived work'),
),
/target child session is terminated/,
);
const abortedChild = await createGraphOperatorSession(store, parent.id);
await store.updateHeader(abortedChild.id, { status: 'aborted' });
const abortedClaim = graphIntentClaim(
{
claimId: `graph_claim_${'5'.repeat(32)}`,
intentId: `graph_intent_${'6'.repeat(32)}`,
targetSessionId: abortedChild.id,
targetTurnId: 'aborted-turn',
targetRunId: 'aborted-run',
},
'must not revive aborted work',
);
await expectRejects(
manager.runClaimedAgentGraphIntent(
graphExecutionInput(abortedClaim, 'must not revive aborted work'),
),
/target child session is terminated/,
);
expect(backendBuilds).toBe(0);
});
});
describe('SessionManager child-session runtime primitive', () => {
test('creates a fresh read-only child with a session-inline first run and no parent history', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const contexts: BackendFactoryContext[] = [];
const backendActivationSessions: string[] = [];
const backendsBySession = new Map<string, TestBackend>();
backends.register('fake', (ctx) => {
contexts.push(ctx);
const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate);
backendsBySession.set(ctx.sessionId, backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(100),
runtimeSource: 'test',
runBackendActivation: async (operation) => {
const contextIndex = contexts.length;
const result = await operation();
const activatedContext = contexts[contextIndex];
if (!activatedContext) throw new Error('Backend activation did not build a backend');
backendActivationSessions.push(activatedContext.sessionId);
return result;
},
});
const parent = await manager.createSession(
makeInput({
cwd: '/tmp/project',
projectId: 'project-1',
llmConnectionSlug: 'connection-1',
model: 'model-1',
thinkingLevel: 'medium',
permissionMode: 'ask',
}),
);
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'private parent history' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const result = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-1',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect the storage boundary',
});
const childHeader = await store.readHeader(result.childSessionId);
expect(await store.readExecutionBoundary(result.childSessionId)).toEqual(
await store.readExecutionBoundary(parent.id),
);
expect(childHeader.cwd).toBe('/tmp/project');
expect(childHeader.projectId).toBe('project-1');
expect(childHeader.workspaceRoot).toBe((await store.readHeader(parent.id)).workspaceRoot);
expect(childHeader.backend).toBe('fake');
expect(childHeader.llmConnectionSlug).toBe('connection-1');
expect(childHeader.model).toBe('model-1');
expect(childHeader.thinkingLevel).toBe('medium');
expect(childHeader.permissionMode).toBe('explore');
expect(childHeader.connectionLocked).toBe(true);
expect(childHeader.subagentParent).toEqual({
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-1',
},
lifecycle: 'foreground',
});
expect(childHeader.subagentRuntime).toEqual({
schemaVersion: 1,
definitionVersion: 1,
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Local Read',
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: ['Read', 'Glob', 'Grep'],
categoryPolicy: {},
});
expect(childHeader.subagentSpawn?.schemaVersion).toBe(1);
expect(childHeader.subagentSpawn?.requestFingerprint).toMatch(/^[a-f0-9]{64}$/);
expect(childHeader.subagentSpawn?.initialTurnId).toBe(result.turnId);
expect(childHeader.subagentSpawn?.initialRunId).toBe(result.runId);
const [childRun] = await runStore.listSessionRuns(result.childSessionId);
if (!childRun) throw new Error('child run was not recorded');
expect(childRun.runId).toBe(result.runId);
expect(childRun.parentRunId).toBe(undefined);
expect(childRun.agentId).toBe(LOCAL_READ_AGENT_ID);
expect(isSessionInlineRun(childRun)).toBe(true);
expect(result.status).toBe('completed');
expect(backendActivationSessions).toEqual([parent.id, result.childSessionId]);
expect(
(await runStore.readRuntimeEvents(result.childSessionId, childRun.runId)).every(
(event) => event.sessionId === result.childSessionId,
),
).toBe(true);
const childContext = contexts.find((ctx) => ctx.sessionId === result.childSessionId);
expect(childContext?.systemPrompt).toBe(LOCAL_READ_AGENT_DEFINITION.systemPrompt);
expect(childContext?.tools?.map((tool) => tool.name)).toEqual(['Read', 'Glob', 'Grep']);
expect(backendsBySession.get(result.childSessionId)?.sendInputs[0]?.context).toEqual([]);
expect(
backendsBySession
.get(result.childSessionId)
?.sendInputs[0]?.runtimeContext?.some(
(event) =>
event.content?.kind === 'text' && event.content.text === 'private parent history',
) ?? false,
).toBe(false);
const parentMessages = await store.readMessages(parent.id);
const childMessages = await store.readMessages(result.childSessionId);
expect(
parentMessages.some(
(message) => message.type === 'user' && message.text === 'inspect the storage boundary',
),
).toBe(false);
expect(
childMessages.some(
(message) => message.type === 'user' && message.text === 'inspect the storage boundary',
),
).toBe(true);
await manager.setPermissionMode(result.childSessionId, 'execute');
expect((await store.readHeader(result.childSessionId)).permissionMode).toBe('execute');
const projection = await manager.listChildAgents(parent.id);
expect(projection.runs).toEqual([]);
expect(projection.executions).toHaveLength(1);
expect(projection.executions[0]?.execution).toEqual({
kind: 'child_session',
sessionId: result.childSessionId,
currentRunId: result.runId,
});
expect(projection.executions[0]?.status).toBe('completed');
const output = await manager.readChildAgentOutput(parent.id, {
execution: {
kind: 'child_session',
sessionId: result.childSessionId,
},
});
expect(output.execution).toEqual({
kind: 'child_session',
sessionId: result.childSessionId,
currentRunId: result.runId,
});
expect(output.header.sessionId).toBe(result.childSessionId);
expect(output.header.runId).toBe(result.runId);
const unrelatedParent = await manager.createSession(makeInput({ name: 'Unrelated parent' }));
await expectRejects(
manager.readChildAgentOutput(unrelatedParent.id, {
execution: {
kind: 'child_session',
sessionId: result.childSessionId,
},
}),
/could not find the requested child session/,
);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('freezes a configured subagent model target independently from the parent', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register(
'fake',
(ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
subagentCatalog: {
list: async () => [
{
id: 'fast-reader',
name: 'Fast reader',
description: 'Cheap scans',
profile: 'local_read',
connectionSlug: 'worker-connection',
model: 'worker-model',
thinkingLevel: 'low',
enabled: true,
availability: { status: 'available' },
},
],
resolve: async (id) => {
if (id !== 'fast-reader') throw new Error('unknown preset');
return {
id,
name: 'Fast reader',
description: 'Cheap scans',
profile: 'local_read',
connectionSlug: 'worker-connection',
model: 'worker-model',
thinkingLevel: 'low',
enabled: true,
};
},
},
newId: nextId(),
now: nextNow(150),
runtimeSource: 'test',
});
const parent = await manager.createSession(
makeInput({
llmConnectionSlug: 'parent-connection',
model: 'parent-model',
thinkingLevel: 'high',
}),
);
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn-preset', text: 'delegate' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const result = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-preset',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
subagentId: 'fast-reader',
prompt: 'inspect cheaply',
});
const child = await store.readHeader(result.childSessionId);
expect(child.llmConnectionSlug).toBe('worker-connection');
expect(child.model).toBe('worker-model');
expect(child.thinkingLevel).toBe('low');
expect(child.subagentRuntime?.presetId).toBe('fast-reader');
expect(child.subagentRuntime?.agentName).toBe('Fast reader');
expect(child.connectionLocked).toBe(true);
expect((await manager.listChildAgents(parent.id)).presets[0]?.id).toBe('fast-reader');
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('child sessions preserve an explicit no-project association', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register(
'fake',
(ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(200),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput({ projectId: null }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep the parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-no-project',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect without a project',
});
expect((await store.readHeader(child.childSessionId)).projectId).toBe(null);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('resumes a fresh child by returned runId inside the same linked Session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const backendsBySession = new Map<string, TestBackend>();
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate);
backendsBySession.set(ctx.sessionId, backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(130),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'fresh-swarm-item',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect the initial boundary',
});
const prepared = await manager.prepareChildAgentResume(parent.id, child.runId);
expect(prepared.execution).toEqual({
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: child.runId,
});
const resumed = await manager.resumeChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
prompt: 'continue from the returned swarm run id',
});
expect(resumed.childSessionId).toBe(child.childSessionId);
expect(resumed.resumedFromRunId).toBe(child.runId);
const resumedRun = await runStore.readRun(child.childSessionId, resumed.runId!);
expect(isSessionInlineRun(resumedRun)).toBe(true);
expect(resumedRun.parentRunId).toBe(undefined);
expect(resumedRun.resumedFromRunId).toBe(child.runId);
expect(
backendsBySession
.get(child.childSessionId)
?.sendInputs[1]?.runtimeContext?.some((event) => event.runId === child.runId),
).toBe(true);
expect((await manager.listChildAgents(parent.id)).executions[0]?.execution).toEqual({
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: resumed.runId,
});
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('does not resume a linked child after its parent boundary narrows', async () => {
const store = new AtomicBoundaryMemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register(
'fake',
(ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(140),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput({ permissionMode: 'bypass' }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'stale-boundary-child',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect before the boundary narrows',
});
store.forceBoundary(parent.id, {
...createGenesisExecutionBoundary('ask'),
revision: 1,
});
await expectRejects(
manager.prepareChildAgentResume(parent.id, child.runId),
/execution boundary no longer matches its parent/,
);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('retries a rate-limited fresh child inside the same linked Session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const childInputs: BackendSendInput[] = [];
let childAttempts = 0;
let failClaimOnce = true;
backends.register('fake', (ctx) => {
if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate);
return {
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
childAttempts += 1;
childInputs.push(input);
if (childAttempts <= 2) {
yield {
type: 'error',
id: `${input.turnId}-error`,
turnId: input.turnId,
ts: 1,
recoverable: true,
reason: 'RateLimit',
message: 'provider 429',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'error',
};
return;
}
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 3,
messageId: `${input.turnId}-message`,
text: 'recovered',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
},
async stop(): Promise<void> {},
async respondToSandboxBoundary(): Promise<void> {},
async dispose(): Promise<void> {},
};
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
inspectContinuationSafety: async () => ({
workspaceIdentity: '/tmp/cwd',
backgroundOperationsSettled: true,
availableToolNames: [],
}),
continuationFailpoint: async (point) => {
if (point === 'after_continuation_claim_committed' && failClaimOnce) {
failClaimOnce = false;
throw new Error('simulated linked child claim-only crash');
}
},
newId: nextId(),
now: nextNow(145),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'rate-limited-swarm-item',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect with a transient provider failure',
});
expect(child.status).toBe('failed');
expect(child.failureClass).toBe('RateLimit');
const resumed = await manager.resumeChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
prompt: 'retry with additional guidance',
});
expect(resumed.status).toBe('failed');
expect(resumed.failureClass).toBe('RateLimit');
await expectRejects(
manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
}),
/already has a successor/,
);
const resumedHeader = await runStore.readRun(child.childSessionId, resumed.runId!);
const resumedTerminal = (
await runStore.readImmutableRuntimeEvents(child.childSessionId, resumed.runId!)
).find(isTerminalRuntimeEvent);
if (!resumedTerminal) throw new Error('resumed child terminal fact was not recorded');
runStore.replaceRuntimeEvent(child.childSessionId, resumed.runId!, resumedTerminal.id, {
...resumedTerminal,
actions: {
...resumedTerminal.actions,
stateDelta: {
...resumedTerminal.actions?.stateDelta,
failureClass: 'continuation_abandoned_before_provider_dispatch',
},
},
});
await runStore.updateRun(child.childSessionId, resumed.runId!, {
failureClass: 'continuation_abandoned_before_provider_dispatch',
});
await expectRejects(
manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: resumed.runId!,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: resumed.runId!,
},
}),
/proven pre-provider continuation abandonment/,
);
runStore.replaceRuntimeEvent(
child.childSessionId,
resumed.runId!,
resumedTerminal.id,
resumedTerminal,
);
await runStore.updateRun(child.childSessionId, resumed.runId!, {
failureClass: resumedHeader.failureClass,
});
await expectRejects(
manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: resumed.runId!,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: resumed.runId!,
},
}),
/simulated linked child claim-only crash/,
);
expect(childInputs).toHaveLength(2);
const [claimState] = await runStore.listContinuationClaimsForRecovery(child.childSessionId);
if (!claimState) throw new Error('linked child continuation claim was not recorded');
const claimedHeader = claimState.claim.targetRunHeader;
if (!claimedHeader.agentId || !claimedHeader.agentName) {
throw new Error('linked child continuation claim lost its trusted agent identity');
}
await manager.closePendingHostedAdmission({
sessionId: child.childSessionId,
turnId: claimState.claim.target.turnId,
runId: claimState.claim.target.runId,
admittedAt: claimState.claim.claimedAt - 1,
execution: {
kind: 'linked_child_provider_retry',
agentId: claimedHeader.agentId,
agentName: claimedHeader.agentName,
sourceRunId: resumed.runId!,
},
});
await expectRejects(
runStore.readRun(child.childSessionId, claimState.claim.target.runId),
/unknown run/i,
);
await manager.recoverInterruptedSessions();
const repaired = (await runStore.listSessionRuns(child.childSessionId)).find(
(run) =>
run.retriedFromRunId === resumed.runId &&
run.failureClass === 'continuation_abandoned_before_provider_dispatch',
);
if (!repaired) throw new Error('linked child claim-only target was not repaired');
const retried = await manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: repaired.runId,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: repaired.runId,
},
});
expect(retried.status).toBe('completed');
expect(retried.childSessionId).toBe(child.childSessionId);
expect(retried.retriedFromRunId).toBe(repaired.runId);
expect(childInputs.map((input) => input.text)).toEqual([
'inspect with a transient provider failure',
'retry with additional guidance',
'',
]);
const retryRun = await runStore.readRun(child.childSessionId, retried.runId!);
expect(isSessionInlineRun(retryRun)).toBe(true);
expect(retryRun.parentRunId).toBe(undefined);
expect(retryRun.retriedFromRunId).toBe(repaired.runId);
expect(retryRun.continuationSource).toMatchObject({
protocol: 'continuation_source_v2',
sourceRunId: repaired.runId,
});
const retryStart = (
await runStore.readImmutableRuntimeEvents(child.childSessionId, retried.runId!)
)[0]?.actions?.continuationStart;
expect(retryStart?.protocol).toBe('continuation_start_v2');
expect(retryStart?.immediateSource.runId).toBe(repaired.runId);
await expectRejects(
manager.prepareChildAgentResume(parent.id, child.runId),
/already has a successor/,
);
expect((await manager.prepareChildAgentResume(parent.id, retried.runId!)).execution).toEqual({
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: retried.runId,
});
expect(
(await store.readMessages(child.childSessionId)).filter(
(message) => 'turnId' in message && message.turnId === retried.turnId,
),
).toEqual([]);
expect((await manager.listChildAgents(parent.id)).executions[0]?.execution).toEqual({
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: retried.runId,
});
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('linked retry exposes its hinted target Session before the first ledger read', async () => {
const ledgerReadStarted = makeGate();
const releaseLedgerRead = makeGate();
let gateLedgerRead = false;
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventRead: async () => {
if (!gateLedgerRead) return;
gateLedgerRead = false;
ledgerReadStarted.release();
await releaseLedgerRead.promise;
},
});
const backends = new BackendRegistry();
const parentGate = makeGate();
const childInputs: BackendSendInput[] = [];
backends.register('fake', (ctx) => {
if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate);
return {
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
childInputs.push(input);
yield {
type: 'error',
id: `${input.turnId}-error`,
turnId: input.turnId,
ts: 1,
recoverable: true,
reason: 'RateLimit',
message: 'provider 429',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'error',
};
},
async stop(): Promise<void> {},
async respondToSandboxBoundary(): Promise<void> {},
async dispose(): Promise<void> {},
};
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
inspectContinuationSafety: async () => ({
workspaceIdentity: '/tmp/cwd',
backgroundOperationsSettled: true,
availableToolNames: [],
}),
newId: nextId(),
now: nextNow(155),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-linked-retry', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'target-stop-retry',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'fail once before target stop',
});
expect(child.status).toBe('failed');
gateLedgerRead = true;
const retried = manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: child.runId,
},
});
await ledgerReadStarted.promise;
let stopSettled = false;
const stopping = manager
.stopSession(child.childSessionId, { source: 'stop_button' })
.finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseLedgerRead.release();
const [result] = await Promise.all([retried, stopping]);
expect(result.status).toBe('cancelled');
expect(result.retriedFromRunId).toBe(child.runId);
expect(childInputs).toHaveLength(1);
const retryRun = (await runStore.listSessionRuns(child.childSessionId)).find(
(run) => run.retriedFromRunId === child.runId,
);
expect(retryRun?.status).toBe('cancelled');
expect((await store.readHeader(child.childSessionId)).status === 'blocked').toBe(false);
await expectRejects(
manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: 'stale-hinted-run',
},
}),
/execution identity changed/,
);
await manager.stopSession(child.childSessionId, { source: 'stop_button' });
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('hosted child spawn joins the exact in-flight identity and rejects request drift', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const childGate = makeGate();
const backendsBySession = new Map<string, TestBackend>();
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? childGate : parentGate);
backendsBySession.set(ctx.sessionId, backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(150),
messageAuthority: hostedRootAuthority(),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const spawnInput = {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'same-tool-call',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'one durable task',
} as const;
const ready = makeGate();
const first = manager.spawnChildSession(parent.id, {
...spawnInput,
onReady: () => ready.release(),
});
await ready.promise;
const joined = manager.spawnChildSession(parent.id, spawnInput);
await expectRejects(
manager.spawnChildSession(parent.id, { ...spawnInput, prompt: 'different work' }),
/reused for different work/,
);
expect((await manager.listChildSessions(parent.id)).length).toBe(1);
childGate.release();
const [firstResult, joinedResult] = await Promise.all([first, joined]);
expect(joinedResult.childSessionId).toBe(firstResult.childSessionId);
expect(joinedResult.runId).toBe(firstResult.runId);
expect((await runStore.listSessionRuns(firstResult.childSessionId)).length).toBe(1);
const durableRetry = await manager.spawnChildSession(parent.id, spawnInput);
expect(durableRetry.childSessionId).toBe(firstResult.childSessionId);
expect(durableRetry.runId).toBe(firstResult.runId);
expect(durableRetry.summary).toBe('ok');
expect((await manager.listChildSessions(parent.id)).length).toBe(1);
expect(backendsBySession.get(firstResult.childSessionId)?.sendInputs.length).toBe(1);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('hosted linked-child gate precedes resume preparation and remains held through terminal', async () => {
const store = new MemorySessionStore();
const preparationEntered = makeGate();
const allowPreparation = makeGate();
let watchedRunId: string | undefined;
let preparationReads = 0;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventRead: async (_sessionId, runId) => {
if (runId !== watchedRunId) return;
preparationReads += 1;
preparationEntered.release();
await allowPreparation.promise;
},
});
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register(
'fake',
(ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(165),
messageAuthority: hostedRootAuthority(),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'hosted-gate-cut',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect the gate cut',
});
watchedRunId = child.runId;
const first = manager.resumeChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
prompt: 'first successor',
});
await preparationEntered.promise;
const second = manager.resumeChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: child.runId,
prompt: 'stale second successor',
});
const outcomes = Promise.allSettled([first, second]);
await new Promise((resolve) => setTimeout(resolve, 10));
const readsBeforeRelease = preparationReads;
allowPreparation.release();
const [firstOutcome, secondOutcome] = await outcomes;
expect(readsBeforeRelease).toBe(1);
expect(firstOutcome.status).toBe('fulfilled');
expect(secondOutcome.status).toBe('rejected');
expect(secondOutcome.status === 'rejected' ? String(secondOutcome.reason) : '').toContain(
'already has an active execution',
);
expect(
(await runStore.listSessionRuns(child.childSessionId)).filter(
(run) => run.resumedFromRunId === child.runId,
).length,
).toBe(1);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('starts a metadata-only retry once, notifies once, and rechecks cancellation', async () => {
const store = new MemorySessionStore();
const abortController = new AbortController();
const runStore = new MemoryAgentRunStore({
beforeRunRead: (sessionId, runId) => {
if (sessionId === 'session-3' && runId === 'cancelled-child-run') {
abortController.abort();
}
},
});
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register(
'fake',
(ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(170),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const seedMetadataOnlyChild = async (
toolCallId: string,
prompt: string,
initialTurnId: string,
initialRunId: string,
): Promise<SessionHeader> => {
const requestFingerprint = createHash('sha256')
.update(
JSON.stringify([
1,
parent.id,
parentRun.runId,
parentRun.turnId,
toolCallId,
LOCAL_READ_AGENT_PROFILE,
prompt,
null,
null,
]),
)
.digest('hex');
return (
await store.createSubagent(
makeInput({
permissionMode: 'explore',
collaborationMode: 'agent',
orchestrationMode: 'default',
subagentParent: {
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId,
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: LOCAL_READ_AGENT_DEFINITION.definitionVersion,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: [...LOCAL_READ_AGENT_DEFINITION.tools],
categoryPolicy: {},
permissionCeiling: 'ask',
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint,
initialTurnId,
initialRunId,
},
}),
)
).header;
};
const metadataOnly = await seedMetadataOnlyChild(
'metadata-only-tool',
'resume after metadata commit',
'metadata-only-turn',
'metadata-only-run',
);
let readyCalls = 0;
const resumed = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'metadata-only-tool',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'resume after metadata commit',
onReady: () => {
readyCalls += 1;
},
});
expect(resumed.childSessionId).toBe(metadataOnly.id);
expect(resumed.runId).toBe('metadata-only-run');
expect(readyCalls).toBe(1);
expect((await runStore.listSessionRuns(metadataOnly.id)).length).toBe(1);
const cancelled = await seedMetadataOnlyChild(
'cancelled-metadata-tool',
'must remain cancelled',
'cancelled-child-turn',
'cancelled-child-run',
);
let cancelledReadyCalls = 0;
await expectRejects(
manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'cancelled-metadata-tool',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'must remain cancelled',
abortSignal: abortController.signal,
onReady: () => {
cancelledReadyCalls += 1;
},
}),
/cancelled before its first run/,
);
expect(cancelledReadyCalls).toBe(0);
expect(await runStore.listSessionRuns(cancelled.id)).toEqual([]);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('reopens a child from its exact runtime snapshot after the builtin profile changes', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const contexts: BackendFactoryContext[] = [];
backends.register('fake', (ctx) => {
contexts.push(ctx);
return new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(175),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'snapshot-tool-call',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'first child turn',
});
const durablePrompt = (await store.readHeader(child.childSessionId)).subagentRuntime
?.systemPrompt;
if (!durablePrompt) throw new Error('child runtime snapshot was not persisted');
const originalPrompt = LOCAL_READ_AGENT_DEFINITION.systemPrompt;
let parentTurnDrained = false;
const drainParentTurn = async (): Promise<void> => {
parentGate.release();
while (!(await parentTurn.next()).done) {}
parentTurnDrained = true;
};
try {
LOCAL_READ_AGENT_DEFINITION.systemPrompt = 'Changed catalog prompt that must not leak.';
let refreshSettled = false;
const refresh = manager.refreshIdleBackends().finally(() => {
refreshSettled = true;
});
await Promise.resolve();
expect(refreshSettled).toBe(false);
expect(contexts.filter((ctx) => ctx.sessionId === parent.id).length).toBe(1);
await drainParentTurn();
await refresh;
await drain(
manager.sendMessage(child.childSessionId, {
turnId: 'child-follow-up',
text: 'use the durable profile',
}),
);
} finally {
if (!parentTurnDrained) await drainParentTurn();
LOCAL_READ_AGENT_DEFINITION.systemPrompt = originalPrompt;
}
const childContexts = contexts.filter((ctx) => ctx.sessionId === child.childSessionId);
expect(childContexts.length).toBe(2);
expect(childContexts[1]?.systemPrompt).toBe(durablePrompt);
expect(childContexts[1]?.tools?.map((tool) => tool.name)).toEqual(['Read', 'Glob', 'Grep']);
const missingToolManager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob')],
newId: nextId(),
now: nextNow(185),
});
await expectRejects(
drain(
missingToolManager.sendMessage(child.childSessionId, {
turnId: 'missing-tool-follow-up',
text: 'must fail closed',
}),
),
/runtime tool snapshot is unavailable/,
);
});
test('reopens after restart with isolated history, tool activity, usage, and compaction', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = [];
const childBackends: LifecycleChildBackend[] = [];
backends.register('fake', (ctx) => {
if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate);
const backend = new LifecycleChildBackend(ctx, compactCalls);
childBackends.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(186),
runtimeSource: 'test',
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'private parent context' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'restart-observation-tool',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect README before restart',
});
// A new manager represents a restarted Runtime Host. It must activate the
// child through the durable Session header and replay only child history.
const restarted = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(196),
runtimeSource: 'test',
});
await drain(
restarted.sendMessage(child.childSessionId, {
turnId: 'child-follow-up',
text: 'inspect it again after restart',
}),
);
const restartedBackend = childBackends.at(-1);
const followUpContext = restartedBackend?.sendInputs.at(-1)?.runtimeContext ?? [];
expect(followUpContext.some((event) => event.runId === child.runId)).toBe(true);
expect(
followUpContext.some(
(event) =>
event.content?.kind === 'text' && event.content.text.includes('private parent context'),
),
).toBe(false);
await drain(restarted.compactSession(child.childSessionId, { turnId: 'child-compact' }));
expect(compactCalls).toHaveLength(1);
expect(compactCalls[0]?.turnId).toBe('child-compact');
const childMessages = await restarted.getMessages(child.childSessionId);
expect(childMessages.some((message) => message.type === 'tool_call')).toBe(true);
expect(childMessages.some((message) => message.type === 'tool_result')).toBe(true);
expect(childMessages.some((message) => message.type === 'token_usage')).toBe(true);
expect(
childMessages.some(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'child-compact' &&
message.status === 'completed',
),
).toBe(true);
const parentMessages = await restarted.getMessages(parent.id);
expect(
parentMessages.some(
(message) =>
message.turnId === child.turnId ||
message.turnId === 'child-follow-up' ||
message.turnId === 'child-compact',
),
).toBe(false);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('refuses to activate a linked legacy child without a runtime snapshot', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(188),
});
const legacyChild = await manager.createSession(
makeInput({
subagentParent: {
kind: 'subagent',
parentSessionId: 'legacy-parent',
spawnedBy: {
parentRunId: 'legacy-parent-run',
parentTurnId: 'legacy-parent-turn',
toolCallId: 'legacy-tool-call',
},
lifecycle: 'foreground',
},
}),
);
await expectRejects(
drain(
manager.sendMessage(legacyChild.id, {
turnId: 'legacy-child-turn',
text: 'must not execute unrestricted',
}),
),
/missing its durable runtime snapshot/,
);
});
test('recovers an idempotent retry whose persisted initial run is no longer active', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
backends.register('fake', (ctx) => new TestBackend(ctx, parentGate));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(190),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'keep parent active' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const toolCallId = 'recovery-tool-call';
const prompt = 'recover this exact request';
const requestFingerprint = createHash('sha256')
.update(
JSON.stringify([
1,
parent.id,
parentRun.runId,
parentRun.turnId,
toolCallId,
LOCAL_READ_AGENT_PROFILE,
prompt,
null,
null,
]),
)
.digest('hex');
const { header: child } = await store.createSubagent(
makeInput({
name: 'Stale child',
permissionMode: 'explore',
collaborationMode: 'agent',
orchestrationMode: 'default',
subagentParent: {
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId,
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: LOCAL_READ_AGENT_DEFINITION.definitionVersion,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: [...LOCAL_READ_AGENT_DEFINITION.tools],
categoryPolicy: {},
permissionCeiling: 'ask',
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint,
initialTurnId: 'stale-child-turn',
initialRunId: 'stale-child-run',
},
}),
);
await seedRunningTurn(store, child.id, 'stale-child-turn');
await seedRun(
runStore,
makeRunHeader({
sessionId: child.id,
runId: 'stale-child-run',
turnId: 'stale-child-turn',
status: 'running',
permissionMode: 'explore',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
}),
[
makeRunEvent({
sessionId: child.id,
runId: 'stale-child-run',
turnId: 'stale-child-turn',
type: 'run_started',
ts: 191,
}),
],
);
const recovered = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId,
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt,
});
expect(recovered.childSessionId).toBe(child.id);
expect(recovered.runId).toBe('stale-child-run');
expect(recovered.status).toBe('failed');
expect(recovered.failureClass).toBe('app_restarted');
expect((await manager.listChildSessions(parent.id)).length).toBe(1);
parentGate.release();
while (!(await parentTurn.next()).done) {}
});
test('requires the exact parent run to remain active before admitting child work', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(200),
});
const parent = await manager.createSession(makeInput());
await drain(
manager.sendMessage(parent.id, { turnId: 'parent-turn', text: 'already complete' }),
);
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
await expectRejects(
manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-1',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'must not start',
}),
/parent run is not active/,
);
expect(await manager.listChildSessions(parent.id)).toEqual([]);
});
test('admits child work through an external parent-run authority', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
let externalParent:
| {
sessionId: string;
runId: string;
turnId: string;
}
| undefined;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
isParentRunActive: (sessionId, runId, turnId) =>
externalParent !== undefined &&
externalParent.sessionId === sessionId &&
externalParent.runId === runId &&
externalParent.turnId === turnId,
newId: nextId(),
now: nextNow(250),
});
const parent = await manager.createSession(makeInput());
await drain(manager.sendMessage(parent.id, { turnId: 'parent-turn', text: 'parent' }));
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
externalParent = {
sessionId: parent.id,
runId: parentRun.runId,
turnId: parentRun.turnId,
};
const child = await manager.spawnChildSession(parent.id, {
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-1',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'inspect',
});
expect(child.status).toBe('completed');
expect(child.childSessionId === parent.id).toBe(false);
});
test('child stop is isolated while parent stop reaches every foreground child session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const childGates = [makeGate(), makeGate()];
let childGateIndex = 0;
const backendsBySession = new Map<string, TestBackend>();
backends.register('fake', (ctx) => {
const gate = ctx.header.subagentRuntime ? childGates[childGateIndex++] : parentGate;
const backend = new TestBackend(ctx, gate);
backendsBySession.set(ctx.sessionId, backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(300),
});
const parent = await manager.createSession(makeInput());
const parentTurn = manager
.sendMessage(parent.id, { turnId: 'parent-turn', text: 'coordinate children' })
[Symbol.asyncIterator]();
await parentTurn.next();
const [parentRun] = await runStore.listSessionRuns(parent.id);
if (!parentRun) throw new Error('parent run was not recorded');
const childOneStarted = makeGate();
let childOneId = '';
const childOne = manager.spawnChildSession(parent.id, {
name: 'Child one',
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-1',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'first child',
onReady: ({ childSessionId }) => {
childOneId = childSessionId;
},
onEvent: (event) => {
if (event.type === 'text_delta') childOneStarted.release();
},
});
await childOneStarted.promise;
const childTwoStarted = makeGate();
let childTwoId = '';
const childTwo = manager.spawnChildSession(parent.id, {
name: 'Child two',
spawnedBy: {
parentRunId: parentRun.runId,
parentTurnId: parentRun.turnId,
toolCallId: 'tool-call-2',
},
agentProfile: LOCAL_READ_AGENT_PROFILE,
prompt: 'second child',
onReady: ({ childSessionId }) => {
childTwoId = childSessionId;
},
onEvent: (event) => {
if (event.type === 'text_delta') childTwoStarted.release();
},
});
await childTwoStarted.promise;
await manager.stopSession(childOneId, { source: 'stop_button' });
expect(backendsBySession.get(childOneId)?.stopCalls).toBe(1);
expect(backendsBySession.get(childTwoId)?.stopCalls).toBe(0);
expect(backendsBySession.get(parent.id)?.stopCalls).toBe(0);
expect((await runStore.readRun(parent.id, parentRun.runId)).status).toBe('running');
await manager.stopSession(parent.id, { source: 'stop_button' });
expect(backendsBySession.get(parent.id)?.stopCalls).toBe(1);
expect(backendsBySession.get(childOneId)?.stopCalls).toBe(1);
expect(backendsBySession.get(childTwoId)?.stopCalls).toBe(1);
parentGate.release();
for (const gate of childGates) gate.release();
while (!(await parentTurn.next()).done) {}
const [childOneResult, childTwoResult] = await Promise.all([childOne, childTwo]);
expect(childOneResult.status).toBe('cancelled');
expect(childTwoResult.status).toBe('cancelled');
});
test('startup recovery repairs an interrupted child inline run only in the child session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(400),
});
const parent = await manager.createSession(makeInput());
await store.appendMessage(parent.id, {
type: 'system_note',
id: 'parent-marker',
ts: 1,
kind: 'session_start',
data: { marker: 'parent stays untouched' },
});
const child = await manager.createSession(
makeInput({
name: 'Interrupted child',
status: 'running',
permissionMode: 'explore',
subagentParent: {
kind: 'subagent',
parentSessionId: parent.id,
spawnedBy: {
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
toolCallId: 'tool-call',
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: 1,
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Local Read',
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: ['Read', 'Glob', 'Grep'],
categoryPolicy: { read: 'allow' },
permissionCeiling: 'ask',
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint: 'a'.repeat(64),
initialTurnId: 'child-turn',
initialRunId: 'child-run',
},
}),
);
await seedRunningTurn(store, child.id, 'child-turn');
await seedRun(
runStore,
makeRunHeader({
sessionId: child.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'running',
permissionMode: 'explore',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Local Read',
}),
[
makeRunEvent({
sessionId: child.id,
runId: 'child-run',
turnId: 'child-turn',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: child.id,
runId: 'child-run',
turnId: 'child-turn',
type: 'model_stream_started',
ts: 12,
}),
],
);
const parentMessagesBefore = await store.readMessages(parent.id);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([child.id]);
const recoveredRun = await runStore.readRun(child.id, 'child-run');
expect(recoveredRun.parentRunId).toBe(undefined);
expect(isSessionInlineRun(recoveredRun)).toBe(true);
expect(recoveredRun.status).toBe('failed');
expect(recoveredRun.failureClass).toBe('app_restarted');
expect(
(await store.readMessages(child.id)).some(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'child-turn' &&
message.status === 'failed',
),
).toBe(true);
expect(await store.readMessages(parent.id)).toEqual(parentMessagesBefore);
});
});
describe('SessionManager automatic titles', () => {
test('starts after user persistence and does not block the turn', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const titleStarted = makeGate();
const titleRelease = makeGate();
const titleChanged = makeGate();
const callbackRuns: string[] = [];
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(500),
generateSessionTitle: async ({ sessionId, sourceText }) => {
expect(sourceText).toBe('visible prompt');
expect(
(await store.readMessages(sessionId)).some((message) => message.type === 'user'),
).toBe(true);
titleStarted.release();
await titleRelease.promise;
return 'Generated title';
},
onSessionTitleChanged: () => titleChanged.release(),
});
const session = await manager.createSession(makeInput({ name: 'New Chat' }));
await drain(
manager.sendMessage(
session.id,
{
turnId: 'turn-title',
text: '<system-reminder>hidden</system-reminder>model envelope',
displayText: 'visible prompt',
},
{
onRunStarted: (runId) => {
callbackRuns.push(runId);
},
},
),
);
await titleStarted.promise;
expect((await store.readHeader(session.id)).name).toBe('New Chat');
expect(callbackRuns).toHaveLength(1);
titleRelease.release();
await titleChanged.promise;
expect((await store.readHeader(session.id)).name).toBe('Generated title');
});
test('falls back once on generation failure', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const changed = makeGate();
let calls = 0;
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(600),
generateSessionTitle: async () => {
calls += 1;
throw new Error('offline');
},
onSessionTitleChanged: () => changed.release(),
});
const session = await manager.createSession(makeInput({ name: 'New Chat' }));
await drain(
manager.sendMessage(session.id, { turnId: 'first', text: '\nFallback title\nignored' }),
);
await changed.promise;
await drain(manager.sendMessage(session.id, { turnId: 'second', text: 'second prompt' }));
expect((await store.readHeader(session.id)).name).toBe('Fallback title');
expect(calls).toBe(1);
});
test('does not overwrite or notify after a racing manual rename', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const release = makeGate();
const attempted = makeGate();
store.generatedTitleAttempted = attempted;
let notifications = 0;
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(700),
generateSessionTitle: async () => {
await release.promise;
return 'Generated loses';
},
onSessionTitleChanged: () => {
notifications += 1;
},
});
const session = await manager.createSession(makeInput({ name: 'New Chat' }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-race', text: 'hello' }));
await manager.renameSession(session.id, 'Manual wins');
release.release();
await attempted.promise;
expect((await store.readHeader(session.id)).name).toBe('Manual wins');
expect(notifications).toBe(0);
});
});
describe('SessionManager manual compaction and quiescent session changes', () => {
test('runs backend history compaction as a runtime turn and persists diagnostics', async () => {
const store = new MemorySessionStore();
const runStore = new OrderingAgentRunStore();
const backends = new BackendRegistry();
const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = [];
backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(10_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
runStore.operations = [];
const events = await collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact' }),
);
expect(compactCalls).toEqual([{ turnId: 'turn-compact', runtimeContextCount: 3 }]);
expect(events.map((event) => event.type)).toEqual(['token_usage', 'complete']);
const usage = events[0];
if (usage?.type !== 'token_usage') throw new Error('expected token_usage');
expect(usage.contextBudget?.compactionDecisions?.[0]?.decision).toBe('replaced');
const messages = await store.readMessages(session.id);
expect(
messages.some((message) => message.type === 'user' && message.text.includes('compact')),
).toBe(false);
expect(
messages.some(
(message) => message.type === 'token_usage' && message.turnId === 'turn-compact',
),
).toBe(true);
expect(
messages.some(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-compact' &&
message.status === 'completed',
),
).toBe(true);
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact',
);
expect(compactRun?.status).toBe('completed');
expect(runStore.operations).toEqual(['terminalRuntimeEvent', 'completedRunHeader']);
expect(
(await runStore.readRuntimeEvents(session.id, compactRun!.runId)).some(
(event) => event.actions?.tokenUsage?.contextBudget,
),
).toBe(true);
await manager.stopSession(session.id, { source: 'stop_button' });
});
test('manual compaction settles one canonical record for the run the kernel opened', async () => {
// `sessions:compact` and CLI `/compact` both land here. The call has no
// send to inherit a run from, so the kernel states the run it opened and
// the record must carry it — otherwise a real, billed summarization is
// silently unmetered (#1679).
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const modelCalls: ModelCallAttempt[] = [];
const summarizerModel = new MockLanguageModelV4({
doGenerate: {
content: [{ type: 'text', text: 'MANUAL_COMPACT_SUMMARY' }],
finishReason: { unified: 'stop', raw: 'stop' },
usage: {
inputTokens: { total: 41, noCache: 41, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 9, text: 9, reasoning: 0 },
raw: { input_tokens: 41, output_tokens: 9 },
},
warnings: [],
},
});
backends.register('fake', (ctx) =>
createTestAiSdkBackend({
sessionId: ctx.sessionId,
header: ctx.header,
appendMessage: async () => {},
connection: {
slug: 'mock-main',
providerType: 'anthropic',
defaultModel: 'mock-model-id',
},
apiKey: 'sk-test',
modelId: 'mock-model-id',
modelFactory: () =>
new MockLanguageModelV4({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'stream-start', warnings: [] },
{ type: 'text-start', id: 'text-1' },
{ type: 'text-delta', id: 'text-1', delta: 'ok '.repeat(80) },
{ type: 'text-end', id: 'text-1' },
{
type: 'finish',
finishReason: { unified: 'stop', raw: 'stop' },
usage: {
inputTokens: { total: 100, noCache: 100, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 10, text: 10, reasoning: 0 },
},
},
] as LanguageModelV4StreamPart[],
initialDelayInMs: null,
chunkDelayInMs: null,
}),
}),
}),
tools: [],
newId: nextId(),
now: nextNow(1),
contextBudget: {
name: 'manual-compact-accounting',
maxHistoryEstimatedTokens: 10_000,
minRecentTurns: 1,
charsPerToken: 1,
},
summarizeHistoryCompact: buildLlmHistorySummarizer({
resolveModel: () => summarizerModel,
}),
recordHistoryCompactCheckpoint: () => {},
recordModelCallAttempt: (attempt) => {
modelCalls.push(attempt);
},
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(13_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first '.repeat(400) }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' }));
await drain(manager.compactSession(session.id, { turnId: 'turn-compact' }));
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact',
);
assert.ok(compactRun, 'the kernel opens a run for a manual compaction');
const compactions = modelCalls
.map((attempt) => decodeModelCallAttempt(attempt))
.filter((attempt) => attempt.callKind === 'history_compact');
assert.equal(compactions.length, 1, 'one manual compaction is one record');
assert.equal(
compactions[0]?.runId,
compactRun.runId,
'attributed to the run the kernel opened, not to whatever ran last',
);
assert.equal(compactions[0]?.turnId, 'turn-compact');
assert.equal(compactions[0]?.inputTokens, 41);
assert.equal(compactions[0]?.usageBasis, 'reported');
await manager.stopSession(session.id, { source: 'stop_button' });
});
test('persists one visible warning when manual compaction fails open', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FailOpenCompactingBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
await drain(manager.compactSession(session.id, { turnId: 'turn-compact' }));
const warnings = (await store.readMessages(session.id)).filter(
(message) =>
message.type === 'system_note' &&
message.turnId === 'turn-compact' &&
message.kind === 'context_compaction_failed_open',
);
expect(warnings).toHaveLength(1);
});
test('manual compaction stopped before backend start does not write compact artifacts', async () => {
const store = new MemorySessionStore();
const readGate = makeGate();
const readStarted = makeGate();
let blockPriorRead = false;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventRead: async () => {
if (!blockPriorRead) return;
readStarted.release();
await readGate.promise;
},
});
const backends = new BackendRegistry();
const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = [];
backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
blockPriorRead = true;
const compactPromise = collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact' }),
);
await readStarted.promise;
let stopSettled = false;
const stop = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
await Promise.resolve();
expect(stopSettled).toBe(false);
expect(compactCalls).toEqual([]);
readGate.release();
await stop;
const compactEvents = await compactPromise.catch(() => []);
expect(compactCalls).toEqual([]);
expect(compactEvents.some((event) => event.type === 'token_usage')).toBe(false);
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact',
);
expect(compactRun?.status).toBe('cancelled');
});
test('cold manual compaction normalizes only its execution cancellation reason', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const factoryStarts = new Map<string, ReturnType<typeof makeGate>>();
const factoryModes = new Map<string, 'execution_cancellation' | 'abort_error'>();
backends.register('fake', async (ctx) => {
const started = factoryStarts.get(ctx.sessionId);
const mode = factoryModes.get(ctx.sessionId);
if (!started || !mode) throw new Error('cold compact factory was not configured');
const signal = ctx.abortSignal;
if (!signal) throw new Error('cold compact factory did not receive an abort signal');
started.release();
if (!signal.aborted) {
await new Promise<void>((resolve) => {
signal.addEventListener('abort', () => resolve(), { once: true });
});
}
if (mode === 'abort_error') {
const error = new Error('cold compact factory timed out');
error.name = 'AbortError';
throw error;
}
throw signal.reason;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_500),
});
const cancelledSession = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const abortErrorSession = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const cancelledStart = makeGate();
const abortErrorStart = makeGate();
factoryStarts.set(cancelledSession.id, cancelledStart);
factoryStarts.set(abortErrorSession.id, abortErrorStart);
factoryModes.set(cancelledSession.id, 'execution_cancellation');
factoryModes.set(abortErrorSession.id, 'abort_error');
const cancelledCompact = collectSessionEvents(
manager.compactSession(cancelledSession.id, { turnId: 'compact-cancelled-factory' }),
);
await cancelledStart.promise;
const cancelledStop = manager.stopSession(cancelledSession.id, {
source: 'stop_button',
});
const [cancelledEvents] = await Promise.all([cancelledCompact, cancelledStop]);
expect(cancelledEvents).toEqual([]);
const abortErrorCompact = collectSessionEvents(
manager.compactSession(abortErrorSession.id, { turnId: 'compact-abort-error-factory' }),
);
await abortErrorStart.promise;
const abortErrorRejection = assert.rejects(abortErrorCompact, /cold compact factory timed out/);
const abortErrorStop = manager.stopSession(abortErrorSession.id, {
source: 'stop_button',
});
await Promise.all([abortErrorRejection, abortErrorStop]);
const [cancelledRun] = await runStore.listSessionRuns(cancelledSession.id);
const [abortErrorRun] = await runStore.listSessionRuns(abortErrorSession.id);
expect(cancelledRun?.status).toBe('cancelled');
expect(abortErrorRun?.status).toBe('cancelled');
});
test('stopSession waits for compaction blocked before Run reservation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = [];
backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(17_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const compact = collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact-pending' }),
);
await readStarted.promise;
let stopSettled = false;
const stop = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
await Promise.resolve();
expect(stopSettled).toBe(false);
expect(compactCalls).toEqual([]);
releaseRead.release();
await stop;
await compact.catch(() => []);
expect(compactCalls).toEqual([]);
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact-pending',
);
expect(compactRun?.status).toBe('cancelled');
});
test('manual compaction is stopped through the active runtime run lifecycle', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const compactGate = makeGate();
const compactStarted = makeGate();
let compactingBackend: BlockingCompactBackend | undefined;
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new BlockingCompactBackend(ctx, {
compactGate,
onCompactStart: (backend) => {
compactingBackend = backend;
compactStarted.release();
},
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(20_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const compactPromise = collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact' }),
);
await compactStarted.promise;
await manager.stopSession(session.id, { source: 'stop_button' });
compactGate.release();
const compactEvents = await compactPromise.catch(() => []);
expect(compactingBackend?.stopCalls).toBe(1);
expect(compactEvents.some((event) => event.type === 'token_usage')).toBe(false);
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact',
);
expect(compactRun?.status).toBe('cancelled');
});
test('compactSession rejects while a turn is running and writes no compact artifacts', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const sendGate = makeGate();
const turnStarted = makeGate();
const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new ActiveTurnBackend(ctx, { turnStarted, sendGate, compactCalls }),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(25_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const sendPromise = (async () => {
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'hi',
})) {
// turn held open at the send gate; drained after the compact assertion
}
})();
await turnStarted.promise;
const compactError = await collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact' }),
).catch((error: unknown) => error);
expect(compactError instanceof RuntimeContextCompactError).toBe(true);
expect((compactError as RuntimeContextCompactError).code).toBe('session_busy');
expect(compactCalls).toEqual([]);
const messages = await store.readMessages(session.id);
expect(messages.some((message) => message.turnId === 'turn-compact')).toBe(false);
const compactRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-compact',
);
expect(compactRun).toBeUndefined();
sendGate.release();
await sendPromise;
});
test('configuration transitions fence active runs and unavailable resource side effects', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const kernel = new DelegatingRuntimeKernel();
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(26_400),
runtimeKernel: kernel,
});
const session = await manager.createSession(
makeInput({ permissionMode: 'bypass', orchestrationMode: 'default' }),
);
const baseConfiguration = {
backend: session.backend,
llmConnectionSlug: session.llmConnectionSlug,
connectionLocked: true,
model: session.model,
thinkingLevel: session.thinkingLevel,
permissionMode: session.permissionMode,
collaborationMode: session.collaborationMode ?? 'agent',
orchestrationMode: 'graph' as const,
};
kernel.activeRuns = true;
await assert.rejects(
manager.transitionSessionConfiguration(session.id, {
expectedRevision: 1,
configuration: baseConfiguration,
}),
(error: unknown) => {
assert.ok(error instanceof SessionConfigurationTransitionError);
assert.equal(error.code, 'session_busy');
return true;
},
);
assert.deepEqual(kernel.disposed, []);
kernel.activeRuns = false;
const committed = await manager.transitionSessionConfiguration(session.id, {
expectedRevision: 1,
configuration: baseConfiguration,
});
assert.equal(committed.revision, 2);
assert.equal(committed.header.orchestrationMode, 'graph');
assert.deepEqual(kernel.disposed, [session.id]);
await assert.rejects(
manager.transitionSessionConfiguration(session.id, {
expectedRevision: 2,
configuration: {
...baseConfiguration,
permissionMode: 'explore',
},
}),
(error: unknown) => {
assert.ok(error instanceof SessionConfigurationTransitionError);
assert.equal(error.code, 'operation_unavailable');
return true;
},
);
assert.deepEqual(kernel.disposed, [session.id]);
});
test('workspace relocation uses the same quiescent revision fence as execution configuration', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const kernel = new DelegatingRuntimeKernel();
const resourceCalls: string[] = [];
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(26_425),
runtimeKernel: kernel,
shellRuns: {
async terminateSession(sessionId: string) {
resourceCalls.push(`terminate:${sessionId}`);
return { sessionId, token: Symbol('relocate') };
},
async commitSessionClose() {
resourceCalls.push('commit');
},
rollbackSessionClose() {
resourceCalls.push('rollback');
},
resumeSession(sessionId: string) {
resourceCalls.push(`resume:${sessionId}`);
},
} as never,
});
const session = await manager.createSession(makeInput({ cwd: '/workspace/old' }));
kernel.activeRuns = true;
await assert.rejects(
manager.relocateSessionWorkspace(session.id, {
expectedRevision: 1,
cwd: '/workspace/new',
}),
(error: unknown) => {
assert.ok(error instanceof SessionConfigurationTransitionError);
assert.equal(error.code, 'session_busy');
return true;
},
);
kernel.activeRuns = false;
const committed = await manager.relocateSessionWorkspace(session.id, {
expectedRevision: 1,
cwd: '/workspace/new',
projectId: 'project-2',
});
assert.equal(committed.revision, 2);
assert.equal(committed.header.cwd, '/workspace/new');
assert.equal(committed.header.projectId, 'project-2');
assert.deepEqual(kernel.disposed, [session.id]);
assert.deepEqual(resourceCalls, [`terminate:${session.id}`, 'commit', `resume:${session.id}`]);
await assert.rejects(
manager.relocateSessionWorkspace(session.id, {
expectedRevision: 1,
cwd: '/workspace/stale',
}),
(error: unknown) => {
assert.ok(error instanceof SessionConfigurationRevisionConflictError);
assert.equal(error.actualRevision, 2);
return true;
},
);
assert.equal((await store.readHeader(session.id)).cwd, '/workspace/new');
});
test('configuration transitions reject a claimed turn without waiting for it to settle', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const readStarted = makeGate();
const releaseRead = makeGate();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(26_450),
});
const session = await manager.createSession(makeInput({ orchestrationMode: 'default' }));
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const turn = drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'start' }));
await readStarted.promise;
const transitionResult = await manager
.transitionSessionConfiguration(session.id, {
expectedRevision: 1,
configuration: {
backend: session.backend,
llmConnectionSlug: session.llmConnectionSlug,
connectionLocked: true,
model: session.model,
thinkingLevel: session.thinkingLevel,
permissionMode: session.permissionMode,
collaborationMode: session.collaborationMode ?? 'agent',
orchestrationMode: 'graph',
},
})
.then(
(value) => ({ ok: true as const, value }),
(error: unknown) => ({ ok: false as const, error }),
);
assert.equal(transitionResult.ok, false);
if (transitionResult.ok) assert.fail('Configuration transition unexpectedly committed');
assert.ok(transitionResult.error instanceof SessionConfigurationTransitionError);
assert.equal(transitionResult.error.code, 'session_busy');
assert.equal((await store.readHeader(session.id)).orchestrationMode, 'default');
releaseRead.release();
await turn;
});
test('a claimed turn waits for an in-flight session mutation before reading its header', async () => {
const store = new VersionedConfigurationMemorySessionStore();
const updateStarted = makeGate();
const releaseUpdate = makeGate();
const activatedModels: string[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => {
activatedModels.push(ctx.header.model);
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(26_475),
});
const session = await manager.createSession(makeInput({ model: 'old-model' }));
store.nextConfigurationUpdateGate = {
started: updateStarted,
release: releaseUpdate,
};
const transition = manager.transitionSessionConfiguration(session.id, {
expectedRevision: 1,
configuration: {
backend: session.backend,
llmConnectionSlug: session.llmConnectionSlug,
connectionLocked: true,
model: 'new-model',
thinkingLevel: session.thinkingLevel,
permissionMode: session.permissionMode,
collaborationMode: session.collaborationMode ?? 'agent',
orchestrationMode: session.orchestrationMode ?? 'default',
},
});
await updateStarted.promise;
const turn = drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'start' }));
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(activatedModels, []);
releaseUpdate.release();
await transition;
await turn;
assert.deepEqual(activatedModels, ['new-model']);
});
test('backend refresh propagates delayed disposal failure after an active turn settles', async () => {
const store = new MemorySessionStore();
const sendGate = makeGate();
const turnStarted = makeGate();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new (class extends ActiveTurnBackend {
override async dispose(): Promise<void> {
throw new Error('injected delayed disposal failure');
}
})(ctx, { turnStarted, sendGate, compactCalls: [] }),
);
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(26_500),
});
const session = await manager.createSession(makeInput());
const firstTurn = drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hi' }));
await turnStarted.promise;
const refreshResult = manager.refreshIdleBackends().then(
() => undefined,
(error: unknown) => error,
);
sendGate.release();
await firstTurn;
const refreshError = await refreshResult;
expect(refreshError instanceof Error).toBe(true);
expect((refreshError as Error).message).toBe('injected delayed disposal failure');
});
test('backend refresh joins an in-flight best-effort disposal and observes its failure', async () => {
const store = new MemorySessionStore();
const disposeStarted = makeGate();
const releaseDispose = makeGate();
let disposeCalls = 0;
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new (class extends TestBackend {
override async dispose(): Promise<void> {
disposeCalls += 1;
disposeStarted.release();
await releaseDispose.promise;
throw new Error('injected concurrent disposal failure');
}
})(ctx),
);
const newId = nextId();
const now = nextNow(26_750);
const runtimeKernel = new RuntimeKernel({ store, backends, newId, now });
const manager = new SessionManager({
store,
backends,
runtimeKernel,
newId,
now,
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'cache me' }));
const bestEffort = runtimeKernel.invalidateBackend(session.id);
await disposeStarted.promise;
let strictSettled = false;
const strictResult = manager.refreshIdleBackends().then(
() => ({ ok: true as const }),
(error: unknown) => ({ ok: false as const, error }),
);
void strictResult.finally(() => {
strictSettled = true;
});
await Promise.resolve();
expect(strictSettled).toBe(false);
releaseDispose.release();
await bestEffort;
const strict = await strictResult;
expect(strict.ok).toBe(false);
if (strict.ok) throw new Error('Strict refresh unexpectedly succeeded');
expect(strict.error instanceof Error).toBe(true);
expect((strict.error as Error).message).toBe('injected concurrent disposal failure');
expect(disposeCalls).toBe(1);
});
test('backend refresh reports a retained child generation disposal failure', async () => {
const store = new MemorySessionStore();
let builds = 0;
let disposeCalls = 0;
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new (class extends TestBackend {
constructor() {
super(ctx);
builds += 1;
}
override async dispose(): Promise<void> {
disposeCalls += 1;
throw new Error('injected child generation disposal failure');
}
})(),
);
const newId = nextId();
const now = nextNow(26_800);
const childTools = [testTool('Read'), testTool('Glob'), testTool('Grep')];
const runtimeKernel = new RuntimeKernel({
store,
backends,
childTools,
newId,
now,
});
const manager = new SessionManager({
store,
backends,
runtimeKernel,
childTools,
newId,
now,
});
const session = await manager.createSession(makeInput());
await drain(
manager.startChildTurn(session.id, {
turnId: 'child-disposal-failure',
parentRunId: 'parent-run',
spec: {
id: LOCAL_READ_AGENT_ID,
name: 'Researcher',
systemPrompt: 'read only',
},
prompt: 'inspect',
}),
);
await expectRejects(
runtimeKernel.disposeBackend(session.id),
/injected child generation disposal failure/,
);
await expectRejects(
manager.refreshIdleBackends(),
/injected child generation disposal failure/,
);
expect(builds).toBe(1);
expect(disposeCalls).toBe(1);
});
test('backend activation waits for disposal and a later mutation invalidates its replacement', async () => {
const store = new MemorySessionStore();
const firstDisposeStarted = makeGate();
const releaseFirstDispose = makeGate();
const replacementPreBuildReached = makeGate();
const releaseReplacementPreBuild = makeGate();
let replacementCheckpointArmed = true;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventAppend: async (_sessionId, _runId, event) => {
if (!replacementCheckpointArmed || event.turnId !== 'turn-b') return;
replacementCheckpointArmed = false;
replacementPreBuildReached.release();
await releaseReplacementPreBuild.promise;
},
});
const { runBackendActivation, runMutation } = makeTestRuntimePolicyGate();
let builds = 0;
const disposed: number[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => {
const generation = ++builds;
return new (class extends TestBackend {
override async dispose(): Promise<void> {
disposed.push(generation);
if (generation === 1) {
firstDisposeStarted.release();
await releaseFirstDispose.promise;
}
}
})(ctx);
});
const newId = nextId();
const now = nextNow(26_875);
const runtimeKernel = new RuntimeKernel({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId,
now,
runBackendActivation,
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
runtimeKernel,
newId,
now,
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-a', text: 'build A' }));
const bestEffort = runtimeKernel.invalidateBackend(session.id);
await firstDisposeStarted.promise;
const replacementTurn = drain(
manager.sendMessage(session.id, { turnId: 'turn-b', text: 'build B' }),
);
await replacementPreBuildReached.promise;
releaseReplacementPreBuild.release();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(builds).toBe(1);
const mutation = runMutation(() => manager.refreshIdleBackends());
releaseFirstDispose.release();
await bestEffort;
await replacementTurn;
await mutation;
expect(builds).toBe(2);
expect(disposed).toEqual([1, 2]);
await drain(manager.sendMessage(session.id, { turnId: 'turn-c', text: 'build C' }));
expect(builds).toBe(3);
});
test('child backend activation waits for disposal and is covered by the later mutation', async () => {
const store = new MemorySessionStore();
const firstDisposeStarted = makeGate();
const releaseFirstDispose = makeGate();
const childPreBuildReached = makeGate();
const releaseChildPreBuild = makeGate();
let childCheckpointArmed = true;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventAppend: async (_sessionId, _runId, event) => {
if (!childCheckpointArmed || event.turnId !== 'child-turn') return;
childCheckpointArmed = false;
childPreBuildReached.release();
await releaseChildPreBuild.promise;
},
});
const { runBackendActivation, runMutation } = makeTestRuntimePolicyGate();
let builds = 0;
const disposed: number[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => {
const generation = ++builds;
return new (class extends TestBackend {
override async dispose(): Promise<void> {
disposed.push(generation);
if (generation === 1) {
firstDisposeStarted.release();
await releaseFirstDispose.promise;
}
}
})(ctx);
});
const newId = nextId();
const now = nextNow(26_900);
const runtimeKernel = new RuntimeKernel({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId,
now,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
runBackendActivation,
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
runtimeKernel,
newId,
now,
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-a', text: 'build A' }));
const bestEffort = runtimeKernel.invalidateBackend(session.id);
await firstDisposeStarted.promise;
const childTurn = drain(
manager.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: 'parent-run',
spec: {
id: LOCAL_READ_AGENT_ID,
name: 'Researcher',
systemPrompt: 'read only',
},
prompt: 'inspect',
}),
);
await childPreBuildReached.promise;
releaseChildPreBuild.release();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(builds).toBe(1);
const mutation = runMutation(() => manager.refreshIdleBackends());
releaseFirstDispose.release();
await bestEffort;
await childTurn;
await mutation;
expect(builds).toBe(2);
expect(disposed).toEqual([1, 2]);
});
test('backend refresh invalidates only cached sessions without listing persisted history', async () => {
const store = new MemorySessionStore();
const disposed: string[] = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new (class extends TestBackend {
override async dispose(): Promise<void> {
disposed.push(this.sessionId);
}
})(ctx),
);
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(27_000),
});
const cached = await manager.createSession(makeInput());
await manager.createSession(makeInput());
await drain(manager.sendMessage(cached.id, { turnId: 'turn-1', text: 'cache me' }));
store.list = async () => {
throw new Error('refresh must not list persisted sessions');
};
await manager.refreshIdleBackends();
expect(disposed).toEqual([cached.id]);
});
});
describe('SessionManager permission mode updates', () => {
test('revokes background shell authority before narrowing Auto to Explore', async () => {
const store = new AtomicBoundaryMemorySessionStore();
const calls: string[] = [];
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(985),
shellRuns: {
async terminateSession(sessionId: string) {
calls.push(`terminate:${sessionId}`);
return { sessionId, token: Symbol('test') };
},
async commitSessionClose() {
calls.push('commit');
},
rollbackSessionClose() {
calls.push('rollback');
},
resumeSession(sessionId: string) {
calls.push(`resume:${sessionId}`);
},
} as never,
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await manager.setPermissionMode(session.id, 'explore');
expect(calls).toEqual([`terminate:${session.id}`, 'commit', `resume:${session.id}`]);
const boundary = await store.readExecutionBoundary(session.id);
expect(boundary.kind).toBe('managed');
if (boundary.kind === 'managed') expect(boundary.profile.name).toBe('read-only');
});
test('revokes descendant background shell authority through the direct boundary API', async () => {
const store = new AtomicBoundaryMemorySessionStore();
const calls: string[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(990),
shellRuns: {
async terminateSession(sessionId: string) {
calls.push(`terminate:${sessionId}`);
return { sessionId, token: Symbol('test') };
},
async commitSessionClose() {
calls.push('commit');
},
rollbackSessionClose() {
calls.push('rollback');
},
resumeSession(sessionId: string) {
calls.push(`resume:${sessionId}`);
},
} as never,
});
const session = await manager.createSession(makeInput({ permissionMode: 'bypass' }));
const child = await manager.createSession(
makeInput({
permissionMode: 'bypass',
subagentParent: {
kind: 'subagent',
parentSessionId: session.id,
spawnedBy: {
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
toolCallId: 'child-tool',
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: LOCAL_READ_AGENT_DEFINITION.definitionVersion,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: ['Read', 'Glob', 'Grep'],
categoryPolicy: {},
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint: 'a'.repeat(64),
initialTurnId: 'child-turn',
initialRunId: 'child-run',
},
}),
);
const grandchild = await manager.createSession(
makeInput({
permissionMode: 'bypass',
subagentParent: {
kind: 'subagent',
parentSessionId: child.id,
spawnedBy: {
parentRunId: 'child-run',
parentTurnId: 'child-turn',
toolCallId: 'grandchild-tool',
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: LOCAL_READ_AGENT_DEFINITION.definitionVersion,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: ['Read', 'Glob', 'Grep'],
categoryPolicy: {},
},
subagentSpawn: {
schemaVersion: 1,
requestFingerprint: 'b'.repeat(64),
initialTurnId: 'grandchild-turn',
initialRunId: 'grandchild-run',
},
}),
);
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent' }));
await drain(manager.sendMessage(child.id, { turnId: 'child-turn', text: 'child' }));
await drain(
manager.sendMessage(grandchild.id, { turnId: 'grandchild-turn', text: 'grandchild' }),
);
store.disposeCount = 0;
await manager.setExecutionBoundaryKind(session.id, 'managed');
expect(calls).toEqual([
`terminate:${session.id}`,
`terminate:${child.id}`,
`terminate:${grandchild.id}`,
'commit',
'commit',
'commit',
`resume:${session.id}`,
]);
expect(store.disposeCount).toBe(3);
});
test('keeps mode changes blocked until all overlapping turns finish', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const firstGate = makeGate();
const secondGate = makeGate();
const gates = [firstGate, secondGate];
backends.register('fake', (ctx) => new TestBackend(ctx, gates.shift()));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(4_000),
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const first = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'first' })
[Symbol.asyncIterator]();
await first.next();
const second = manager
.sendMessage(session.id, { turnId: 'turn-2', text: 'second' })
[Symbol.asyncIterator]();
await second.next();
firstGate.release();
await first.next();
await first.next();
expect((await store.readHeader(session.id)).status).toBe('running');
const afterFirstRuns = await runStore.listSessionRuns(session.id);
expect(afterFirstRuns.find((run) => run.turnId === 'turn-1')?.status).toBe('completed');
expect(afterFirstRuns.find((run) => run.turnId === 'turn-2')?.status).toBe('running');
await expectRejects(manager.setPermissionMode(session.id, 'execute'), /当前对话正在运行/);
secondGate.release();
await second.next();
await second.next();
expect((await store.readHeader(session.id)).status).toBe('active');
const finalRuns = await runStore.listSessionRuns(session.id);
expect(finalRuns.map((run) => [run.turnId, run.status])).toEqual([
['turn-1', 'completed'],
['turn-2', 'completed'],
]);
const firstEvents = await runStore.readEvents(session.id, finalRuns[0]!.runId);
expect(firstEvents.map((event) => event.type)).toContain('run_created');
expect(firstEvents.map((event) => event.type)).toContain('run_started');
expect(firstEvents.map((event) => event.type)).toContain('run_completed');
const summary = await manager.setPermissionMode(session.id, 'execute');
expect(summary.permissionMode).toBe('execute');
});
test('persists orchestration mode changes and records a dimensioned audit note', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(4_500) });
const session = await manager.createSession(makeInput());
const summary = await manager.setOrchestrationMode(session.id, 'swarm');
expect(summary.orchestrationMode).toBe('swarm');
expect((await store.readHeader(session.id)).orchestrationMode).toBe('swarm');
const notes = (await store.readMessages(session.id)).filter(
(message): message is Extract<StoredMessage, { type: 'system_note' }> =>
message.type === 'system_note' && message.kind === 'mode_change',
);
expect(notes).toHaveLength(1);
expect(notes[0]?.data).toEqual({
dimension: 'orchestration',
from: 'default',
to: 'swarm',
});
await manager.setOrchestrationMode(session.id, 'swarm');
expect(
(await store.readMessages(session.id)).filter((message) => message.type === 'system_note'),
).toHaveLength(1);
});
test('snapshots persisted and one-turn orchestration into runs and backend sends', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(4_600),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ orchestrationMode: 'swarm' }));
await drain(manager.sendMessage(session.id, { turnId: 'persisted', text: 'first' }));
await drain(
manager.sendMessage(session.id, {
turnId: 'one-shot-default',
text: 'second',
turnOrchestration: { mode: 'default', source: 'host_api' },
}),
);
const runs = await runStore.listSessionRuns(session.id);
expect(runs.find((run) => run.turnId === 'persisted')).toMatchObject({
orchestrationMode: 'swarm',
orchestrationSource: 'session',
agentSwarmAuthorization: 'session_mode',
});
expect(runs.find((run) => run.turnId === 'one-shot-default')).toMatchObject({
orchestrationMode: 'default',
orchestrationSource: 'turn_override',
agentSwarmAuthorization: 'none',
});
expect(backend?.sendInputs.map((input) => input.orchestration)).toEqual([
{ mode: 'swarm', source: 'session', agentSwarmAuthorization: 'session_mode' },
{ mode: 'default', source: 'turn_override', agentSwarmAuthorization: 'none' },
]);
expect((await store.readHeader(session.id)).orchestrationMode).toBe('swarm');
});
test('snapshots the per-run tool mode into the run ledger and backend send', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(4_700),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(
manager.sendMessage(session.id, {
turnId: 'code-mode-turn',
text: 'inspect with code',
toolMode: 'code_mode',
}),
);
expect((await runStore.listSessionRuns(session.id))[0]).toMatchObject({
toolMode: 'code_mode',
});
expect(backend?.sendInputs[0]?.toolMode).toBe('code_mode');
});
test('leaving explore clears the deep research label so visible read-only copy stays truthful', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(6_000) });
const session = await manager.createSession(
makeInput({
permissionMode: 'explore',
labels: [DEEP_RESEARCH_SESSION_LABEL, 'kept'],
}),
);
const summary = await manager.setPermissionMode(session.id, 'ask');
expect(summary.permissionMode).toBe('ask');
expect(summary.labels).toEqual(['kept']);
expect((await store.readHeader(session.id)).labels).toEqual(['kept']);
const messages = await store.readMessages(session.id);
const modeNote = messages.find(
(message) => message.type === 'system_note' && message.kind === 'mode_change',
);
if (modeNote?.type !== 'system_note') throw new Error('mode_change note was not written');
expect(modeNote.data).toEqual({ from: 'explore', to: 'ask' });
});
test('backend configuration updates rebuild an already-active backend', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const built: string[] = [];
backends.register('fake', (ctx) => {
built.push(
`${ctx.header.backend}:${ctx.header.llmConnectionSlug}:${ctx.header.model}:${ctx.header.cwd}`,
);
return new TestBackend(ctx);
});
backends.register('ai-sdk', (ctx) => {
built.push(
`${ctx.header.backend}:${ctx.header.llmConnectionSlug}:${ctx.header.model}:${ctx.header.cwd}`,
);
return new TestBackend(ctx);
});
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(5_000) });
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect(built).toEqual(['fake:fake:fake-model:/tmp/cwd']);
const summary = await manager.updateSession(session.id, {
backend: 'ai-sdk',
llmConnectionSlug: 'zai-coding-plan',
model: 'glm-4.7',
cwd: '/tmp/worktree-cwd',
});
expect(summary.backend).toBe('ai-sdk');
expect(summary.llmConnectionSlug).toBe('zai-coding-plan');
expect(summary.cwd).toBe('/tmp/worktree-cwd');
expect(store.disposeCount).toBe(1);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'again' }));
expect(built).toEqual([
'fake:fake:fake-model:/tmp/cwd',
'ai-sdk:zai-coding-plan:glm-4.7:/tmp/worktree-cwd',
]);
});
test('records the authoritative workspace identity on a new AgentRun', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
safeBoundaryResumeEnabled: true,
inspectContinuationSafety: async () => ({
workspaceIdentity: 'workspace-authoritative',
backgroundOperationsSettled: true,
availableToolNames: [],
}),
newId: nextId(),
now: nextNow(6_525),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-workspace-identity',
text: 'record workspace identity',
}),
);
const [run] = await runStore.listSessionRuns(session.id);
expect((run as AgentRunHeader & { workspaceIdentity?: string }).workspaceIdentity).toBe(
'workspace-authoritative',
);
});
test('starts a new turn without workspace identity when safety inspection fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
safeBoundaryResumeEnabled: true,
inspectContinuationSafety: async () => {
throw new Error('workspace marker is unavailable');
},
newId: nextId(),
now: nextNow(6_526),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const events = await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-workspace-identity-unavailable',
text: 'continue without resumability',
}),
);
expect(events.map((event) => event.type)).toEqual(['text_complete', 'complete']);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.workspaceIdentity).toBeUndefined();
});
test('does not inspect continuation safety on normal turns while resume is disabled', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let inspectionCalls = 0;
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: async () => {
inspectionCalls += 1;
return {
workspaceIdentity: 'workspace-should-not-be-read',
backgroundOperationsSettled: true,
availableToolNames: [],
};
},
newId: nextId(),
now: nextNow(6_527),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-resume-disabled',
text: 'normal happy path',
}),
);
expect(inspectionCalls).toBe(0);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.workspaceIdentity).toBeUndefined();
});
test('does not declare the T1 protocol for a backend without the durable tool boundary', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new CountingFinalTextBackend(ctx, () => {}));
const runtimeEventStore = Object.assign(runStore, {
toolBoundaryProtocol: 't1_after_preflight_v1' as const,
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore,
toolBoundaryProtocol: 't1_after_preflight_v1',
backends,
newId: nextId(),
now: nextNow(6_050),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-no-tool-boundary',
text: 'hello',
}),
);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('expected run');
const events = await runStore.readRuntimeEvents(session.id, run.runId);
expect(events[0]?.actions?.runtimeProtocol).toBeUndefined();
});
test('declares the T1 protocol for an AiSdk run when the host wires the durable boundary', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => {}));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
toolBoundaryProtocol: 't1_after_preflight_v1',
backends,
newId: nextId(),
now: nextNow(6_060),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ backend: 'ai-sdk' }));
await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-with-tool-boundary',
text: 'hello',
}),
);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('expected run');
const events = await runStore.readRuntimeEvents(session.id, run.runId);
expect(events[0]?.actions?.runtimeProtocol).toEqual({
toolBoundary: 't1_after_preflight_v1',
});
});
test('plans continuation from authoritative host facts without caller-supplied safety claims', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
safeBoundaryResumeEnabled: true,
inspectContinuationSafety: async () => ({
workspaceIdentity: 'workspace-authoritative',
backgroundOperationsSettled: true,
availableToolNames: [],
}),
newId: nextId(),
now: nextNow(6_530),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-authoritative-plan';
const sourceTurnId = 'source-turn-authoritative-plan';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
workspaceIdentity: 'workspace-authoritative',
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-authoritative-plan',
invocationId: 'source-invocation-authoritative-plan',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue from host facts' },
}),
runtimeEvent({
id: 'source-terminal-authoritative-plan',
invocationId: 'source-invocation-authoritative-plan',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planAuthoritativeSafeBoundaryContinuation(session.id, {
sourceRunId,
});
expect(plan.disposition).toBe('continue');
expect(plan.continuation?.safetySnapshot).toEqual({
workspaceIdentity: 'workspace-authoritative',
backgroundOperationsSettled: true,
availableToolNames: [],
});
});
test('parks when authoritative continuation safety inspection fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
safeBoundaryResumeEnabled: true,
inspectContinuationSafety: async () => {
throw new Error('safety authority unavailable');
},
newId: nextId(),
now: nextNow(6_535),
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
await runStore.createRun(
makeRunHeader({
runId: 'source-run-safety-failure',
sessionId: session.id,
turnId: 'source-turn-safety-failure',
status: 'failed',
cwd: header.cwd,
workspaceIdentity: 'workspace-safety-failure',
completedAt: 2,
failureClass: 'app_restarted',
}),
);
const plan = await manager.planAuthoritativeSafeBoundaryContinuation(session.id, {
sourceRunId: 'source-run-safety-failure',
});
expect(plan.disposition).toBe('park');
expect(plan.rejectionReasons).toEqual(['safety_observation_unavailable']);
});
test('keeps the authoritative continuation entry disabled unless the host enables it', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const lifecycleEvents: Array<{ type: string; rejectionReasons?: readonly string[] }> = [];
const manager = new SessionManager({
store,
backends,
onContinuationLifecycleEvent: (event) => {
lifecycleEvents.push(event);
},
newId: nextId(),
now: nextNow(6_535),
});
const plan = await manager.planAuthoritativeSafeBoundaryContinuation('session-disabled', {
sourceRunId: 'source-run-disabled',
});
expect(plan.disposition).toBe('park');
expect(plan.rejectionReasons).toEqual(['resume_feature_disabled']);
expect(lifecycleEvents).toEqual([
{
type: 'plan_parked',
sessionId: 'session-disabled',
sourceRunId: 'source-run-disabled',
rejectionReasons: ['resume_feature_disabled'],
},
]);
});
test('discovers the newest resumable top-level source run from durable state', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
safeBoundaryResumeEnabled: true,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_540),
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
for (const [index, runId] of ['source-run-older', 'source-run-newer'].entries()) {
const turnId = `source-turn-${index}`;
const invocationId = `source-invocation-${index}`;
await seedRuntimeRun(
runStore,
makeRunHeader({
runId,
sessionId: session.id,
turnId,
status: 'failed',
cwd: header.cwd,
workspaceIdentity: 'workspace-1',
createdAt: index + 1,
updatedAt: index + 2,
completedAt: index + 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: `source-user-${index}`,
invocationId,
runId,
sessionId: session.id,
turnId,
ts: index + 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: `continue source ${index}` },
}),
runtimeEvent({
id: `source-terminal-${index}`,
invocationId,
runId,
sessionId: session.id,
turnId,
ts: index + 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
}
const plan = await manager.planLatestAuthoritativeSafeBoundaryContinuation(session.id);
expect(plan.disposition).toBe('continue');
expect(plan.continuation?.sourceRunId).toBe('source-run-newer');
});
test('RuntimeKernel drives RuntimeRunner while preserving the SessionEvent stream', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const runtimeEventStore = new MemoryRuntimeEventStore();
const backends = new BackendRegistry();
const observed: InvocationResult[] = [];
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore,
backends,
newId: nextId(),
now: nextNow(6_500),
runtimeSource: 'test',
runtimeInvocationObserver: (result) => {
observed.push(result);
},
});
const session = await manager.createSession(makeInput());
const sessionEvents = await collectSessionEvents(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }),
);
expect(sessionEvents.map((event) => event.type)).toEqual(['text_complete', 'complete']);
expect(sessionEvents.map((event) => event.id)).toEqual(['turn-1-final', 'turn-1-complete']);
expect(observed.length).toBe(1);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
const result = observed[0]!;
expect(result.runId).toBe(run.runId);
expect(result.sessionId).toBe(session.id);
expect(result.turnId).toBe('turn-1');
expect(result.status).toBe('completed');
expect(result.finalOutput).toBe('ok');
expect(result.events.map((event) => event.runId)).toEqual([run.runId, run.runId, run.runId]);
expect(result.events.map((event) => event.sessionId)).toEqual([
session.id,
session.id,
session.id,
]);
expect(result.events.map((event) => event.turnId)).toEqual(['turn-1', 'turn-1', 'turn-1']);
expect(result.events.map((event) => event.role)).toEqual(['user', 'model', 'system']);
expect(result.events[0]?.content).toEqual({ kind: 'text', text: 'hello' });
expect(result.events[1]?.content).toEqual({ kind: 'text', text: 'ok' });
expect(result.events[2]?.status).toBe('completed');
const runtimeEvents = await runtimeEventStore.readRuntimeEvents(session.id, run.runId);
expect(runtimeEvents.map((event) => event.id)).toEqual(result.events.map((event) => event.id));
expect(runtimeEvents.map((event) => event.runId)).toEqual([run.runId, run.runId, run.runId]);
expect(runtimeEvents.map((event) => event.sessionId)).toEqual([
session.id,
session.id,
session.id,
]);
expect(runtimeEvents.map((event) => event.turnId)).toEqual(['turn-1', 'turn-1', 'turn-1']);
expect(runtimeEvents.map((event) => event.role)).toEqual(['user', 'model', 'system']);
expect(runtimeEvents[0]?.content).toEqual({ kind: 'text', text: 'hello' });
expect(runtimeEvents[1]?.content).toEqual({ kind: 'text', text: 'ok' });
expect(runtimeEvents[2]?.status).toBe('completed');
});
test('executes an approved continuation after a path move without another user message', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const lifecycleEvents: Array<{ type: string }> = [];
let backend: FinalTextTestBackend | undefined;
backends.register('ai-sdk', (ctx) => {
backend = new FinalTextTestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
toolBoundaryProtocol: 't1_after_preflight_v1',
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
onContinuationLifecycleEvent: (event) => {
lifecycleEvents.push(event);
},
newId: nextId(),
now: nextNow(6_550),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ backend: 'ai-sdk' }));
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run';
const sourceTurnId = 'source-turn';
const sourceInvocationId = 'source-invocation';
await runStore.createRun({
runId: sourceRunId,
invocationId: sourceInvocationId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
failureClass: 'runtime_interrupted',
backendKind: header.backend,
llmConnectionSlug: header.llmConnectionSlug,
modelId: header.model,
cwd: header.cwd,
permissionMode: header.permissionMode,
orchestrationMode: 'swarm',
orchestrationSource: 'turn_override',
agentSwarmAuthorization: 'turn_override',
toolMode: 'code_mode',
createdAt: 1,
updatedAt: 2,
completedAt: 2,
});
const sourceEvents: RuntimeEvent[] = [
{
id: 'source-user',
sessionId: session.id,
invocationId: sourceInvocationId,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 1,
partial: false,
author: 'user',
role: 'user',
content: { kind: 'text', text: 'continue safely' },
},
{
id: 'source-terminal',
sessionId: session.id,
invocationId: sourceInvocationId,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 2,
partial: false,
author: 'system',
role: 'system',
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'runtime_interrupted' } },
},
];
for (const event of sourceEvents) {
await runStore.appendRuntimeEvent(session.id, sourceRunId, event);
}
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(plan.disposition).toBe('continue');
if (!plan.continuation) throw new Error('expected continuation');
const movedCwd = '/fresh-sandbox/repo';
await store.updateHeader(session.id, { cwd: movedCwd });
const sessionEvents = await collectSessionEvents(
manager.resumeSafeBoundaryContinuation(plan.continuation),
);
expect(sessionEvents.map((event) => event.type)).toEqual(['text_complete', 'complete']);
const continuationRun = await runStore.readRun(session.id, plan.continuation.runId);
expect(continuationRun.invocationId).toBe(plan.continuation.invocationId);
expect(continuationRun.turnId).toBe(plan.continuation.turnId);
expect(continuationRun.parentRunId).toBe(sourceRunId);
expect(continuationRun.parentTurnId).toBe(sourceTurnId);
expect(continuationRun.cwd).toBe(movedCwd);
expect(continuationRun.status).toBe('completed');
expect(continuationRun).toMatchObject({
orchestrationMode: 'swarm',
orchestrationSource: 'turn_override',
agentSwarmAuthorization: 'turn_override',
toolMode: 'code_mode',
});
expect(backend?.sendInputs[0]?.toolMode).toBe('code_mode');
const continuationEvents = await runStore.readRuntimeEvents(
session.id,
plan.continuation.runId,
);
expect(continuationEvents[0]?.actions?.continuationStart).toMatchObject({
protocol: 'continuation_start_v2',
claimId: plan.continuation.claimId,
boundaryDigest: plan.continuation.boundary?.manifestDigest,
immediateSource: {
sessionId: session.id,
invocationId: sourceInvocationId,
runId: sourceRunId,
turnId: sourceTurnId,
highWater: sourceEvents.length,
prefixDigest: plan.continuation.boundary?.segments.at(-1)?.prefixDigest,
},
replayManifestDigest: plan.continuation.boundary?.manifestDigest,
providerProjectionVersion: 1,
providerReplayDigest: plan.continuation.providerReplayDigest,
});
expect(continuationEvents[0]?.actions?.runtimeProtocol).toEqual({
toolBoundary: 't1_after_preflight_v1',
});
expect(continuationEvents[0]?.refs).toBe(undefined);
expect(continuationEvents.some((event) => event.role === 'user')).toBe(false);
expect((await store.readMessages(session.id)).some((message) => message.type === 'user')).toBe(
false,
);
expect(await runStore.readRuntimeEvents(session.id, sourceRunId)).toEqual(sourceEvents);
expect(lifecycleEvents.map((event) => event.type)).toEqual([
'plan_approved',
'execution_started',
'execution_completed',
]);
await collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-after-continuation',
text: 'what happened after recovery?',
}),
);
const followUpContext = backend?.sendInputs.at(-1)?.runtimeContext ?? [];
expect(followUpContext.some((event) => event.runId === plan.continuation?.runId)).toBe(true);
});
test('strict backend refresh settles after an active continuation finalizes', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const turnStarted = makeGate();
const sendGate = makeGate();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new ActiveTurnBackend(ctx, {
turnStarted,
sendGate,
compactCalls: [],
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_558),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'stopped-source-run';
const sourceTurnId = 'stopped-source-turn';
const sourceInvocationId = 'stopped-source-invocation';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
invocationId: sourceInvocationId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
failureClass: 'runtime_interrupted',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
}),
[
runtimeEvent({
id: 'stopped-source-user',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue until stopped' },
}),
runtimeEvent({
id: 'stopped-source-terminal',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'runtime_interrupted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
const execution = collectSessionEvents(
manager.resumeSafeBoundaryContinuation(plan.continuation),
);
await turnStarted.promise;
let refreshSettled = false;
const refresh = manager.refreshIdleBackends();
void refresh.finally(() => {
refreshSettled = true;
});
await Promise.resolve();
expect(refreshSettled).toBe(false);
await manager.stopSession(session.id, { source: 'stop_button' });
sendGate.release();
await execution;
await refresh;
expect(store.disposeCount).toBe(1);
const cachedMessages = await store.readMessages(session.id);
expect(
cachedMessages
.filter(
(message) =>
message.type === 'turn_state' && message.turnId === plan.continuation?.turnId,
)
.at(-1),
).toMatchObject({
type: 'turn_state',
status: 'aborted',
abortSource: 'renderer.stop_button',
parentTurnId: sourceTurnId,
});
});
test('parks repeated planning after the source run already produced a continuation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_565),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-idempotent';
const sourceTurnId = 'source-turn-idempotent';
const sourceInvocationId = 'source-invocation-idempotent';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-idempotent',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue once' },
}),
runtimeEvent({
id: 'source-terminal-idempotent',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const firstPlan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!firstPlan.continuation) throw new Error('expected first continuation');
await collectSessionEvents(manager.resumeSafeBoundaryContinuation(firstPlan.continuation));
const repeatedPlan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(repeatedPlan.disposition).toBe('park');
expect(repeatedPlan.rejectionReasons).toEqual(['continuation_already_exists']);
const targetRunId = firstPlan.continuation.runId;
const targetRun = await runStore.readRun(session.id, targetRunId);
await runStore.updateRun(session.id, targetRunId, {
modelId: 'tampered-model',
updatedAt: targetRun.updatedAt + 1,
});
const targetIdentityMismatch = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(targetIdentityMismatch.rejectionReasons).toEqual(['continuation_claim_repair_required']);
await runStore.updateRun(session.id, targetRunId, {
modelId: targetRun.modelId,
status: 'failed',
failureClass: 'tampered_terminal_state',
updatedAt: targetRun.updatedAt + 2,
});
const targetTerminalMismatch = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(targetTerminalMismatch.rejectionReasons).toEqual(['continuation_claim_repair_required']);
await runStore.updateRun(session.id, targetRunId, {
status: targetRun.status,
failureClass: targetRun.failureClass,
completedAt: targetRun.completedAt,
updatedAt: targetRun.updatedAt,
});
await runStore.appendRuntimeEvent(
session.id,
targetRunId,
runtimeEvent({
id: 'post-terminal-continuation-output',
invocationId: targetRun.invocationId ?? targetRun.runId,
runId: targetRun.runId,
sessionId: targetRun.sessionId,
turnId: targetRun.turnId,
ts: targetRun.updatedAt + 1,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'must not follow a terminal fact' },
}),
);
const postTerminalMismatch = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(postTerminalMismatch.rejectionReasons).toEqual(['continuation_claim_repair_required']);
});
test('rejects continuation while a normal turn is still registering', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FinalTextTestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_575),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: 'race-source-run',
invocationId: 'race-source-invocation',
sessionId: session.id,
turnId: 'race-source-turn',
status: 'failed',
failureClass: 'runtime_interrupted',
cwd: header.cwd,
workspaceIdentity: 'workspace-1',
createdAt: 1,
updatedAt: 2,
completedAt: 2,
}),
[
runtimeEvent({
id: 'race-source-user',
invocationId: 'race-source-invocation',
sessionId: session.id,
runId: 'race-source-run',
turnId: 'race-source-turn',
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue safely' },
}),
runtimeEvent({
id: 'race-source-terminal',
invocationId: 'race-source-invocation',
sessionId: session.id,
runId: 'race-source-run',
turnId: 'race-source-turn',
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'runtime_interrupted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId: 'race-source-run',
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const pendingTurn = manager
.sendMessage(session.id, {
turnId: 'turn-still-registering',
text: 'new work',
})
[Symbol.asyncIterator]();
const pendingFirst = pendingTurn.next();
await readStarted.promise;
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/another run is active/,
);
releaseRead.release();
await pendingFirst;
while (!(await pendingTurn.next()).done) {}
const continuationReadStarted = makeGate();
const releaseContinuationRead = makeGate();
store.nextReadHeaderGate = {
started: continuationReadStarted,
release: releaseContinuationRead,
};
const pendingContinuation = manager
.resumeSafeBoundaryContinuation(plan.continuation)
[Symbol.asyncIterator]();
const continuationFirst = pendingContinuation.next();
await continuationReadStarted.promise;
await expectRejects(
collectSessionEvents(
manager.sendMessage(session.id, {
turnId: 'turn-racing-continuation',
text: 'must not race',
}),
),
/runtime continuation is being claimed/,
);
releaseContinuationRead.release();
await continuationFirst;
while (!(await pendingContinuation.next()).done) {}
});
test('rejects a stale second plan after another continuation claims the same source', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_570),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-stale-plan';
const sourceTurnId = 'source-turn-stale-plan';
const sourceInvocationId = 'source-invocation-stale-plan';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-stale-plan',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue exactly once' },
}),
runtimeEvent({
id: 'source-terminal-stale-plan',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const planInput = {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [] as string[],
};
const firstPlan = await manager.planSafeBoundaryContinuation(session.id, planInput);
const stalePlan = await manager.planSafeBoundaryContinuation(session.id, planInput);
if (!firstPlan.continuation || !stalePlan.continuation) {
throw new Error('expected two pre-claim continuation plans');
}
await collectSessionEvents(manager.resumeSafeBoundaryContinuation(firstPlan.continuation));
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(stalePlan.continuation)),
/boundary is already claimed/i,
);
expect(backendCalls).toBe(1);
});
test('serializes concurrent continuation claims for the same source boundary', async () => {
const store = new MemorySessionStore();
const runStore = new ContinuationClaimBarrierRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_572),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-concurrent-claim';
const sourceTurnId = 'source-turn-concurrent-claim';
const sourceInvocationId = 'source-invocation-concurrent-claim';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-concurrent-claim',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue once under concurrency' },
}),
runtimeEvent({
id: 'source-terminal-concurrent-claim',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const planInput = {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [] as string[],
};
const firstPlan = await manager.planSafeBoundaryContinuation(session.id, planInput);
const secondPlan = await manager.planSafeBoundaryContinuation(session.id, planInput);
if (!firstPlan.continuation || !secondPlan.continuation) {
throw new Error('expected two pre-claim continuation plans');
}
runStore.armContinuationClaimBarrier();
const firstExecution = collectSessionEvents(
manager.resumeSafeBoundaryContinuation(firstPlan.continuation),
);
await runStore.waitForContinuationClaimRead();
const secondResult = await Promise.allSettled([
collectSessionEvents(manager.resumeSafeBoundaryContinuation(secondPlan.continuation)),
]);
runStore.releaseContinuationClaimRead();
const firstResult = await Promise.allSettled([firstExecution]);
const results = [...firstResult, ...secondResult];
expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1);
expect(results.filter((result) => result.status === 'rejected')).toHaveLength(1);
expect(backendCalls).toBe(1);
});
test('does not call the backend and claim recovery closes a continuation-start persistence failure', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 2 });
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_575),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-write-failure';
const sourceTurnId = 'source-turn-write-failure';
const sourceInvocationId = 'source-invocation-write-failure';
await runStore.createRun({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
backendKind: header.backend,
llmConnectionSlug: header.llmConnectionSlug,
modelId: header.model,
cwd: header.cwd,
permissionMode: header.permissionMode,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
});
const sourceEvents: RuntimeEvent[] = [
{
id: 'source-user-write-failure',
sessionId: session.id,
invocationId: sourceInvocationId,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 1,
partial: false,
author: 'user',
role: 'user',
content: { kind: 'text', text: 'continue safely' },
},
{
id: 'source-terminal-write-failure',
sessionId: session.id,
invocationId: sourceInvocationId,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 2,
partial: false,
author: 'system',
role: 'system',
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
},
];
for (const event of sourceEvents) {
await runStore.appendRuntimeEvent(session.id, sourceRunId, event);
}
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/runtime event append failed/,
);
expect(backendCalls).toBe(0);
const targetRun = await runStore.readRun(session.id, plan.continuation.runId);
expect(['created', 'running'].includes(targetRun.status)).toBe(true);
expect(targetRun.completedAt).toBeUndefined();
expect(await runStore.readRuntimeEvents(session.id, plan.continuation.runId)).toEqual([]);
await manager.recoverInterruptedSessions();
const recoveredRun = await runStore.readRun(session.id, plan.continuation.runId);
const recoveredEvents = await runStore.readRuntimeEvents(session.id, plan.continuation.runId);
expect(recoveredRun.status).toBe('failed');
expect(recoveredRun.failureClass).toBe('continuation_abandoned_before_provider_dispatch');
expect(recoveredEvents).toHaveLength(2);
expect(recoveredEvents[0]?.actions?.continuationStart?.claimId).toBe(plan.continuation.claimId);
expect(recoveredEvents.filter(isTerminalRuntimeEvent)).toHaveLength(1);
expect(recoveredEvents.at(-1)?.actions?.stateDelta).toMatchObject({
recovered: true,
recoveryReason: 'continuation_abandoned_before_provider_dispatch',
failureClass: 'continuation_abandoned_before_provider_dispatch',
});
});
test('stop after the durable continuation-start commit fences backend dispatch', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const continuationCommitted = makeGate();
const releaseContinuationCommit = makeGate();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
continuationFailpoint: async (point) => {
if (point !== 'after_continuation_start_committed') return;
continuationCommitted.release();
await releaseContinuationCommit.promise;
},
newId: nextId(),
now: nextNow(6_576),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-post-commit-stop';
const sourceTurnId = 'source-turn-post-commit-stop';
const sourceInvocationId = 'source-invocation-post-commit-stop';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-post-commit-stop',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue only if the Run still owns dispatch' },
}),
runtimeEvent({
id: 'source-terminal-post-commit-stop',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
const execution = collectSessionEvents(
manager.resumeSafeBoundaryContinuation(plan.continuation),
);
await continuationCommitted.promise;
const stop = manager.stopSession(session.id, { source: 'stop_button' });
releaseContinuationCommit.release();
await stop;
await execution.catch(() => []);
expect(backendCalls).toBe(0);
const targetRun = await runStore.readRun(session.id, plan.continuation.runId);
expect(targetRun.status).toBe('cancelled');
const targetEvents = await runStore.readRuntimeEvents(session.id, plan.continuation.runId);
expect(
targetEvents.filter((event) => event.actions?.continuationStart !== undefined),
).toHaveLength(1);
expect(targetEvents.filter(isTerminalRuntimeEvent)).toHaveLength(1);
});
test('does not call the backend when the durable continuation claim cannot be created', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failContinuationCreate: true });
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_577),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-claim-create-failure';
const sourceTurnId = 'source-turn-claim-create-failure';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-claim-create-failure',
invocationId: 'source-invocation-claim-create-failure',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue only with a durable claim' },
}),
runtimeEvent({
id: 'source-terminal-claim-create-failure',
invocationId: 'source-invocation-claim-create-failure',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/continuation claim create failed/,
);
expect(backendCalls).toBe(0);
await expectRejects(runStore.readRun(session.id, plan.continuation.runId), /unknown run/i);
});
test('revalidates terminal ledger consistency before executing a planned continuation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
newId: nextId(),
now: nextNow(6_590),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-race';
const sourceTurnId = 'source-turn-race';
const sourceInvocationId = 'source-invocation-race';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue safely' },
}),
runtimeEvent({
id: 'source-terminal-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
await runStore.updateRun(session.id, sourceRunId, {
status: 'completed',
updatedAt: 3,
completedAt: 3,
});
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/terminal/i,
);
expect(backendCalls).toBe(0);
});
test('startup recovery retries claim-only terminal projection without dispatching the provider', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'failed' });
const backends = new BackendRegistry();
let backendCalls = 0;
let failOnce = true;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
continuationFailpoint: async (point) => {
if (point === 'after_continuation_claim_committed' && failOnce) {
failOnce = false;
throw new Error('simulated claim-only crash');
}
},
newId: nextId(),
now: nextNow(6_591),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-claim-only-crash';
const sourceTurnId = 'source-turn-claim-only-crash';
const sourceInvocationId = 'source-invocation-claim-only-crash';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
invocationId: sourceInvocationId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
workspaceIdentity: 'workspace-1',
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-claim-only-crash',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue after a claim-only crash' },
}),
runtimeEvent({
id: 'source-terminal-claim-only-crash',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/simulated claim-only crash/,
);
await expectRejects(runStore.readRun(session.id, plan.continuation.runId), /unknown run/i);
expect(backendCalls).toBe(0);
expect(await manager.recoverInterruptedSessions()).not.toContain(session.id);
const durableRepairEvents = await runStore.readRuntimeEvents(
session.id,
plan.continuation.runId,
);
expect(durableRepairEvents).toHaveLength(2);
expect(durableRepairEvents.filter(isTerminalRuntimeEvent)).toHaveLength(1);
expect((await runStore.readRun(session.id, plan.continuation.runId)).status).toBe('created');
expect(await manager.recoverInterruptedSessions()).toContain(session.id);
const repairedRun = await runStore.readRun(session.id, plan.continuation.runId);
expect(repairedRun.status).toBe('failed');
expect(repairedRun.failureClass).toBe('continuation_abandoned_before_provider_dispatch');
expect(
repairedRun.continuationSource && 'protocol' in repairedRun.continuationSource
? repairedRun.continuationSource.protocol
: undefined,
).toBe('continuation_source_v2');
expect(
repairedRun.continuationSource && 'claimId' in repairedRun.continuationSource
? repairedRun.continuationSource.claimId
: undefined,
).toBe(plan.continuation.claimId);
const repairedEvents = await runStore.readRuntimeEvents(session.id, plan.continuation.runId);
expect(repairedEvents).toHaveLength(2);
expect(repairedEvents[0]?.actions?.continuationStart?.claimId).toBe(plan.continuation.claimId);
expect(repairedEvents[1]?.status).toBe('failed');
expect(backendCalls).toBe(0);
const snapshot = JSON.stringify({
run: repairedRun,
events: repairedEvents,
});
await manager.recoverInterruptedSessions();
expect(
JSON.stringify({
run: await runStore.readRun(session.id, plan.continuation.runId),
events: await runStore.readRuntimeEvents(session.id, plan.continuation.runId),
}),
).toBe(snapshot);
expect(backendCalls).toBe(0);
const nextPlan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId: plan.continuation.runId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
expect(nextPlan.disposition).toBe('continue');
});
test('canonical continuation authority read failure quarantines legacy run repair', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failContinuationClaimReads: true });
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(6_595),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const runId = 'continuation-authority-unreadable-run';
const turnId = 'continuation-authority-unreadable-turn';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId,
invocationId: 'continuation-authority-unreadable-invocation',
sessionId: session.id,
turnId,
status: 'running',
cwd: header.cwd,
createdAt: 1,
updatedAt: 1,
}),
[
runtimeEvent({
id: 'continuation-authority-unreadable-user',
invocationId: 'continuation-authority-unreadable-invocation',
runId,
sessionId: session.id,
turnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'must remain quarantined' },
}),
],
);
expect(await manager.recoverInterruptedSessions()).not.toContain(session.id);
expect((await runStore.readRun(session.id, runId)).status).toBe('running');
expect(await runStore.readRuntimeEvents(session.id, runId)).toHaveLength(1);
expect(await runStore.readEvents(session.id, runId)).toHaveLength(0);
});
test('rejects continuation when the authoritative workspace identity changes after planning', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
let workspaceIdentity = 'workspace-1';
const lifecycleEvents: Array<{ type: string; errorClass?: string }> = [];
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: async () => ({
workspaceIdentity,
backgroundOperationsSettled: true,
availableToolNames: [],
}),
newId: nextId(),
now: nextNow(6_595),
runtimeSource: 'test',
onContinuationLifecycleEvent: (event) => {
lifecycleEvents.push(event);
},
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-workspace-race';
const sourceTurnId = 'source-turn-workspace-race';
const sourceInvocationId = 'source-invocation-workspace-race';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-workspace-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue in the same workspace' },
}),
runtimeEvent({
id: 'source-terminal-workspace-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: workspaceIdentity,
currentWorkspaceIdentity: workspaceIdentity,
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
workspaceIdentity = 'workspace-2';
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/workspace identity changed/i,
);
expect(backendCalls).toBe(0);
expect(lifecycleEvents.at(-1)).toEqual({
type: 'execution_failed',
sessionId: session.id,
sourceRunId,
targetRunId: plan.continuation.runId,
errorClass: 'workspace_identity_changed',
});
});
test('requires exact tool-catalog equality between continuation planning and execution', async () => {
const scenarios = [
{ name: 'added', planned: ['Write'], current: ['Read', 'Write'] },
{ name: 'removed', planned: ['Read', 'Write'], current: ['Write'] },
{ name: 'replaced', planned: ['Write'], current: ['Edit'] },
] as const;
for (const scenario of scenarios) {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
let availableToolNames: readonly string[] = scenario.planned;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: async () => ({
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames,
}),
newId: nextId(),
now: nextNow(6_597),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = `source-run-tool-catalog-${scenario.name}`;
const sourceTurnId = `source-turn-tool-catalog-${scenario.name}`;
const sourceInvocationId = `source-invocation-tool-catalog-${scenario.name}`;
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: `source-user-tool-catalog-${scenario.name}`,
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue with the same tools' },
}),
runtimeEvent({
id: `source-terminal-tool-catalog-${scenario.name}`,
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: scenario.planned,
});
if (!plan.continuation) throw new Error(`expected ${scenario.name} continuation`);
availableToolNames = scenario.current;
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/tool catalog changed/i,
);
expect(backendCalls).toBe(0);
}
});
test('revalidates continuation safety inside the backend activation barrier', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
let availableToolNames: readonly string[] = ['Write'];
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const newId = nextId();
const now = nextNow(6_599);
const runtimeKernel = new RuntimeKernel({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: async () => ({
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames,
}),
runBackendActivation: async (operation) => {
availableToolNames = ['Read', 'Write'];
return operation();
},
newId,
now,
runtimeSource: 'test',
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
runtimeKernel,
inspectContinuationSafety: async () => ({
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames,
}),
newId,
now,
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-activation-safety-race';
const sourceTurnId = 'source-turn-activation-safety-race';
const sourceInvocationId = 'source-invocation-activation-safety-race';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-activation-safety-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue under one activation snapshot' },
}),
runtimeEvent({
id: 'source-terminal-activation-safety-race',
invocationId: sourceInvocationId,
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames,
});
if (!plan.continuation) throw new Error('expected continuation');
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/tool catalog changed/i,
);
expect(backendCalls).toBe(0);
await expectRejects(runStore.readRun(session.id, plan.continuation.runId), /Unknown run/);
});
test('fails closed when continuation execution has no authoritative safety inspector', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backendCalls = 0;
backends.register(
'fake',
(ctx) =>
new CountingFinalTextBackend(ctx, () => {
backendCalls += 1;
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_600),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = await store.readHeader(session.id);
const sourceRunId = 'source-run-no-safety-inspector';
const sourceTurnId = 'source-turn-no-safety-inspector';
await seedRuntimeRun(
runStore,
makeRunHeader({
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
status: 'failed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user-no-safety-inspector',
invocationId: 'source-invocation-no-safety-inspector',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'continue only with authoritative facts' },
}),
runtimeEvent({
id: 'source-terminal-no-safety-inspector',
invocationId: 'source-invocation-no-safety-inspector',
runId: sourceRunId,
sessionId: session.id,
turnId: sourceTurnId,
ts: 2,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
const plan = await manager.planSafeBoundaryContinuation(session.id, {
sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: 'workspace-1',
currentWorkspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [],
});
if (!plan.continuation) throw new Error('expected continuation');
await expectRejects(
collectSessionEvents(manager.resumeSafeBoundaryContinuation(plan.continuation)),
/safety inspector/i,
);
expect(backendCalls).toBe(0);
});
test('completed turns are readable when the complete event reaches the renderer', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const runtimeEventStore = new MemoryRuntimeEventStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TextCompleteBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore,
backends,
newId: nextId(),
now: nextNow(6_625),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
expect((await iterator.next()).value?.type).toBe('text_delta');
const textComplete = (await iterator.next()).value;
expect(textComplete?.type).toBe('text_complete');
expect((await iterator.next()).value?.type).toBe('complete');
const messages = await manager.getMessages(session.id);
expect(messages.map((message) => message.type)).toEqual(['user', 'assistant', 'turn_state']);
expect(messages[1]?.id).toBe(
textComplete?.type === 'text_complete' ? textComplete.messageId : undefined,
);
await iterator.next();
});
test('terminal RuntimeEvent is recorded when terminal session projection fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_626),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
expect((await iterator.next()).value?.type).toBe('text_delta');
store.failUpdateHeaderFor.add(session.id);
expect((await iterator.next()).value?.type).toBe('complete');
store.failUpdateHeaderFor.delete(session.id);
await iterator.next();
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
expect(run.status).toBe('completed');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run.runId);
expect(runtimeEvents.filter((event) => event.status === 'completed')).toHaveLength(1);
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(view.terminalFacts.map((fact) => fact.runStatus)).toEqual(['completed']);
});
test('terminal header is never persisted without a terminal RuntimeEvent', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 2 });
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_750),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await expectRejects(
collectSessionEvents(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/runtime event append failed/,
);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run.runId);
const terminalEvents = runtimeEvents.filter(isTerminalRuntimeEvent);
expect(['created', 'running', 'waiting_for_user'].includes(run.status)).toBe(true);
expect(run.completedAt).toBeUndefined();
expect(runtimeEvents.map((event) => event.role)).toEqual(['user', 'model']);
expect(runtimeEvents[0]?.content).toEqual({ kind: 'text', text: 'hello' });
expect(runtimeEvents[1]?.id).toBe('turn-1-delta');
expect(terminalEvents).toEqual([]);
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(view.messages.some((message) => message.type === 'user')).toBe(true);
expect((await manager.getMessages(session.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('fails immediately when a non-terminal canonical RuntimeEvent write fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 1 });
const canonicalRuntimeStore = Object.assign(runStore, { durability: 'canonical' as const });
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: canonicalRuntimeStore,
backends,
newId: nextId(),
now: nextNow(6_751),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await expectRejects(
collectSessionEvents(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/runtime event append failed/,
);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
expect(
(await runStore.readRuntimeEvents(session.id, run.runId)).map((event) => event.role),
).toEqual(['user']);
expect(run.status === 'completed').toBe(false);
});
test('backend errors before terminal synthesize a failed terminal RuntimeEvent before the failed header', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ThrowBeforeTerminalBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_800),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await expectRejects(
collectSessionEvents(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/backend failed before terminal/,
);
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run.runId);
const terminalEvents = runtimeEvents.filter(isTerminalRuntimeEvent);
expect(run.status).toBe('failed');
expect(run.failureClass).toBe('missing_terminal_event');
expect(runtimeEvents.map((event) => event.role)).toEqual(['user', 'model', 'system']);
expect(runtimeEvents[0]?.content).toEqual({ kind: 'text', text: 'hello' });
expect(runtimeEvents[1]?.id).toBe('turn-1-delta');
expect(runtimeEvents[2]?.id).toBe(terminalEvents[0]?.id);
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('failed');
expect(terminalEvents[0]?.invocationId).toBe(run.runId);
expect(terminalEvents[0]?.actions?.stateDelta?.failureClass).toBe('missing_terminal_event');
expect(terminalEvents[0]?.actions?.stateDelta?.recovered).toBeUndefined();
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(view.messages.some((message) => message.type === 'user')).toBe(true);
expect((await manager.getMessages(session.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('startup recovery preserves the terminal header RuntimeEvent invariant', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 2 });
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_850),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await expectRejects(
collectSessionEvents(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/runtime event append failed/,
);
await manager.recoverInterruptedSessions();
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run.runId);
const hasTerminalHeader = ['completed', 'failed', 'cancelled'].includes(run.status);
const hasTerminalFact = runtimeEvents.some(isTerminalRuntimeEvent);
expect(!hasTerminalHeader || hasTerminalFact).toBe(true);
const terminalEvents = runtimeEvents.filter(isTerminalRuntimeEvent);
expect(run.status).toBe('failed');
expect(run.failureClass).toBe('app_restarted');
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('failed');
expect(terminalEvents[0]?.actions?.stateDelta?.recovered).toBe(true);
expect(terminalEvents[0]?.actions?.stateDelta?.recoveryReason).toBe('run_interrupted');
expect(terminalEvents[0]?.actions?.stateDelta?.failureClass).toBe('app_restarted');
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(view.messages.some((message) => message.type === 'user')).toBe(true);
expect((await manager.getMessages(session.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('pre-terminal backend errors preserve the terminal invariant before startup recovery', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ThrowBeforeTerminalBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_875),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await expectRejects(
collectSessionEvents(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/backend failed before terminal/,
);
await manager.recoverInterruptedSessions();
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run.runId);
const hasTerminalHeader = ['completed', 'failed', 'cancelled'].includes(run.status);
const hasTerminalFact = runtimeEvents.some(isTerminalRuntimeEvent);
expect(!hasTerminalHeader || hasTerminalFact).toBe(true);
const terminalEvents = runtimeEvents.filter(isTerminalRuntimeEvent);
expect(run.status).toBe('failed');
expect(run.failureClass).toBe('missing_terminal_event');
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('failed');
expect(terminalEvents[0]?.invocationId).toBe(run.runId);
expect(terminalEvents[0]?.actions?.stateDelta?.recovered).toBeUndefined();
expect(terminalEvents[0]?.actions?.stateDelta?.recoveryReason).toBeUndefined();
expect(terminalEvents[0]?.actions?.stateDelta?.failureClass).toBe('missing_terminal_event');
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(view.messages.some((message) => message.type === 'user')).toBe(true);
expect((await manager.getMessages(session.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('sendMessage preserves token usage fields while resuming from an empty prior runtime ledger', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const newId = nextId();
const now = nextNow(7_000);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId,
now,
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'prior question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'prior answer',
modelId: 'fake-model',
},
{
type: 'token_usage',
id: 'legacy-usage',
turnId: 'turn-1',
ts: 103,
input: 100,
output: 25,
runtimeSteps: 3,
contextRemaining: 9000,
providerRequestTraceId: 'provider-trace-1',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 104,
status: 'completed',
partialOutputRetained: true,
},
]);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 104,
completedAt: 104,
}),
);
const restarted = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId,
now,
runtimeSource: 'test',
});
const sessionEvents = await collectSessionEvents(
restarted.sendMessage(session.id, { turnId: 'turn-2', text: 'follow up' }),
);
expect(sessionEvents.map((event) => event.type)).toEqual(['text_delta', 'complete']);
expect(backend?.sendInputs[0]?.context.map((message) => message.type)).toEqual([
'user',
'assistant',
'token_usage',
'turn_state',
]);
expect(
backend?.sendInputs[0]?.context.map((message) =>
'text' in message ? message.text : message.type,
),
).toEqual(['prior question', 'prior answer', 'token_usage', 'turn_state']);
expect(backend?.sendInputs[0]?.runtimeContext?.map((event) => event.runId)).toEqual([
'run-1',
'run-1',
'run-1',
'run-1',
]);
const resumedUsage = backend?.sendInputs[0]?.runtimeContext?.find(
(event) => event.actions?.tokenUsage,
);
expect(resumedUsage?.actions?.tokenUsage?.runtimeSteps).toBe(3);
expect(resumedUsage?.actions?.tokenUsage?.contextRemaining).toBe(9000);
expect(resumedUsage?.refs?.providerRequestTraceId).toBe('provider-trace-1');
const repairedRuntimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRuntimeEvents.map((event) => event.refs?.storedMessageId)).toEqual([
'legacy-user',
'legacy-assistant',
'legacy-usage',
'legacy-state',
]);
expect(repairedRuntimeEvents.at(-1)?.status).toBe('completed');
});
test('sendMessage rejects missing prior ledger when legacy message reads fail', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(7_025),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
);
store.failReadMessagesFor.add(session.id);
await expectRejects(
collectSessionEvents(
manager.sendMessage(session.id, { turnId: 'turn-2', text: 'follow up' }),
),
/RuntimeEvent ledger is missing for prior run run-1/,
);
});
test('sendMessage rejects prior runtime context without a valid terminal fact', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(7_050),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'prior question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'prior answer' },
}),
runtimeEvent({
id: 'rt-completed-a',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
status: 'completed',
actions: { endInvocation: true },
}),
runtimeEvent({
id: 'rt-completed-b',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 104,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await expectRejects(
drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'follow up' })),
/valid terminal fact/,
);
expect(backend?.sendInputs.length ?? 0).toBe(0);
const currentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-2',
);
if (!currentRun) throw new Error('current AgentRunStore run was not created');
expect(currentRun.status).toBe('failed');
expect(currentRun.failureClass).toBe('missing_terminal_event');
const currentTerminalEvents = (
await runStore.readRuntimeEvents(session.id, currentRun.runId)
).filter(isTerminalRuntimeEvent);
expect(currentTerminalEvents).toHaveLength(1);
expect(currentTerminalEvents[0]?.status).toBe('failed');
expect(currentTerminalEvents[0]?.actions?.stateDelta?.failureClass).toBe(
'missing_terminal_event',
);
});
test('sendMessage replays a prior run left non-terminal by an unanswered interaction', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(7_075),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
// A run parked on AskUserQuestion and then stopped: the header never left
// `waiting_for_user` and the ledger never received a terminal fact. Its
// turn is still conversation the model must see.
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'waiting_for_user',
createdAt: 100,
updatedAt: 102,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'prior question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'prior answer' },
}),
],
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'follow up' }));
expect(backend?.sendInputs[0]?.runtimeContext?.map((event) => event.id)).toEqual([
'rt-user',
'rt-assistant',
]);
});
test('sendMessage resumes incomplete legacy backfill for prior context', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 1 });
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(7_100),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'prior question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'prior answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
]);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
);
await expectRejects(manager.getMessages(session.id), /runtime event append failed/);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'follow up' }));
expect(
backend?.sendInputs[0]?.runtimeContext?.map((event) => event.refs?.storedMessageId),
).toEqual(['legacy-user', 'legacy-assistant', 'legacy-state']);
expect(
(await runStore.readRuntimeEvents(session.id, 'run-1')).map(
(event) => event.refs?.storedMessageId,
),
).toEqual(['legacy-user', 'legacy-assistant', 'legacy-state']);
});
test('RuntimeReadModel projects messages turns replay and terminal facts without SessionStore messages', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const session = await store.create(makeInput());
const seeded = await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'turn-1',
runId: 'run-1',
userText: 'runtime question',
assistantText: 'runtime answer',
legacyIdPrefix: 'cache',
});
store.failReadMessagesFor.add(session.id);
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
}).getSessionView(session.id);
expect(view.messages).toEqual(seeded.projectedMessages);
expect(view.turns).toEqual([
{
turnId: 'turn-1',
status: 'completed',
statusSource: 'recorded',
partialOutputRetained: true,
},
]);
expect(view.terminalFacts.map((fact) => fact.runStatus)).toEqual(['completed']);
expect(view.replayPlan.textMessages.map((message) => message.content)).toEqual([
'runtime question',
'runtime answer',
]);
});
test('SessionManager projects hosted permission details from the canonical outcome', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const header = makeRunHeader({ status: 'completed' });
await store.create(makeInput());
await seedCanonicalPermissionRun(runStore, header);
const canonicalPermissionOutcomes: CanonicalPermissionOutcomeReader = {
readPermissionOutcome: async (requestId) =>
canonicalPermissionRecord(header, {
requestId,
outcome: {
kind: 'permission_answer' as const,
decision: 'allow' as const,
rememberForTurn: true,
reviewer: 'auto_review' as const,
rationale: 'The requested write is limited to the reviewed path.',
riskLevel: 'medium' as const,
committedAt: 125,
},
}),
};
const backends = new BackendRegistry();
const interactionAuthority: RuntimeInteractionAuthority = {
bindRun: () => {
throw new Error('read-only test authority cannot bind a Run');
},
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(125),
interactionAuthority,
canonicalPermissionOutcomes,
});
const messages = await manager.getMessages(header.sessionId);
expect(messages.find((message) => message.type === 'permission_decision')).toEqual({
type: 'permission_decision',
id: 'request-canonical',
turnId: header.turnId,
ts: 125,
toolUseId: 'tool-canonical',
toolName: 'Write',
decision: 'allow',
rememberForTurn: true,
reviewer: 'auto_review',
rationale: 'The requested write is limited to the reviewed path.',
riskLevel: 'medium',
hint: 'write approval',
});
const cachedView = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: {
readMessages: async () =>
messages.filter((message) => message.type !== 'permission_decision'),
},
canonicalPermissionOutcomes,
}).getSessionView(header.sessionId);
expect(cachedView.diagnostics).toEqual([]);
});
test('SessionManager joins a canonical hosted permission without a ledger request', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const header = makeRunHeader({ status: 'completed' });
await store.create(makeInput());
await seedCanonicalPermissionRun(runStore, header, false);
const canonicalPermissionOutcomes: CanonicalPermissionOutcomeReader = {
readPermissionOutcome: async (requestId) =>
canonicalPermissionRecord(header, {
requestId,
outcome: {
kind: 'permission_answer',
decision: 'allow',
rememberForTurn: true,
reviewer: 'auto_review',
committedAt: 125,
},
}),
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(125),
interactionAuthority: {
bindRun: () => {
throw new Error('read-only test authority cannot bind a Run');
},
},
canonicalPermissionOutcomes,
});
const messages = await manager.getMessages(header.sessionId);
expect(messages.find((message) => message.type === 'permission_decision')).toEqual({
type: 'permission_decision',
id: 'request-canonical',
turnId: header.turnId,
ts: 125,
toolUseId: 'tool-canonical',
toolName: 'Write',
decision: 'allow',
rememberForTurn: true,
reviewer: 'auto_review',
});
});
test('RuntimeReadModel fails closed for a missing or mismatched hosted permission outcome', async () => {
const header = makeRunHeader({ status: 'completed' });
const canonical = canonicalPermissionRecord(header);
const outcomes: Array<CanonicalPermissionOutcomeRecord | undefined> = [
undefined,
{
...canonical,
sessionId: 'wrong-session',
outcome: {
kind: 'permission_answer' as const,
decision: 'deny' as const,
rememberForTurn: false as const,
reviewer: 'user' as const,
committedAt: 125,
},
},
];
for (const outcome of outcomes) {
const runStore = new MemoryAgentRunStore();
await seedCanonicalPermissionRun(runStore, header);
await assert.rejects(
new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
canonicalPermissionOutcomes: {
readPermissionOutcome: async () => outcome,
},
}).getSessionView(header.sessionId),
(error: unknown) =>
error instanceof RuntimeReadModelError &&
error.diagnostics.some((diagnostic) => diagnostic.code === 'incomplete_event'),
);
}
});
test('RuntimeReadModel bounds concurrent hosted permission outcome reads', {
timeout: 2_000,
}, async (t) => {
const runStore = new MemoryAgentRunStore();
const header = makeRunHeader({ status: 'completed' });
const requestIds = Array.from({ length: 20 }, (_, index) => `request-${index}`);
await seedRuntimeRun(runStore, header, [
runtimeEvent({
id: 'permission-concurrency-user',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 99,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'approve the operations' },
}),
...requestIds.map((requestId, index) =>
runtimeEvent({
id: `permission-concurrency-${index}`,
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 100 + index,
author: 'user',
actions: { permissionAnswerAccepted: { requestId } },
refs: { toolCallId: `tool-${requestId}` },
}),
),
runtimeEvent({
id: 'permission-concurrency-terminal',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 200,
status: 'completed',
actions: { endInvocation: true },
}),
]);
let activeReads = 0;
let maxActiveReads = 0;
let startedReads = 0;
let releaseReads!: () => void;
const readsReleased = new Promise<void>((resolve) => {
releaseReads = resolve;
});
t.after(() => releaseReads());
let initialWorkersStarted!: () => void;
const initialWorkers = new Promise<void>((resolve) => {
initialWorkersStarted = resolve;
});
const viewPromise = new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
canonicalPermissionOutcomes: {
readPermissionOutcome: async (requestId) => {
activeReads += 1;
startedReads += 1;
maxActiveReads = Math.max(maxActiveReads, activeReads);
if (startedReads === 8) initialWorkersStarted();
await readsReleased;
activeReads -= 1;
const canonical = canonicalPermissionRecord(header);
return {
...canonical,
requestId,
request: {
...canonical.request,
toolUseId: `tool-${requestId}`,
},
};
},
},
}).getSessionView(header.sessionId);
await initialWorkers;
expect(startedReads).toBe(8);
expect(maxActiveReads).toBe(8);
releaseReads();
const view = await viewPromise;
expect(startedReads).toBe(requestIds.length);
expect(maxActiveReads).toBe(8);
expect(view.messages.filter((message) => message.type === 'permission_decision').length).toBe(
requestIds.length,
);
});
test('RuntimeReadModel excludes child runs from the default session transcript', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const session = await store.create(makeInput());
await seedRuntimeReadTurnWithHeader({
store,
runStore,
sessionId: session.id,
turnId: 'parent-turn',
runId: 'parent-run',
userText: 'parent question',
assistantText: 'parent answer',
legacyIdPrefix: 'parent',
header: {},
tsBase: 100,
});
await seedRuntimeReadTurnWithHeader({
store,
runStore,
sessionId: session.id,
turnId: 'child-turn',
runId: 'child-run',
userText: 'child prompt',
assistantText: 'child private answer',
legacyIdPrefix: 'child',
header: { parentRunId: 'parent-run', agentName: 'Researcher' },
tsBase: 200,
});
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
}).getSessionView(session.id);
expect(view.runs.map((run) => run.runId)).toEqual(['parent-run']);
expect(view.messages.map((message) => message.turnId)).toEqual([
'parent-turn',
'parent-turn',
'parent-turn',
]);
expect(view.replayPlan.textMessages.map((message) => message.content)).toEqual([
'parent question',
'parent answer',
]);
});
test('projection/cache mismatch does not override RuntimeEvent read output', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const legacyMessages: StoredMessage[] = [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'legacy answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
];
await store.appendMessages(session.id, legacyMessages);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-complete',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
expect(await manager.getMessages(session.id)).toEqual([
{ type: 'user', id: 'rt-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'turn_state',
id: 'rt-complete',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: false,
},
]);
});
test('getMessages backfills low-risk legacy rows when a terminal run has no runtime ledger', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const legacyMessages: StoredMessage[] = [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'legacy only' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'legacy answer',
modelId: 'fake-model',
},
{
type: 'tool_call',
id: 'tool-1',
turnId: 'turn-1',
ts: 103,
toolName: 'Read',
args: { path: 'README.md' },
},
{
type: 'tool_result',
id: 'legacy-tool-result',
turnId: 'turn-1',
ts: 104,
toolUseId: 'tool-1',
isError: false,
content: { kind: 'text', text: 'file body' },
},
{ type: 'token_usage', id: 'legacy-usage', turnId: 'turn-1', ts: 105, input: 10, output: 5 },
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 106,
status: 'completed',
partialOutputRetained: true,
},
];
await store.appendMessages(session.id, legacyMessages);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
completedAt: 106,
}),
);
const messages = await manager.getMessages(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(messages.map((message) => message.type)).toEqual([
'user',
'assistant',
'tool_call',
'tool_result',
'token_usage',
'turn_state',
]);
expect(messages.map((message) => message.id)).toEqual([
'legacy-user',
'legacy-assistant',
'tool-1',
'legacy-tool-result',
'legacy-usage',
'legacy-state',
]);
expect(runtimeEvents.map((event) => event.refs?.storedMessageId)).toEqual([
'legacy-user',
'legacy-assistant',
'tool-1',
'legacy-tool-result',
'legacy-usage',
'legacy-state',
]);
expect(runtimeEvents.at(-1)?.status).toBe('completed');
});
test('getMessages backfills assistant text and thinking from one legacy message', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
thinking: { text: 'reasoning', signature: 'sig-1' },
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
]);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
);
await manager.getMessages(session.id);
await manager.getMessages(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(runtimeEvents.map((event) => event.content?.kind ?? event.status)).toEqual([
'text',
'text',
'thinking',
'completed',
]);
expect(runtimeEvents.map((event) => event.refs?.storedMessageId)).toEqual([
'legacy-user',
'legacy-assistant',
'legacy-assistant',
'legacy-state',
]);
});
test('getMessages preserves legacy transcript when fallback repair supplies missing terminal evidence', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
]);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
);
const messages = await manager.getMessages(session.id);
await manager.getMessages(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(messages.map((message) => message.type)).toEqual(['user', 'assistant', 'turn_state']);
expect(messages.map((message) => message.id)).toEqual([
'legacy-user',
'legacy-assistant',
runtimeEvents.at(-1)?.id,
]);
expect(runtimeEvents.map((event) => event.refs?.storedMessageId)).toEqual([
'legacy-user',
'legacy-assistant',
undefined,
]);
expect(runtimeEvents.at(-1)?.status).toBe('failed');
expect(runtimeEvents.at(-1)?.actions?.stateDelta?.failureClass).toBe('missing_terminal_event');
});
test('getMessages resumes incomplete legacy backfill after append failure', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 1 });
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
]);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
);
await expectRejects(manager.getMessages(session.id), /runtime event append failed/);
const messages = await manager.getMessages(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(messages.map((message) => message.type)).toEqual(['user', 'assistant', 'turn_state']);
expect(messages.map((message) => message.id)).toEqual([
'legacy-user',
'legacy-assistant',
'legacy-state',
]);
expect(runtimeEvents.map((event) => event.refs?.storedMessageId)).toEqual([
'legacy-user',
'legacy-assistant',
'legacy-state',
]);
});
test('getMessages repairs a non-empty RuntimeEvent ledger that is missing only the terminal fact', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
const messages = await manager.getMessages(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(messages.map((message) => message.type)).toEqual(['user', 'assistant', 'turn_state']);
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
});
expect(runtimeEvents.slice(0, 2).map((event) => event.id)).toEqual(['rt-user', 'rt-assistant']);
expect(runtimeEvents.at(-1)?.status).toBe('completed');
expect(runtimeEvents.at(-1)?.refs?.storedMessageId).toBe('legacy-state');
});
test('getMessages does not trust failed legacy terminal state for a completed run header', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'tool_failed',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
const messages = await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.status).toBe('failed');
expect(repairedRun.failureClass).toBe('missing_terminal_event');
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: runtimeEvents.at(-1)?.id,
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'missing_terminal_event',
partialOutputRetained: true,
});
expect(runtimeEvents.at(-1)?.status).toBe('failed');
});
test('getMessages does not trust aborted legacy terminal state for a completed run header', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'aborted',
abortedAt: 103,
abortSource: 'user',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
const messages = await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.status).toBe('failed');
expect(repairedRun.failureClass).toBe('missing_terminal_event');
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: runtimeEvents.at(-1)?.id,
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'missing_terminal_event',
partialOutputRetained: true,
});
expect(runtimeEvents.at(-1)?.status).toBe('failed');
});
test('getMessages preserves failed legacy terminal state when failureClass is missing', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'tool_failed',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'failed',
failureClass: undefined,
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
const messages = await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.status).toBe('failed');
expect(repairedRun.failureClass).toBe('tool_failed');
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'tool_failed',
partialOutputRetained: true,
});
expect(runtimeEvents.at(-1)?.status).toBe('failed');
});
test('getMessages waits for legacy messages before fallback repair', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
store.failReadMessagesFor.add(session.id);
await manager.getMessages(session.id).catch(() => undefined);
const runAfterBlockedRepair = await runStore.readRun(session.id, 'run-1');
const runtimeEventsAfterBlockedRepair = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(runAfterBlockedRepair.status).toBe('completed');
expect(runAfterBlockedRepair.failureClass).toBeUndefined();
expect(runtimeEventsAfterBlockedRepair.some(isTerminalRuntimeEvent)).toBe(false);
store.failReadMessagesFor.delete(session.id);
const messagesAfterBlockedRepair = await store.readMessages(session.id);
expect(
messagesAfterBlockedRepair.filter(
(message) => message.type === 'turn_state' && message.status === 'failed',
),
).toHaveLength(0);
await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const repairedRuntimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
const terminalEvents = repairedRuntimeEvents.filter(isTerminalRuntimeEvent);
expect(repairedRun.status).toBe('completed');
expect(repairedRun.failureClass).toBeUndefined();
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('completed');
});
test('getMessages repair writes terminal turn_state for a continuation run', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const sourceRunId = 'repair-source-run';
const sourceTurnId = 'repair-source-turn';
const sourceInvocationId = 'repair-source-invocation';
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
status: 'completed',
createdAt: 100,
updatedAt: 101,
completedAt: 101,
}),
[
runtimeEvent({
id: 'repair-source-complete',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 101,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'repair-continuation-run',
turnId: 'repair-continuation-turn',
status: 'completed',
parentRunId: sourceRunId,
parentTurnId: sourceTurnId,
continuationSource: {
sourceInvocationId,
sourceRunId,
sourceTurnId,
sourceRuntimeEventHighWater: 1,
},
createdAt: 102,
updatedAt: 104,
completedAt: 104,
}),
[
runtimeEvent({
id: 'repair-continuation-text',
invocationId: 'repair-continuation-invocation',
sessionId: session.id,
runId: 'repair-continuation-run',
turnId: 'repair-continuation-turn',
ts: 103,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'retained continuation output' },
}),
],
);
await manager.getMessages(session.id);
const cachedMessages = await store.readMessages(session.id);
expect(
cachedMessages.find(
(message) => message.type === 'turn_state' && message.turnId === 'repair-continuation-turn',
),
).toMatchObject({
type: 'turn_state',
status: 'failed',
errorClass: 'missing_terminal_event',
parentTurnId: sourceTurnId,
partialOutputRetained: false,
});
});
test('getMessages can retry repair when the failed header update is interrupted', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failUpdateRunOnce: true });
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
await expectRejects(manager.getMessages(session.id), /update run failed/);
const messages = await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.status).toBe('failed');
expect(repairedRun.failureClass).toBe('missing_terminal_event');
expect(messages.at(-1)?.type).toBe('turn_state');
expect(runtimeEvents.filter((event) => event.status === 'failed')).toHaveLength(1);
});
test('getMessages repairs missing failed header class from an existing terminal RuntimeEvent', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'failed',
failureClass: undefined,
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
runtimeEvent({
id: 'rt-failed',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
role: 'system',
author: 'system',
status: 'failed',
content: {
kind: 'error',
code: 'tool_failed',
reason: 'tool_failed',
message: 'tool failed',
},
actions: { endInvocation: true, stateDelta: { failureClass: 'tool_failed' } },
}),
],
);
const messages = await manager.getMessages(session.id);
await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.failureClass).toBe('tool_failed');
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: 'rt-failed',
turnId: 'turn-1',
ts: 103,
status: 'failed',
errorClass: 'tool_failed',
partialOutputRetained: true,
});
expect(runtimeEvents.filter((event) => event.status === 'failed')).toHaveLength(1);
});
test('getMessages uses fallback failed header class when an existing terminal RuntimeEvent has no class', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'failed',
failureClass: undefined,
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
runtimeEvent({
id: 'rt-failed',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
],
);
await manager.getMessages(session.id);
await manager.getMessages(session.id);
const repairedRun = await runStore.readRun(session.id, 'run-1');
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.failureClass).toBe('missing_terminal_event');
expect(runtimeEvents.filter((event) => event.status === 'failed')).toHaveLength(1);
});
test('getMessages serializes concurrent terminal repairs for the same run', async () => {
const store = new MemorySessionStore();
let repairReads = 0;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventRead: async (_sessionId, runId) => {
if (runId !== 'run-1' || repairReads >= 2) return;
repairReads += 1;
await Promise.resolve();
},
});
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'question' },
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 102,
text: 'answer',
modelId: 'fake-model',
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
}),
],
);
await Promise.all([manager.getMessages(session.id), manager.getMessages(session.id)]);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(runtimeEvents.filter((event) => event.status === 'failed')).toHaveLength(1);
});
test('getMessages includes continuation output without inlining child agent output', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const sourceRunId = 'source-run';
const sourceTurnId = 'source-turn';
const sourceInvocationId = 'source-invocation';
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
status: 'failed',
createdAt: 100,
updatedAt: 102,
completedAt: 102,
failureClass: 'app_restarted',
}),
[
runtimeEvent({
id: 'source-user',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'write an article' },
}),
runtimeEvent({
id: 'source-failed',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 102,
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
status: 'completed',
parentRunId: sourceRunId,
parentTurnId: sourceTurnId,
continuationSource: {
sourceInvocationId,
sourceRunId,
sourceTurnId,
sourceRuntimeEventHighWater: 2,
},
createdAt: 103,
updatedAt: 105,
completedAt: 105,
}),
[
runtimeEvent({
id: 'continuation-start',
invocationId: 'continuation-invocation',
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
ts: 103,
role: 'system',
author: 'system',
actions: {
stateDelta: { continuationStart: true },
runtimeProtocol: { toolBoundary: 't1_after_preflight_v1' },
},
refs: {
sourceInvocationId,
sourceRunId,
sourceTurnId,
sourceRuntimeEventHighWater: 2,
},
}),
runtimeEvent({
id: 'continuation-article',
invocationId: 'continuation-invocation',
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
ts: 104,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'the resumed article' },
}),
runtimeEvent({
id: 'continuation-complete',
invocationId: 'continuation-invocation',
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
ts: 105,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
parentRunId: sourceRunId,
parentTurnId: sourceTurnId,
createdAt: 106,
updatedAt: 108,
completedAt: 108,
}),
[
runtimeEvent({
id: 'child-answer',
invocationId: 'child-invocation',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 107,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'private child output' },
}),
runtimeEvent({
id: 'child-complete',
invocationId: 'child-invocation',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 108,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const messages = await manager.getMessages(session.id);
const assistantTexts = messages.flatMap((message) =>
message.type === 'assistant' ? [message.text] : [],
);
expect(assistantTexts).toContain('the resumed article');
expect(assistantTexts).not.toContain('private child output');
expect(messages.some((message) => message.id === 'continuation-start')).toBe(false);
});
test('getMessages includes in-flight projection cache rows for an active RuntimeEvent run', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const completed = await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'turn-1',
runId: 'run-1',
userText: 'completed question',
assistantText: 'completed answer',
legacyIdPrefix: 'legacy',
});
const activeMessages: StoredMessage[] = [
{ type: 'user', id: 'active-user', turnId: 'turn-2', ts: 201, text: 'active question' },
{
type: 'assistant',
id: 'active-assistant',
turnId: 'turn-2',
ts: 202,
text: 'partial active answer',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'active-state',
turnId: 'turn-2',
ts: 203,
status: 'running',
partialOutputRetained: true,
},
];
await store.appendMessages(session.id, activeMessages);
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-2',
turnId: 'turn-2',
status: 'running',
createdAt: 200,
updatedAt: 203,
}),
);
const messages = await manager.getMessages(session.id);
expect(messages).toEqual([...completed.projectedMessages, ...activeMessages]);
expect(await manager.listTurns(session.id)).toEqual([
{
turnId: 'turn-1',
status: 'completed',
statusSource: 'recorded',
partialOutputRetained: true,
},
{
turnId: 'turn-2',
status: 'running',
statusSource: 'recorded',
partialOutputRetained: true,
},
]);
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
expect(
view.diagnostics.some(
(diagnostic) =>
diagnostic.code === 'incomplete_event' &&
diagnostic.message.includes('in-flight projection cache'),
),
).toBe(true);
});
test('getMessages overlays a canonical permission acceptance from a running ledger', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const session = await store.create(makeInput());
const header = makeRunHeader({
sessionId: session.id,
runId: 'active-run',
turnId: 'active-turn',
status: 'running',
createdAt: 100,
updatedAt: 125,
});
await runStore.createRun(header);
await store.appendMessages(session.id, [
{
type: 'user',
id: 'active-user',
turnId: header.turnId,
ts: 100,
text: 'write the file',
},
{
type: 'turn_state',
id: 'active-state',
turnId: header.turnId,
ts: 101,
status: 'running',
partialOutputRetained: false,
},
]);
await runStore.appendRuntimeEvent(
session.id,
header.runId,
runtimeEvent({
id: 'active-permission-request',
sessionId: session.id,
runId: header.runId,
turnId: header.turnId,
ts: 110,
actions: {
permissionRequest: {
kind: 'tool_permission',
requestId: 'request-canonical',
toolUseId: 'tool-canonical',
toolName: 'Write',
category: 'file_write',
reason: 'file_write',
args: { path: '/tmp/file' },
rememberForTurnAllowed: true,
hint: 'write approval',
},
},
refs: { toolCallId: 'tool-canonical' },
}),
);
await runStore.appendRuntimeEvent(
session.id,
header.runId,
runtimeEvent({
id: 'active-permission-acceptance',
sessionId: session.id,
runId: header.runId,
turnId: header.turnId,
ts: 126,
actions: { permissionAnswerAccepted: { requestId: 'request-canonical' } },
refs: { toolCallId: 'tool-canonical' },
}),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(125),
interactionAuthority: {
bindRun: () => {
throw new Error('read-only test authority cannot bind a Run');
},
},
canonicalPermissionOutcomes: {
readPermissionOutcome: async () =>
canonicalPermissionRecord(header, {
outcome: {
kind: 'permission_answer',
decision: 'allow',
rememberForTurn: true,
reviewer: 'auto_review',
committedAt: 125,
},
}),
},
});
expect(
(await manager.getMessages(session.id)).find(
(message) => message.type === 'permission_decision',
),
).toMatchObject({
type: 'permission_decision',
id: 'request-canonical',
turnId: header.turnId,
toolUseId: 'tool-canonical',
decision: 'allow',
reviewer: 'auto_review',
});
});
test('the in-flight overlay keeps a pending sandbox boundary request visible in the view', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const session = await store.create(makeInput());
const header = makeRunHeader({
sessionId: session.id,
runId: 'active-run',
turnId: 'active-turn',
status: 'running',
createdAt: 100,
updatedAt: 125,
});
await runStore.createRun(header);
await store.appendMessages(session.id, [
{ type: 'user', id: 'active-user', turnId: header.turnId, ts: 100, text: 'build it' },
{
type: 'turn_state',
id: 'active-state',
turnId: header.turnId,
ts: 101,
status: 'running',
partialOutputRetained: false,
},
]);
await runStore.appendRuntimeEvent(
session.id,
header.runId,
runtimeEvent({
id: 'active-boundary-request',
sessionId: session.id,
runId: header.runId,
turnId: header.turnId,
ts: 110,
actions: {
stateDelta: {
sandboxBoundaryRequest: {
requestId: 'boundary-pending',
toolUseId: 'tool-boundary',
justification: 'Write outside the workspace.',
expansion: { network: { enabled: true } },
},
},
},
refs: { toolCallId: 'tool-boundary' },
}),
);
await runStore.appendRuntimeEvent(
session.id,
header.runId,
runtimeEvent({
id: 'active-boundary-decision',
sessionId: session.id,
runId: header.runId,
turnId: header.turnId,
ts: 111,
author: 'user',
actions: {
stateDelta: {
sandboxBoundaryDecision: {
requestId: 'boundary-pending',
decision: 'deny',
status: 'denied',
revision: 0,
},
},
},
refs: { toolCallId: 'tool-boundary' },
}),
);
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
projectionCache: store,
}).getSessionView(session.id);
const readRequestId = (value: unknown): string[] =>
value !== null && typeof value === 'object' && 'requestId' in value
? [(value as { requestId: string }).requestId]
: [];
// Both halves of the interaction survive: without the decision the view
// would show a request that never resolves.
expect(
view.events.flatMap((event) =>
readRequestId(event.actions?.stateDelta?.sandboxBoundaryRequest),
),
).toEqual(['boundary-pending']);
expect(
view.events.flatMap((event) =>
readRequestId(event.actions?.stateDelta?.sandboxBoundaryDecision),
),
).toEqual(['boundary-pending']);
});
test('MAKA_RUNTIME_READ_SOURCE does not force legacy reads when RuntimeEvents are complete', async () => {
const previous = process.env.MAKA_RUNTIME_READ_SOURCE;
process.env.MAKA_RUNTIME_READ_SOURCE = 'legacy';
try {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const seeded = await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'turn-1',
runId: 'run-1',
userText: 'legacy forced question',
assistantText: 'legacy forced answer',
legacyIdPrefix: 'legacy',
});
expect(await manager.getMessages(session.id)).toEqual(seeded.projectedMessages);
} finally {
if (previous === undefined) delete process.env.MAKA_RUNTIME_READ_SOURCE;
else process.env.MAKA_RUNTIME_READ_SOURCE = previous;
}
});
test('listTurns derives from the RuntimeEvent-primary message view', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'turn-1',
runId: 'run-1',
userText: 'runtime question',
assistantText: 'runtime answer',
legacyIdPrefix: 'legacy',
});
store.failListTurnsFor.add(session.id);
const turns = await manager.listTurns(session.id);
expect(turns).toEqual([
{
turnId: 'turn-1',
status: 'completed',
statusSource: 'recorded',
partialOutputRetained: true,
},
]);
});
test('mixed projection-cache-only system notes do not override RuntimeEvent projection', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
const seeded = await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'turn-1',
runId: 'run-1',
userText: 'question',
assistantText: 'answer',
legacyIdPrefix: 'legacy',
});
const legacyNote: StoredMessage = {
type: 'system_note',
id: 'legacy-note',
ts: 104,
kind: 'mode_change',
data: { from: 'ask', to: 'execute' },
};
await store.appendMessage(session.id, legacyNote);
const messages = await manager.getMessages(session.id);
expect(messages).toEqual(seeded.projectedMessages);
});
test('getMessages orders RuntimeEvent-primary reads by session event chronology across runs', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'slow-run',
turnId: 'slow',
status: 'completed',
createdAt: 100,
updatedAt: 107,
completedAt: 107,
}),
[
runtimeEvent({
id: 'slow-user',
sessionId: session.id,
runId: 'slow-run',
turnId: 'slow',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'slow question' },
refs: { storedMessageId: 'slow-user-message' },
}),
runtimeEvent({
id: 'slow-assistant',
sessionId: session.id,
runId: 'slow-run',
turnId: 'slow',
ts: 106,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'slow answer' },
refs: { storedMessageId: 'slow-assistant-message' },
}),
runtimeEvent({
id: 'slow-complete',
sessionId: session.id,
runId: 'slow-run',
turnId: 'slow',
ts: 107,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'fast-run',
turnId: 'fast',
status: 'completed',
createdAt: 102,
updatedAt: 105,
completedAt: 105,
}),
[
runtimeEvent({
id: 'fast-user',
sessionId: session.id,
runId: 'fast-run',
turnId: 'fast',
ts: 103,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'fast question' },
refs: { storedMessageId: 'fast-user-message' },
}),
runtimeEvent({
id: 'fast-assistant',
sessionId: session.id,
runId: 'fast-run',
turnId: 'fast',
ts: 104,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'fast answer' },
refs: { storedMessageId: 'fast-assistant-message' },
}),
runtimeEvent({
id: 'fast-complete',
sessionId: session.id,
runId: 'fast-run',
turnId: 'fast',
ts: 105,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
store.failNextReadMessagesFor.set(session.id, 1);
const messages = await manager.getMessages(session.id);
expect(
messages.map(
(message) =>
`${message.type}:${'turnId' in message ? message.turnId : 'none'}:${message.ts}`,
),
).toEqual([
'user:slow:101',
'user:fast:103',
'assistant:fast:104',
'turn_state:fast:105',
'assistant:slow:106',
'turn_state:slow:107',
]);
});
test('regenerate finds completed source turns through the RuntimeEvent-primary view', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_770),
});
const session = await manager.createSession(makeInput());
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'source',
runId: 'source-run',
userText: 'runtime regenerate text',
assistantText: 'runtime answer',
legacyIdPrefix: 'legacy',
});
store.failNextReadMessagesFor.set(session.id, 1);
await drain(manager.regenerateTurn(session.id, { sourceTurnId: 'source', turnId: 'regen-1' }));
const messages = await store.readMessages(session.id);
const regenUser = messages.find(
(message) => message.type === 'user' && message.turnId === 'regen-1',
);
expect(regenUser?.type === 'user' ? regenUser.text : undefined).toBe('runtime regenerate text');
const regenState = deriveTurnRecords(messages).find((turn) => turn.turnId === 'regen-1');
expect(regenState?.regeneratedFromTurnId).toBe('source');
});
test('regenerate is stop-visible during its first source-ledger preflight', async () => {
const preflightStarted = makeGate();
const releasePreflight = makeGate();
let gatePreflight = false;
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({
beforeListSessionRuns: async () => {
if (!gatePreflight) return;
gatePreflight = false;
preflightStarted.release();
await releasePreflight.promise;
},
});
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_775),
});
const session = await manager.createSession(makeInput());
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'source',
runId: 'source-run',
userText: 'do not regenerate after stop',
assistantText: 'source answer',
legacyIdPrefix: 'legacy-stop',
});
gatePreflight = true;
const regenerating = manager
.regenerateTurn(session.id, {
sourceTurnId: 'source',
turnId: 'regen-stopped-preflight',
})
[Symbol.asyncIterator]();
const firstEvent = regenerating.next();
await preflightStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releasePreflight.release();
await stopping;
expect((await firstEvent).done).toBe(true);
expect(backend?.sendInputs).toEqual([]);
const regenerated = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'regen-stopped-preflight',
);
expect(regenerated?.status).toBe('cancelled');
expect((await store.readHeader(session.id)).status === 'blocked').toBe(false);
});
test('regenerate accepts an aborted source turn (retry semantics merged into regenerate)', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_780),
});
const session = await manager.createSession(makeInput());
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
status: 'cancelled',
createdAt: 100,
updatedAt: 102,
completedAt: 102,
}),
[
runtimeEvent({
id: 'source-user',
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'aborted turn text' },
}),
runtimeEvent({
id: 'source-abort',
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
ts: 102,
role: 'system',
author: 'system',
status: 'aborted',
actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } },
}),
],
);
store.failNextReadMessagesFor.set(session.id, 1);
await drain(
manager.regenerateTurn(session.id, { sourceTurnId: 'source', turnId: 'regen-aborted' }),
);
const regenUser = (await store.readMessages(session.id)).find(
(message) => message.type === 'user' && message.turnId === 'regen-aborted',
);
expect(regenUser?.type === 'user' ? regenUser.text : undefined).toBe('aborted turn text');
});
test('branchFromTurn copies through the RuntimeEvent-primary message boundary', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'source',
runId: 'source-run',
userText: 'runtime branch context',
assistantText: 'runtime branch answer',
legacyIdPrefix: 'legacy',
});
store.failNextReadMessagesFor.set(session.id, 1);
const child = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Child',
});
const childMessages = await store.readMessages(child.id);
expect(childMessages[0]).toMatchObject({
type: 'user',
turnId: 'source',
text: 'runtime branch context',
});
expect(childMessages[1]).toMatchObject({
type: 'assistant',
turnId: 'source',
text: 'runtime branch answer',
});
expect(childMessages[2]).toMatchObject({ type: 'system_note', kind: 'session_start' });
expect(childMessages.some((message) => message.type === 'turn_state')).toBe(false);
const runtimeMessages = await manager.getMessages(child.id);
expect(runtimeMessages[0]).toMatchObject({
type: 'user',
turnId: 'source',
text: 'runtime branch context',
});
expect(runtimeMessages[1]).toMatchObject({
type: 'assistant',
turnId: 'source',
text: 'runtime branch answer',
});
});
test('conversation copies materialize requestless hosted permission history', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const session = await store.create(makeInput({ name: 'Parent' }));
const header = makeRunHeader({
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
status: 'completed',
createdAt: 100,
updatedAt: 130,
completedAt: 130,
});
await seedCanonicalPermissionRun(runStore, header, false);
const sourcePermissionEvents = await runStore.readRuntimeEvents(session.id, header.runId);
expect(sourcePermissionEvents.some((event) => event.actions?.permissionRequest)).toBe(false);
expect(sourcePermissionEvents.some((event) => event.content?.kind === 'function_call')).toBe(
false,
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'later-run',
turnId: 'later',
status: 'completed',
createdAt: 140,
updatedAt: 141,
completedAt: 141,
}),
[
runtimeEvent({
id: 'later-user',
sessionId: session.id,
runId: 'later-run',
turnId: 'later',
ts: 140,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'replace this turn' },
}),
runtimeEvent({
id: 'later-terminal',
sessionId: session.id,
runId: 'later-run',
turnId: 'later',
ts: 141,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
let sourceInteractionAvailable = true;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_755),
interactionAuthority: testInteractionAuthority(),
canonicalPermissionOutcomes: {
readPermissionOutcome: async () =>
sourceInteractionAvailable ? canonicalPermissionRecord(header) : undefined,
},
});
const child = await manager.branchFromTurn(session.id, {
sourceTurnId: header.turnId,
name: 'Child',
});
const revision = await manager.reviseBeforeTurn(session.id, { sourceTurnId: 'later' });
sourceInteractionAvailable = false;
const [childSourceRun] = await runStore.listSessionRuns(child.id);
const childSourceEvents = childSourceRun
? await runStore.readRuntimeEvents(child.id, childSourceRun.runId)
: [];
expect(
childSourceEvents.find((event) => event.actions?.permissionDecision)?.actions
?.permissionDecision,
).toMatchObject({
requestId: 'request-canonical',
toolName: 'Write',
decision: 'deny',
});
expect(childSourceEvents.some((event) => event.actions?.permissionRequest)).toBe(false);
expect(childSourceEvents.some((event) => event.actions?.permissionAnswerAccepted)).toBe(false);
for (const copiedSession of [child, revision]) {
expect(
(await manager.getMessages(copiedSession.id)).find(
(message) => message.type === 'permission_decision',
),
).toMatchObject({
type: 'permission_decision',
id: 'request-canonical',
turnId: header.turnId,
toolUseId: 'tool-canonical',
toolName: 'Write',
decision: 'deny',
reviewer: 'user',
});
}
await drain(manager.sendMessage(child.id, { turnId: 'child-next', text: 'continue' }));
expect(
backendInstances
.find((backend) => backend.sessionId === child.id)
?.sendInputs[0]?.context.find((message) => message.type === 'permission_decision'),
).toMatchObject({
id: 'request-canonical',
toolUseId: 'tool-canonical',
toolName: 'Write',
decision: 'deny',
});
});
test('branch child next turn receives cloned RuntimeEvent context', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_870),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'branch seed' }));
const child = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Child',
});
await store.appendMessage(child.id, {
type: 'assistant',
id: 'child-cache-only',
turnId: 'cache-only',
ts: 6_999,
text: 'cache-only child context',
modelId: 'fake-model',
});
await drain(manager.sendMessage(child.id, { turnId: 'child-next', text: 'child follow-up' }));
const childInput = backendInstances[1]?.sendInputs[0];
if (!childInput) throw new Error('child backend input was not recorded');
expect(
childInput.context.some(
(message) =>
message.type === 'user' && message.turnId === 'source' && message.text === 'branch seed',
),
).toBe(true);
expect(
childInput.context.some(
(message) => message.type === 'assistant' && message.id === 'child-cache-only',
),
).toBe(false);
expect(childInput.runtimeContext?.map((event) => event.turnId)).toEqual([
'source',
'source',
'source',
]);
expect(childInput.runtimeContext?.[0]?.sessionId).toBe(child.id);
});
test('branchFromTurn removes an incomplete target when Runtime ledger copy fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 5 });
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'source',
runId: 'source-run',
userText: 'runtime branch context',
assistantText: 'runtime branch answer',
legacyIdPrefix: 'legacy',
});
let branchError: unknown;
try {
await manager.branchFromTurn(session.id, { sourceTurnId: 'source', name: 'Child' });
} catch (error) {
branchError = error;
}
expect(branchError instanceof Error ? branchError.message : String(branchError)).toContain(
'runtime event append failed',
);
const child = (await store.list()).find((summary) => summary.parentSessionId === session.id);
expect(child).toBeUndefined();
const childRuns = await runStore.listSessionRuns('session-2');
for (const run of childRuns) {
const runtimeEvents = await runStore.readRuntimeEvents('session-2', run.runId);
const hasTerminalFact = runtimeEvents.some(isTerminalRuntimeEvent);
expect(
run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled'
? hasTerminalFact
: true,
).toBe(true);
}
});
test('multi-run RuntimeEvent projection preserves retry regenerate and branch lineage on turns', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = makeManagerForReadCutover(store, runStore);
const session = await manager.createSession(makeInput());
await seedRuntimeReadTurn({
store,
runStore,
sessionId: session.id,
turnId: 'root',
runId: 'root-run',
userText: 'root question',
assistantText: 'root answer',
legacyIdPrefix: 'root-legacy',
});
await seedRuntimeReadTurnWithHeader({
store,
runStore,
sessionId: session.id,
turnId: 'retry',
runId: 'retry-run',
userText: 'retry question',
assistantText: 'retry answer',
legacyIdPrefix: 'retry-legacy',
header: { parentTurnId: 'root', retriedFromTurnId: 'root' },
tsBase: 200,
});
await seedRuntimeReadTurnWithHeader({
store,
runStore,
sessionId: session.id,
turnId: 'regen',
runId: 'regen-run',
userText: 'regen question',
assistantText: 'regen answer',
legacyIdPrefix: 'regen-legacy',
header: { parentTurnId: 'root', regeneratedFromTurnId: 'root' },
tsBase: 300,
});
await seedRuntimeReadTurnWithHeader({
store,
runStore,
sessionId: session.id,
turnId: 'branch',
runId: 'branch-run',
userText: 'branch question',
assistantText: 'branch answer',
legacyIdPrefix: 'branch-legacy',
header: { parentSessionId: 'parent-session', branchOfTurnId: 'root' },
tsBase: 400,
});
store.failNextReadMessagesFor.set(session.id, 1);
const turns = await manager.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'retry')).toMatchObject({
status: 'completed',
parentTurnId: 'root',
retriedFromTurnId: 'root',
});
expect(turns.find((turn) => turn.turnId === 'regen')).toMatchObject({
status: 'completed',
parentTurnId: 'root',
regeneratedFromTurnId: 'root',
});
expect(turns.find((turn) => turn.turnId === 'branch')).toMatchObject({
status: 'completed',
parentSessionId: 'parent-session',
branchOfTurnId: 'root',
});
});
test('next turn projects failed prior RuntimeEvents with the terminal fact failure class', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_810),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
createdAt: 101,
updatedAt: 103,
}),
[
runtimeEvent({
id: 'rt-user',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'first' },
}),
runtimeEvent({
id: 'rt-assistant',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'before failure' },
}),
runtimeEvent({
id: 'rt-failed',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true, stateDelta: { failureClass: 'tool_failed' } },
}),
],
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' }));
const secondInput = backendInstances[0]?.sendInputs[0];
if (!secondInput) throw new Error('backend input was not recorded');
expect(secondInput.runtimeContext?.map((event) => event.turnId)).toEqual([
'turn-1',
'turn-1',
'turn-1',
]);
const turnState = secondInput.context.find(
(message) => message.type === 'turn_state' && message.turnId === 'turn-1',
);
if (turnState?.type !== 'turn_state')
throw new Error('prior failed turn_state was not projected');
expect(turnState.status).toBe('failed');
expect(turnState.errorClass).toBe('tool_failed');
});
test('next turn uses failed terminal RuntimeEvents when failed header commit was interrupted', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'failed' });
const backends = new BackendRegistry();
let backend: TurnScriptBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TurnScriptBackend(ctx, [
[{ type: 'complete', stopReason: 'error' }],
[
{ type: 'text_delta', messageId: 'm2', text: 'second ok' },
{ type: 'complete', stopReason: 'end_turn' },
],
]);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_812),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first' }));
const [firstRun] = await runStore.listSessionRuns(session.id);
if (!firstRun) throw new Error('first run was not recorded');
expect(firstRun.status).toBe('running');
const firstRuntimeEvents = await runStore.readRuntimeEvents(session.id, firstRun.runId);
const firstTerminalEvents = firstRuntimeEvents.filter(isTerminalRuntimeEvent);
expect(firstTerminalEvents).toHaveLength(1);
expect(firstTerminalEvents[0]?.status).toBe('failed');
expect(firstTerminalEvents[0]?.actions?.stateDelta?.failureClass).toBe('runtime_error');
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' }));
const secondInput = backend?.sendInputs[1];
if (!secondInput) throw new Error('second backend input was not recorded');
expect(secondInput.runtimeContext?.map((event) => event.turnId)).toEqual(['turn-1', 'turn-1']);
const turnState = secondInput.context.find(
(message) => message.type === 'turn_state' && message.turnId === 'turn-1',
);
if (turnState?.type !== 'turn_state')
throw new Error('prior failed turn_state was not projected');
expect(turnState.status).toBe('failed');
expect(turnState.errorClass).toBe('runtime_error');
const terminalEventsAfterSecondTurn = (
await runStore.readRuntimeEvents(session.id, firstRun.runId)
).filter(isTerminalRuntimeEvent);
expect(terminalEventsAfterSecondTurn).toHaveLength(1);
});
test('next parent turn excludes child run RuntimeEvents from model context', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_825),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
createdAt: parentRun.updatedAt + 1,
updatedAt: parentRun.updatedAt + 4,
completedAt: parentRun.updatedAt + 4,
parentRunId: parentRun.runId,
agentName: 'Researcher',
}),
[
runtimeEvent({
id: 'child-user',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: parentRun.updatedAt + 2,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'child prompt' },
}),
runtimeEvent({
id: 'child-assistant',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: parentRun.updatedAt + 3,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'child private answer' },
}),
runtimeEvent({
id: 'child-complete',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: parentRun.updatedAt + 4,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' }));
const secondInput = backendInstances[0]?.sendInputs[1];
if (!secondInput) throw new Error('second backend input was not recorded');
expect(secondInput.runtimeContext?.map((event) => event.turnId)).toEqual([
'turn-1',
'turn-1',
'turn-1',
]);
expect(secondInput.runtimeContext?.some((event) => event.turnId === 'child-turn')).toBe(false);
expect(
secondInput.context.some(
(message) => message.type === 'user' && message.turnId === 'child-turn',
),
).toBe(false);
});
test('child run input records parentRunId and starts without implicit prior context', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_835),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await drain(
manager.sendMessage(session.id, {
turnId: 'child-turn',
text: 'child prompt',
parentRunId: parentRun.runId,
agentName: 'Researcher',
}),
);
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
if (!childRun) throw new Error('child run was not recorded');
expect(childRun.parentRunId).toBe(parentRun.runId);
expect(childRun.agentName).toBe('Researcher');
const childInput = backendInstances[0]?.sendInputs[1];
if (!childInput) throw new Error('child backend input was not recorded');
expect(childInput.context).toEqual([]);
expect(childInput.runtimeContext).toBe(undefined);
});
test('startChildTurn uses a separate explore backend with the catalog child definition', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const contexts: BackendFactoryContext[] = [];
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
contexts.push(ctx);
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const childTools = [
testTool('Read'),
testTool('Bash'),
testTool('Glob'),
testTool('WebSearch'),
testTool('Grep'),
testTool('ExploreAgent'),
];
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools,
newId: nextId(),
now: nextNow(6_840),
runtimeSource: 'test',
});
const session = await manager.createSession(
makeInput({ permissionMode: 'ask', orchestrationMode: 'swarm' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await drain(
manager.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: 'Injected Name',
systemPrompt: 'Injected child prompt.',
},
prompt: 'inspect the repo',
}),
);
expect(contexts.map((ctx) => ctx.header.permissionMode)).toEqual(['ask', 'explore']);
expect(contexts[1]?.systemPrompt).toBe(LOCAL_READ_AGENT_DEFINITION.systemPrompt);
expect(contexts[1]?.tools?.map((tool) => tool.name)).toEqual(['Read', 'Glob', 'Grep']);
expect(backendInstances).toHaveLength(2);
expect(backendInstances[0] === backendInstances[1]).toBe(false);
expect(backendInstances[1]?.sendInputs[0]?.context).toEqual([]);
expect(backendInstances[1]?.sendInputs[0]?.runtimeContext).toBe(undefined);
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
expect(childRun?.parentRunId).toBe(parentRun.runId);
expect(childRun?.agentId).toBe(LOCAL_READ_AGENT_ID);
expect(childRun?.agentName).toBe(LOCAL_READ_AGENT_DEFINITION.name);
expect(childRun?.permissionMode).toBe('explore');
expect(parentRun).toMatchObject({
orchestrationMode: 'swarm',
agentSwarmAuthorization: 'session_mode',
});
expect(childRun).toMatchObject({
orchestrationMode: 'default',
orchestrationSource: 'session',
agentSwarmAuthorization: 'none',
});
const childMessages = (await store.readMessages(session.id)).filter(
(message) => 'turnId' in message && message.turnId === 'child-turn',
);
expect(childMessages).toEqual([]);
});
test('startChildTurn uses only WebSearch for the web research child definition', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const contexts: BackendFactoryContext[] = [];
backends.register('fake', (ctx) => {
contexts.push(ctx);
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [
testTool('Read'),
testTool('Glob'),
testTool('Grep'),
testTool('WebSearch'),
testTool('Bash'),
],
newId: nextId(),
now: nextNow(6_841),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'execute' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await drain(
manager.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: {
id: WEB_RESEARCH_AGENT_ID,
name: 'Injected Name',
systemPrompt: 'Injected child prompt.',
},
prompt: 'search the web',
}),
);
expect(contexts.map((ctx) => ctx.header.permissionMode)).toEqual(['execute', 'execute']);
expect(contexts[1]?.systemPrompt).toBe(WEB_RESEARCH_AGENT_DEFINITION.systemPrompt);
expect(contexts[1]?.tools?.map((tool) => tool.name)).toEqual(['WebSearch']);
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
expect(childRun?.agentId).toBe(WEB_RESEARCH_AGENT_ID);
expect(childRun?.agentName).toBe(WEB_RESEARCH_AGENT_DEFINITION.name);
expect(childRun?.permissionMode).toBe('execute');
});
test('spawnChildAgent returns artifacts recorded for the child turn', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
listArtifactsForTurn: async (_sessionId, turnId) =>
turnId === 'child-turn'
? [
{
id: 'artifact-1',
sessionId: 'session-1',
turnId,
createdAt: 200,
name: 'notes.md',
kind: 'file',
relativePath: 'artifacts/notes.md',
sizeBytes: 12,
status: 'live',
},
]
: [],
newId: nextId(),
now: nextNow(6_842),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const result = await manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Injected Name', systemPrompt: 'read only' },
prompt: 'inspect',
});
expect(result.agentId).toBe(LOCAL_READ_AGENT_ID);
expect(result.agentName).toBe(LOCAL_READ_AGENT_DEFINITION.name);
expect(result.artifactIds).toEqual(['artifact-1']);
});
test('resumeChildAgent replays durable child history into a fresh lineage run', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const childBackends: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
if (ctx.systemPrompt) childBackends.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_846),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const source = await manager.spawnChildAgent(session.id, {
turnId: 'child-source',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'ignored', systemPrompt: 'ignored' },
prompt: 'inspect runtime',
});
if (!source.runId) throw new Error('source child run was not recorded');
expect(await manager.prepareChildAgentResume(session.id, source.runId)).toEqual({
sourceRunId: source.runId,
execution: {
kind: 'legacy_child_run',
sessionId: session.id,
runId: source.runId,
},
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_DEFINITION.profile,
});
const resumed = await manager.resumeChildAgent(session.id, {
turnId: 'child-resumed',
parentRunId: parentRun.runId,
sourceRunId: source.runId,
prompt: 'continue with tests',
});
expect(resumed.resumedFromRunId).toBe(source.runId);
const resumedHeader = await runStore.readRun(session.id, resumed.runId!);
expect(resumedHeader).toMatchObject({
parentRunId: parentRun.runId,
resumedFromRunId: source.runId,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
});
const resumedInput = childBackends[1]?.sendInputs[0];
expect(resumedInput?.text).toBe('continue with tests');
expect(resumedInput?.runtimeContext?.some((event) => event.runId === source.runId)).toBe(true);
expect(
resumedInput?.runtimeContext?.some(
(event) =>
event.role === 'user' &&
event.content?.kind === 'text' &&
event.content.text === 'inspect runtime',
),
).toBe(true);
await expectRejects(
manager.prepareChildAgentResume(session.id, source.runId),
/already has a successor/,
);
expect(await manager.prepareChildAgentResume(session.id, resumed.runId!)).toMatchObject({
sourceRunId: resumed.runId,
});
});
test('prepareChildAgentResume rejects an indeterminate child tool boundary', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const child = makeRunHeader({
sessionId: session.id,
runId: 'unsafe-child',
turnId: 'unsafe-turn',
status: 'failed',
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
permissionMode: 'explore',
createdAt: 1,
updatedAt: 4,
completedAt: 4,
});
await seedRuntimeRun(runStore, child, [
runtimeEvent({
id: 'unsafe-user',
sessionId: session.id,
runId: child.runId,
turnId: child.turnId,
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'inspect' },
}),
runtimeEvent({
id: 'unsafe-call',
sessionId: session.id,
runId: child.runId,
turnId: child.turnId,
ts: 2,
role: 'model',
author: 'agent',
content: { kind: 'function_call', id: 'read-1', name: 'Read', args: { path: 'x' } },
refs: { toolCallId: 'read-1', stepId: 'step-1' },
}),
runtimeEvent({
id: 'unsafe-terminal',
sessionId: session.id,
runId: child.runId,
turnId: child.turnId,
ts: 4,
status: 'failed',
actions: { endInvocation: true },
}),
]);
await expectRejects(
manager.prepareChildAgentResume(session.id, child.runId),
/unmatched_tool_call/,
);
});
test('retryChildAgent replays a rate-limited child without appending its prompt again', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const sendInputs: BackendSendInput[] = [];
let childAttempt = 0;
backends.register('fake', (ctx) => ({
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
childAttempt += 1;
sendInputs.push(input);
if (childAttempt <= 2) {
yield {
type: 'error',
id: `${input.turnId}-error`,
turnId: input.turnId,
ts: 1,
recoverable: true,
reason: 'RateLimit',
message: 'provider 429',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'error',
};
return;
}
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 3,
messageId: `${input.turnId}-message`,
text: 'recovered',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
},
async stop(): Promise<void> {},
async respondToSandboxBoundary(): Promise<void> {},
async dispose(): Promise<void> {},
}));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
inspectContinuationSafety: async () => ({
workspaceIdentity: '/tmp/cwd',
backgroundOperationsSettled: true,
availableToolNames: [],
}),
newId: nextId(),
now: nextNow(6_843),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentRun = makeRunHeader({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
status: 'completed',
createdAt: 100,
updatedAt: 110,
completedAt: 110,
});
await seedRuntimeRun(runStore, parentRun, [
runtimeEvent({
id: 'parent-complete',
sessionId: session.id,
runId: parentRun.runId,
turnId: parentRun.turnId,
ts: 110,
status: 'completed',
actions: { endInvocation: true },
}),
]);
const first = await manager.spawnChildAgent(session.id, {
turnId: 'child-rate-limited',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Reader', systemPrompt: 'read only' },
prompt: 'inspect auth',
});
expect(first.status).toBe('failed');
expect(first.failureClass).toBe('RateLimit');
if (!first.runId) throw new Error('rate-limited child run id was not recorded');
const second = await manager.resumeChildAgent(session.id, {
parentRunId: parentRun.runId,
sourceRunId: first.runId,
prompt: 'retry with additional guidance',
});
expect(second.status).toBe('failed');
expect(second.failureClass).toBe('RateLimit');
if (!second.runId) throw new Error('second rate-limited child run id was not recorded');
await expectRejects(
manager.retryChildAgent(session.id, {
parentRunId: parentRun.runId,
sourceRunId: first.runId,
}),
/already has a successor/,
);
const retryAbort = new AbortController();
retryAbort.abort();
const runsBeforeAbortedRetry = await runStore.listSessionRuns(session.id);
let abortedRetryReady = 0;
await expectRejects(
manager.retryChildAgent(session.id, {
parentRunId: parentRun.runId,
sourceRunId: second.runId,
abortSignal: retryAbort.signal,
onReady: () => {
abortedRetryReady += 1;
},
}),
/cancelled before start/,
);
expect((await runStore.listSessionRuns(session.id)).length).toBe(runsBeforeAbortedRetry.length);
expect(sendInputs).toHaveLength(2);
expect(abortedRetryReady).toBe(0);
const retried = await manager.retryChildAgent(session.id, {
parentRunId: parentRun.runId,
sourceRunId: second.runId,
});
expect(retried.status).toBe('completed');
expect(retried.retriedFromRunId).toBe(second.runId);
expect(sendInputs.map((input) => input.text)).toEqual([
'inspect auth',
'retry with additional guidance',
'',
]);
expect(
sendInputs[2]?.runtimeContext?.some(
(event) =>
event.role === 'user' &&
event.content?.kind === 'text' &&
event.content.text === 'inspect auth',
),
).toBe(true);
const retryRun = await runStore.readRun(session.id, retried.runId!);
expect(retryRun.retriedFromRunId).toBe(second.runId);
expect(isSessionInlineRun(retryRun)).toBe(false);
expect(retryRun.continuationSource).toMatchObject({
protocol: 'continuation_source_v2',
sourceRunId: second.runId,
});
const retryStart = (await runStore.readImmutableRuntimeEvents(session.id, retried.runId!))[0]
?.actions?.continuationStart;
expect(retryStart?.protocol).toBe('continuation_start_v2');
expect(retryStart?.immediateSource.runId).toBe(second.runId);
expect(
(await store.readMessages(session.id)).filter(
(message) => 'turnId' in message && message.turnId === retried.turnId,
),
).toEqual([]);
});
test('parent and child runs can read their ledger while only parents may compact session history', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const contexts: BackendFactoryContext[] = [];
const seamReads: Array<{ turnId: string; eventIds: string[] } | undefined> = [];
class SeamProbeBackend extends TestBackend {
constructor(private readonly probeCtx: BackendFactoryContext) {
super(probeCtx);
}
override async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
seamReads.push(
this.probeCtx.loadTurnRuntimeEvents
? {
turnId: input.turnId,
eventIds: (await this.probeCtx.loadTurnRuntimeEvents(input.turnId)).map(
(event) => event.id,
),
}
: undefined,
);
yield* super.send(input);
}
}
backends.register('fake', (ctx) => {
contexts.push(ctx);
return new SeamProbeBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_850),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Reader', systemPrompt: 'read only' },
prompt: 'inspect',
});
// The parent (main-session) backend can read its turn's durable ledger —
// the seam resolves the active run and returns the persisted events.
expect(seamReads.length).toBe(2);
expect(seamReads[0]?.turnId).toBe('parent-turn');
expect((seamReads[0]?.eventIds.length ?? 0) > 0).toBe(true);
// Both runs need their authoritative ledger for Runtime-owned continuation.
// A separate capability keeps child-only ledgers from claiming coverage of
// the parent session projection during mid-turn history compaction.
expect(contexts.length).toBe(2);
expect(typeof contexts[0]?.loadTurnRuntimeEvents).toBe('function');
expect(typeof contexts[1]?.loadTurnRuntimeEvents).toBe('function');
expect((seamReads[1]?.eventIds.length ?? 0) > 0).toBe(true);
const capabilities = contexts as Array<
BackendFactoryContext & { allowMidTurnHistoryCompaction?: boolean }
>;
expect(capabilities[0]?.allowMidTurnHistoryCompaction).toBe(true);
expect(capabilities[1]?.allowMidTurnHistoryCompaction).toBe(false);
});
test('spawnChildAgent returns the terminal RuntimeEvent status when the child header commit fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'completed' });
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_843),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parentRun = makeRunHeader({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
status: 'completed',
createdAt: 100,
updatedAt: 110,
completedAt: 110,
});
await seedRuntimeRun(runStore, parentRun, [
runtimeEvent({
id: 'parent-complete',
sessionId: session.id,
runId: parentRun.runId,
turnId: parentRun.turnId,
ts: 110,
status: 'completed',
actions: { endInvocation: true },
}),
]);
const result = await manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'inspect',
});
expect(result.status).toBe('completed');
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
expect(childRun?.status).toBe('running');
const childTerminalEvents = (
await runStore.readRuntimeEvents(session.id, childRun!.runId)
).filter(isTerminalRuntimeEvent);
expect(childTerminalEvents).toHaveLength(1);
});
test('spawnChildAgent preserves step-limit failure without a run store', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'step_limit' }]),
);
const manager = new SessionManager({
store,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_844),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const result = await manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'inspect',
});
expect(result.status).toBe('failed');
expect(result.failureClass).toBe('tool_step_cap_reached');
});
test('spawnChildAgent summarizes high-volume child output without returning the full stream', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new HighVolumeDeltaBackend(ctx, 512));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_844),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const observed: SessionEvent[] = [];
const result = await manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'produce a large report',
onEvent: (event) => observed.push(event),
});
expect(result.status).toBe('completed');
expect(result.eventCount).toBe(513);
expect(result.summary.length <= 4_000).toBe(true);
expect(result.summary.startsWith('…')).toBe(true);
expect(result.summary.includes('chunk-000')).toBe(false);
expect(result.summary.includes('chunk-511')).toBe(true);
expect(observed).toHaveLength(513);
expect(observed[0]?.type).toBe('text_delta');
expect(observed.at(-1)?.type).toBe('complete');
});
test('stopSession cancels active child runs and disposes their backend', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const childGate = makeGate();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(
ctx,
ctx.header.permissionMode === 'explore' ? childGate : undefined,
);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = manager
.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'inspect slowly',
})
[Symbol.asyncIterator]();
await child.next();
await manager.stopSession(session.id, { source: 'stop_button' });
expect(backendInstances[0]?.stopCalls).toBe(0);
expect(backendInstances[1]?.stopCalls).toBe(1);
childGate.release();
await child.next();
await child.next();
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
if (!childRun) throw new Error('child run was not recorded');
expect(childRun.status).toBe('cancelled');
expect(store.disposeCount).toBe(1);
await manager.setPermissionMode(session.id, 'execute');
expect((await store.readHeader(session.id)).permissionMode).toBe('execute');
});
test('stopSession waits for a child start blocked before Run reservation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let childBackend: TestBackend | undefined;
backends.register('fake', (ctx) => {
childBackend = new TestBackend(ctx);
return childBackend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const child = manager
.startChildTurn(session.id, {
turnId: 'pending-child-turn',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'must not dispatch after stop',
})
[Symbol.asyncIterator]();
const firstEvent = child.next();
await readStarted.promise;
let stopSettled = false;
const stop = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseRead.release();
await stop;
expect((await firstEvent).done).toBe(true);
expect(childBackend?.sendInputs).toEqual([]);
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'pending-child-turn',
);
expect(childRun?.status).toBe('cancelled');
});
test('stopSession owns an active Run and a parent turn admitted before reservation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const activeGate = makeGate();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx, activeGate);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const active = manager
.sendMessage(session.id, { turnId: 'active-parent-turn', text: 'hold active' })
[Symbol.asyncIterator]();
expect((await active.next()).value?.type).toBe('text_delta');
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const pending = manager
.sendMessage(session.id, {
turnId: 'pending-parent-turn',
text: 'must become stopped, not failed',
})
[Symbol.asyncIterator]();
const pendingEvent = pending.next();
await readStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseRead.release();
activeGate.release();
await stopping;
expect((await pendingEvent).done).toBe(true);
while (!(await active.next()).done) {}
expect(backend?.sendInputs.map((input) => input.turnId)).toEqual(['active-parent-turn']);
const runs = await runStore.listSessionRuns(session.id);
expect(runs.find((run) => run.turnId === 'active-parent-turn')?.status).toBe('cancelled');
expect(runs.find((run) => run.turnId === 'pending-parent-turn')?.status).toBe('cancelled');
expect((await store.readHeader(session.id)).status).toBe('aborted');
});
test('public child spawn is stop-visible while async onReady is pending', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const readyStarted = makeGate();
const releaseReady = makeGate();
const spawned = manager.spawnChildAgent(session.id, {
turnId: 'child-on-ready-stop',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'must not reach the provider',
onReady: async () => {
readyStarted.release();
await releaseReady.promise;
},
});
await readyStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseReady.release();
const [result] = await Promise.all([spawned, stopping]);
expect(result.status).toBe('cancelled');
expect(backend?.sendInputs).toEqual([]);
const run = (await runStore.listSessionRuns(session.id)).find(
(candidate) => candidate.turnId === 'child-on-ready-stop',
);
expect(run?.status).toBe('cancelled');
expect((await store.readHeader(session.id)).status === 'blocked').toBe(false);
});
test('a factory AbortError is not normalized as execution cancellation', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
const releaseBuild = makeGate();
backends.register('fake', async () => {
buildStarted.release();
await releaseBuild.promise;
const error = new Error('backend activation timed out');
error.name = 'AbortError';
throw error;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const turn = manager
.sendMessage(session.id, {
turnId: 'stop-owned-build-reject',
text: 'must remain a cancelled Run',
})
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await buildStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseBuild.release();
await expectRejects(firstEvent, /backend activation timed out/);
await stopping;
const run = await runStore.readRun(
session.id,
(await runStore.listSessionRuns(session.id))[0]!.runId,
);
expect(run.status).toBe('cancelled');
expect(run.failureClass).toBe(undefined);
expect((await store.readHeader(session.id)).status).toBe('aborted');
});
test('stop signal cooperatively releases a blocked backend factory', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
let factorySignal: AbortSignal | undefined;
let dispatches = 0;
backends.register('fake', async (ctx) => {
factorySignal = ctx.abortSignal;
if (!factorySignal) throw new Error('backend factory did not receive an abort signal');
buildStarted.release();
await new Promise<void>((resolve) => {
factorySignal?.addEventListener('abort', () => resolve(), { once: true });
});
throw factorySignal.reason;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const turn = manager
.sendMessage(session.id, {
turnId: 'cooperative-factory-stop',
text: 'stop must release backend activation',
})
[Symbol.asyncIterator]();
const firstEvent = turn.next().then((result) => {
dispatches += result.done ? 0 : 1;
return result;
});
await buildStarted.promise;
await manager.stopSession(session.id, { source: 'stop_button' });
expect(factorySignal?.aborted).toBe(true);
expect((await firstEvent).done).toBe(true);
expect(dispatches).toBe(0);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('cancelled');
});
test('node timers AbortError is cancellation only when its cause is this execution stop', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
backends.register('fake', async (ctx) => {
if (!ctx.abortSignal) throw new Error('backend factory did not receive an abort signal');
buildStarted.release();
await timerDelay(60_000, undefined, { signal: ctx.abortSignal });
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const turn = manager
.sendMessage(session.id, {
turnId: 'native-abort-wrapper-stop',
text: 'native AbortError should retain exact cancellation cause',
})
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await buildStarted.promise;
await manager.stopSession(session.id, { source: 'stop_button' });
expect((await firstEvent).done).toBe(true);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('cancelled');
expect(run?.failureClass).toBe(undefined);
});
test('late ignored-signal backend is disposed once and never cached or dispatched', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
const releaseBuild = makeGate();
let builds = 0;
let firstDisposeCalls = 0;
let firstDispatches = 0;
backends.register('fake', async (ctx) => {
builds += 1;
if (builds === 1) {
buildStarted.release();
await releaseBuild.promise;
return {
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(): AsyncIterable<SessionEvent> {
firstDispatches += 1;
},
async stop(): Promise<void> {},
async respondToSandboxBoundary(): Promise<void> {},
async dispose(): Promise<void> {
firstDisposeCalls += 1;
},
};
}
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const turn = manager
.sendMessage(session.id, {
turnId: 'ignored-factory-stop',
text: 'late backend must be rejected',
})
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await buildStarted.promise;
const stopping = manager.stopSession(session.id, { source: 'stop_button' });
releaseBuild.release();
await stopping;
expect((await firstEvent).done).toBe(true);
expect(firstDisposeCalls).toBe(1);
expect(firstDispatches).toBe(0);
const [stoppedRun] = await runStore.listSessionRuns(session.id);
expect(stoppedRun?.status).toBe('cancelled');
await drain(
manager.sendMessage(session.id, {
turnId: 'post-ignored-factory-stop',
text: 'must build a fresh backend',
}),
);
expect(builds).toBe(2);
expect(firstDisposeCalls).toBe(1);
});
test('late cancelled backend disposal failure propagates as an AggregateError', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
const releaseBuild = makeGate();
let builds = 0;
let disposeCalls = 0;
backends.register('fake', async (ctx) => {
builds += 1;
buildStarted.release();
await releaseBuild.promise;
return {
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(): AsyncIterable<SessionEvent> {
throw new Error('cancelled backend must not dispatch');
},
async stop(): Promise<void> {},
async respondToSandboxBoundary(): Promise<void> {},
async dispose(): Promise<void> {
disposeCalls += 1;
throw new Error('late backend disposal failed');
},
};
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_845),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const turn = manager
.sendMessage(session.id, {
turnId: 'late-disposal-failure',
text: 'cleanup failure must remain observable',
})
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await buildStarted.promise;
const streamRejection = assert.rejects(firstEvent, (error: unknown) => {
assert.ok(error instanceof AggregateError);
assert.match(error.message, /Cancelled backend activation disposal failed/);
assert.ok(
error.errors.some(
(entry) => entry instanceof Error && entry.message === 'late backend disposal failed',
),
);
return true;
});
const stopping = manager.stopSession(session.id, { source: 'stop_button' });
releaseBuild.release();
await Promise.all([streamRejection, stopping]);
expect(disposeCalls).toBe(1);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('cancelled');
await expectRejects(
drain(
manager.sendMessage(session.id, {
turnId: 'late-disposal-next-turn',
text: 'must not create a second backend after quarantine failure',
}),
),
/permanently quarantined/,
);
expect(builds).toBe(1);
expect(disposeCalls).toBe(1);
});
test('stopSession keeps a rejected backend generation quarantined across retries', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const childGate = makeGate();
let childBackend: RetryStopBackend | undefined;
backends.register('fake', (ctx) => {
if (ctx.header.permissionMode !== 'explore') return new TestBackend(ctx);
childBackend = new RetryStopBackend(ctx, childGate);
return childBackend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_846),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = manager
.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'inspect slowly',
})
[Symbol.asyncIterator]();
await child.next();
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/first stop failed/,
);
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/first stop failed/,
);
expect(childBackend?.stopCalls).toBe(1);
childGate.release();
while (!(await child.next()).done) {}
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
expect(childRun?.status).toBe('cancelled');
});
test('concurrent stopSession calls share one stop attempt', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const stopStarted = makeGate();
const releaseStop = makeGate();
let backend: ConcurrentStopBackend | undefined;
backends.register('fake', (ctx) => {
backend = new ConcurrentStopBackend(ctx, stopStarted, releaseStop);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const turn = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await turn.next();
const firstStop = manager.stopSession(session.id, { source: 'stop_button' });
await stopStarted.promise;
let secondStopSettled = false;
const secondStop = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
secondStopSettled = true;
});
await Promise.resolve();
await Promise.resolve();
expect(secondStopSettled).toBe(false);
releaseStop.release();
await Promise.all([firstStop, secondStop]);
while (!(await turn.next()).done) {}
expect(backend?.stopCalls).toBe(1);
const messages = await store.readMessages(session.id);
expect(
messages.filter((message) => message.type === 'system_note' && message.kind === 'abort'),
).toHaveLength(1);
expect(
messages.filter(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-1' &&
message.status === 'aborted',
),
).toHaveLength(1);
});
test('stopSession waits for a registering turn and fences it before backend send', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-warm-cache', text: 'warm cache' }));
const readStarted = makeGate();
const releaseRead = makeGate();
store.nextReadHeaderGate = { started: readStarted, release: releaseRead };
const turn = manager
.sendMessage(session.id, { turnId: 'turn-registering', text: 'hello' })
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await readStarted.promise;
let stopSettled = false;
const stop = manager
.stopSession(session.id, {
source: 'benchmark_deadline',
mode: 'after_step',
})
.finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseRead.release();
await stop;
expect((await firstEvent).done).toBe(true);
expect(backend?.stopCalls).toBe(1);
expect(backend?.stopModes).toEqual(['after_step']);
expect(backend?.sendInputs.map((input) => input.turnId)).toEqual(['turn-warm-cache']);
const registeringRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-registering',
);
expect(registeringRun?.status).toBe('cancelled');
expect(
(await runStore.readRuntimeEvents(session.id, registeringRun!.runId)).filter(
isTerminalRuntimeEvent,
),
).toHaveLength(1);
});
test('stopSession fences dispatch after the public onRunStarted hook', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: TestBackend | undefined;
backends.register('fake', (ctx) => {
backend = new TestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const hookStarted = makeGate();
const releaseHook = makeGate();
const turn = manager
.sendMessage(
session.id,
{ turnId: 'turn-post-start-hook-stop', text: 'must not dispatch' },
{
onRunStarted: async () => {
hookStarted.release();
await releaseHook.promise;
},
},
)
[Symbol.asyncIterator]();
const firstEvent = turn.next();
await hookStarted.promise;
await manager.stopSession(session.id, { source: 'stop_button' });
releaseHook.release();
expect((await firstEvent).done).toBe(true);
expect(backend?.sendInputs).toEqual([]);
const stoppedRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'turn-post-start-hook-stop',
);
expect(stoppedRun?.status).toBe('cancelled');
});
test('concurrent cold turns share one backend generation without accepting an ownerless response', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const buildStarted = makeGate();
const releaseBuild = makeGate();
const releaseSend = makeGate();
let builds = 0;
let backend: PermissionBroadcastBackend | undefined;
backends.register('fake', async (ctx) => {
builds += 1;
buildStarted.release();
await releaseBuild.promise;
backend = new PermissionBroadcastBackend(ctx, releaseSend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const first = manager
.sendMessage(session.id, { turnId: 'cold-turn-1', text: 'first' })
[Symbol.asyncIterator]();
const second = manager
.sendMessage(session.id, { turnId: 'cold-turn-2', text: 'second' })
[Symbol.asyncIterator]();
const firstEvent = first.next();
await buildStarted.promise;
const secondEvent = second.next();
await Promise.resolve();
await Promise.resolve();
expect(builds).toBe(1);
releaseBuild.release();
expect((await firstEvent).value?.type).toBe('text_delta');
expect((await secondEvent).value?.type).toBe('text_delta');
await expectRejects(
manager.respondToSandboxBoundary(session.id, {
requestId: 'control-broadcast',
decision: 'deny',
}),
/No pending sandbox boundary request/,
);
expect(backend?.permissionResponses).toBe(0);
expect(builds).toBe(1);
releaseSend.release();
while (!(await first.next()).done) {}
while (!(await second.next()).done) {}
expect(backend?.sendInputs).toHaveLength(2);
});
test('routes a sandbox boundary response only to the generation that emitted its request', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const childGate = makeGate();
let parentBackend: SandboxBoundaryWaitBackend | undefined;
let childBackend: PermissionBroadcastBackend | undefined;
backends.register('fake', (ctx) => {
if (ctx.header.permissionMode === 'explore') {
childBackend = new PermissionBroadcastBackend(ctx, childGate);
return childBackend;
}
parentBackend = new SandboxBoundaryWaitBackend(ctx);
return parentBackend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_848),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const parent = manager
.sendMessage(session.id, { turnId: 'parent-boundary-turn', text: 'parent' })
[Symbol.asyncIterator]();
expect((await parent.next()).value?.type).toBe('sandbox_boundary_request');
const child = manager
.startChildTurn(session.id, {
turnId: 'child-running-turn',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'stay active',
})
[Symbol.asyncIterator]();
expect((await child.next()).value?.type).toBe('text_delta');
await manager.respondToSandboxBoundary(session.id, {
requestId: 'boundary-1',
decision: 'deny',
});
expect(parentBackend?.responses).toEqual([{ requestId: 'boundary-1', decision: 'deny' }]);
expect(childBackend?.permissionResponses).toBe(0);
while (!(await parent.next()).done) {}
childGate.release();
while (!(await child.next()).done) {}
});
test('stopSession does not reenter a failed backend in a multi-generation stop', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const parentGate = makeGate();
const childGate = makeGate();
let parentBackend: CountingStopBackend | undefined;
let childBackend: RetryStopBackend | undefined;
backends.register('fake', (ctx) => {
if (ctx.header.permissionMode === 'explore') {
childBackend = new RetryStopBackend(ctx, childGate);
return childBackend;
}
parentBackend = new CountingStopBackend(ctx, parentGate);
return parentBackend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_848),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const parent = manager
.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent' })
[Symbol.asyncIterator]();
await parent.next();
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
const child = manager
.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'child',
})
[Symbol.asyncIterator]();
await child.next();
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/first stop failed/,
);
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/first stop failed/,
);
expect(parentBackend?.stopCalls).toBe(1);
expect(childBackend?.stopCalls).toBe(1);
parentGate.release();
childGate.release();
while (!(await parent.next()).done) {}
while (!(await child.next()).done) {}
});
test('stopSession retries only unfinished projections', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const sendGate = makeGate();
let backend: CountingStopBackend | undefined;
backends.register('fake', (ctx) => {
backend = new CountingStopBackend(ctx, sendGate);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_849),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const turn = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await turn.next();
store.failAfterNextAppendMessage = (message) =>
message.type === 'system_note' && message.kind === 'abort';
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/append message failed/,
);
await manager.stopSession(session.id, { source: 'stop_button' });
expect(backend?.stopCalls).toBe(1);
const messages = await store.readMessages(session.id);
expect(
messages.filter(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-1' &&
message.status === 'aborted',
),
).toHaveLength(1);
expect(
messages.filter((message) => message.type === 'system_note' && message.kind === 'abort'),
).toHaveLength(1);
sendGate.release();
while (!(await turn.next()).done) {}
});
test('stopSession retains a failed projection after its Run exits without reentering backend stop', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const sendGate = makeGate();
let backend: CountingStopBackend | undefined;
backends.register('fake', (ctx) => {
backend = new CountingStopBackend(ctx, sendGate);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_850),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const turn = manager
.sendMessage(session.id, { turnId: 'turn-retained-stop', text: 'hello' })
[Symbol.asyncIterator]();
await turn.next();
store.failUpdateHeaderFor.add(session.id);
await expectRejects(
manager.stopSession(session.id, { source: 'stop_button' }),
/Cannot update header/,
);
sendGate.release();
while (!(await turn.next()).done) {}
store.failUpdateHeaderFor.delete(session.id);
await manager.stopSession(session.id, { source: 'stop_button' });
expect(backend?.stopCalls).toBe(1);
const messages = await store.readMessages(session.id);
expect(
messages.filter(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-retained-stop' &&
message.status === 'aborted',
),
).toHaveLength(1);
expect(
messages.filter((message) => message.type === 'system_note' && message.kind === 'abort'),
).toHaveLength(1);
});
test('spawnChildAgent fails closed instead of running a degraded catalog agent', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const backendInstances: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
backendInstances.push(backend);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read')],
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'execute' }));
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'parent context' }));
const [parentRun] = await runStore.listSessionRuns(session.id);
if (!parentRun) throw new Error('parent run was not recorded');
await expectRejects(
manager.spawnChildAgent(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect',
}),
/Agent "local-read" is unavailable: missing tools: Glob, Grep/,
);
expect(backendInstances).toHaveLength(1);
expect(
(await runStore.listSessionRuns(session.id)).some((run) => run.turnId === 'child-turn'),
).toBe(false);
await expectRejects(
manager.spawnChildAgent(session.id, {
turnId: 'web-child-turn',
parentRunId: parentRun.runId,
spec: {
id: WEB_RESEARCH_AGENT_ID,
name: WEB_RESEARCH_AGENT_DEFINITION.name,
systemPrompt: WEB_RESEARCH_AGENT_DEFINITION.systemPrompt,
},
prompt: 'search',
}),
/Agent "web-research" is unavailable: missing tools: WebSearch/,
);
expect(backendInstances).toHaveLength(1);
expect(
(await runStore.listSessionRuns(session.id)).some((run) => run.turnId === 'web-child-turn'),
).toBe(false);
});
test('agent projections list catalog definitions separately from child runs and read output artifacts by child turn', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
listArtifactsForTurn: async (_sessionId, turnId) =>
turnId === 'child-turn'
? [
{
id: 'artifact-1',
sessionId: 'session-1',
turnId,
createdAt: 200,
name: 'notes.md',
kind: 'file',
relativePath: 'artifacts/notes.md',
sizeBytes: 12,
status: 'live',
},
]
: [],
newId: nextId(),
now: nextNow(6_848),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'execute' }));
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
status: 'completed',
createdAt: 100,
updatedAt: 110,
completedAt: 110,
}),
[
runtimeEvent({
id: 'parent-user',
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'parent' },
}),
runtimeEvent({
id: 'parent-complete',
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
ts: 110,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
createdAt: 120,
updatedAt: 130,
completedAt: 130,
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Researcher',
permissionMode: 'explore',
}),
[
runtimeEvent({
id: 'child-user',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 121,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'inspect' },
}),
runtimeEvent({
id: 'child-answer',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 125,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'child answer' },
}),
runtimeEvent({
id: 'child-complete',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 130,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
status: 'completed',
createdAt: 140,
updatedAt: 150,
completedAt: 150,
parentRunId: 'parent-run',
parentTurnId: 'parent-turn',
continuationSource: {
sourceInvocationId: 'parent-invocation',
sourceRunId: 'parent-run',
sourceTurnId: 'parent-turn',
sourceRuntimeEventHighWater: 2,
},
}),
[
runtimeEvent({
id: 'continuation-answer',
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
ts: 145,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'continued answer' },
}),
runtimeEvent({
id: 'continuation-complete',
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
ts: 150,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const list = await manager.listChildAgents(session.id);
expect(list.definitions.map((agent) => agent.id)).toEqual([
LOCAL_READ_AGENT_ID,
WEB_RESEARCH_AGENT_ID,
IMPLEMENTATION_AGENT_ID,
]);
expect(list.definitions[0]?.availability).toEqual({ status: 'available' });
expect(list.definitions[0]?.contract.defaultWriteBack).toBe('summary');
expect(list.definitions[0]?.contract.workspace).toBe('same_workspace');
expect(list.definitions[1]?.availability).toEqual({
status: 'unavailable',
reason: 'missing_tools',
missingTools: ['WebSearch'],
});
expect(list.definitions[2]?.availability).toEqual({
status: 'unavailable',
reason: 'workspace_isolation_unavailable',
workspace: AGENT_WORKSPACE_WORKTREE,
requiredRuntime: 'worktree_child_executor',
});
expect(list.runs.map((agent) => agent.runId)).toEqual(['child-run']);
expect(list.executions.map((agent) => agent.execution)).toEqual([
{
kind: 'legacy_child_run',
sessionId: session.id,
runId: 'child-run',
},
]);
expect(list.runs[0]?.agentId).toBe(LOCAL_READ_AGENT_ID);
expect(list.runs[0]?.agentName).toBe('Researcher');
expect(list.runs[0]?.durationMs).toBe(10);
const output = await manager.readChildAgentOutput(session.id, { runId: 'child-run' });
expect(output.header.runId).toBe('child-run');
expect(output.runtimeEvents.map((event) => event.id)).toEqual([
'child-user',
'child-answer',
'child-complete',
]);
expect(output.artifacts.map((artifact) => artifact.id)).toEqual(['artifact-1']);
});
test('child agent projections use the terminal RuntimeEvent fact when the child header is stale', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_900),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'execute' }));
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
status: 'running',
createdAt: 100,
updatedAt: 120,
}),
[
runtimeEvent({
id: 'parent-complete',
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
ts: 120,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'running',
createdAt: 130,
updatedAt: 140,
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Researcher',
permissionMode: 'explore',
}),
[
runtimeEvent({
id: 'child-answer',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 135,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'child answer' },
}),
runtimeEvent({
id: 'child-complete',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 140,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const list = await manager.listChildAgents(session.id);
expect(list.runs[0]?.runId).toBe('child-run');
expect(list.runs[0]?.status).toBe('completed');
expect(list.runs[0]?.completedAt).toBe(140);
expect(list.runs[0]?.durationMs).toBe(10);
const output = await manager.readChildAgentOutput(session.id, { runId: 'child-run' });
expect(output.header.status).toBe('completed');
expect(output.header.completedAt).toBe(140);
});
test('agent output returns a bounded child inspection instead of full replay internals', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_849),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
const header = makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
createdAt: 120,
updatedAt: 200,
completedAt: 200,
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Researcher',
permissionMode: 'explore',
});
await runStore.createRun(header);
for (let index = 0; index < 25; index += 1) {
await runStore.appendEvent(
session.id,
'child-run',
makeRunEvent({
id: `op-${index}`,
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
type: 'model_stream_started',
ts: 120 + index,
}),
);
await runStore.appendRuntimeEvent(
session.id,
'child-run',
runtimeEvent({
id: `rt-${index}`,
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 120 + index,
role: 'model',
author: 'agent',
content: { kind: 'text', text: `line ${index}` },
}),
);
}
const output = await manager.readChildAgentOutput(session.id, {
runId: 'child-run',
maxEvents: 5,
view: 'all',
});
expect(output.header.runId).toBe('child-run');
expect(output.events.map((event) => event.id)).toEqual([
'op-20',
'op-21',
'op-22',
'op-23',
'op-24',
]);
expect(output.runtimeEvents.map((event) => event.id)).toEqual([
'rt-20',
'rt-21',
'rt-22',
'rt-23',
'rt-24',
]);
expect(output.truncated.events).toBe(true);
expect(output.truncated.runtimeEvents).toBe(true);
expect(output.budget.view).toBe('all');
expect(output.budget.projectedBytes <= output.budget.maxBytes).toBe(true);
expect('modelReplay' in output).toBe(false);
expect('projection' in output).toBe(false);
});
test('agent output bounds oversized runtime events by serialized bytes', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_849),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'completed',
createdAt: 120,
updatedAt: 200,
completedAt: 200,
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Researcher',
permissionMode: 'explore',
}),
);
await runStore.appendRuntimeEvent(
session.id,
'child-run',
runtimeEvent({
id: 'oversized',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 130,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'x'.repeat(64 * 1024) },
}),
);
await runStore.appendRuntimeEvent(
session.id,
'child-run',
runtimeEvent({
id: 'final',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 140,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'bounded final result' },
}),
);
const output = await manager.readChildAgentOutput(session.id, {
runId: 'child-run',
maxEvents: 20,
maxBytes: 1024,
});
expect(output.events).toEqual([]);
expect(output.runtimeEvents.map((event) => event.id)).toEqual(['final']);
expect(output.truncated.events).toBe(true);
expect(output.truncated.runtimeEvents).toBe(true);
expect(output.truncated.bytes).toBe(true);
expect(output.budget).toMatchObject({
view: 'runtime_events',
maxBytes: 1024,
});
expect(output.budget.projectedBytes <= output.budget.maxBytes).toBe(true);
});
test('rejects backend configuration updates while a turn is actively streaming', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const gate = makeGate();
backends.register('fake', (ctx) => new TestBackend(ctx, gate));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(7_000) });
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
await expectRejects(
manager.updateSession(session.id, {
backend: 'ai-sdk',
llmConnectionSlug: 'zai-coding-plan',
model: 'glm-4.7',
cwd: '/tmp/worktree-cwd',
}),
/Cannot change backend configuration while a turn is running/,
);
const header = await store.readHeader(session.id);
expect(header.backend).toBe('fake');
expect(header.llmConnectionSlug).toBe('fake');
gate.release();
await iterator.next();
await iterator.next();
});
test('backend build failure after user append writes a failed terminal run fact', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', () => {
throw new Error('backend init failed');
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(7_500),
});
const session = await manager.createSession(makeInput());
await expectRejects(
drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })),
/backend init failed/,
);
const header = await store.readHeader(session.id);
expect(header.status).toBe('blocked');
expect(header.blockedReason).toBe('unknown');
const messages = await store.readMessages(session.id);
expect(messages.some((message) => message.type === 'user' && message.turnId === 'turn-1')).toBe(
true,
);
const turn = (await store.listTurns(session.id)).find(
(candidate) => candidate.turnId === 'turn-1',
);
expect(turn?.status).toBe('failed');
const [run] = await runStore.listSessionRuns(session.id);
if (!run) throw new Error('AgentRunStore run was not created');
expect(run.status).toBe('failed');
expect(run.failureClass).toBe('missing_terminal_event');
const terminalEvents = (await runStore.readRuntimeEvents(session.id, run.runId)).filter(
isTerminalRuntimeEvent,
);
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('failed');
expect(terminalEvents[0]?.actions?.stateDelta?.failureClass).toBe('missing_terminal_event');
expect((await manager.getMessages(session.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('marks a sandbox boundary request waiting and blocks boundary mode changes', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: SandboxBoundaryWaitBackend | undefined;
backends.register('fake', (ctx) => {
backend = new SandboxBoundaryWaitBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_000),
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
expect((await iterator.next()).value?.type).toBe('sandbox_boundary_request');
const [activeBoundaryRequest] = await manager.listActiveInteractions(session.id);
expect(activeBoundaryRequest).toMatchObject({
type: 'sandbox_boundary_request',
requestId: 'boundary-1',
toolUseId: 'tool-1',
turnId: 'turn-1',
});
expect((await store.readHeader(session.id)).status).toBe('waiting_for_user');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('waiting_for_user');
await expectRejects(manager.setPermissionMode(session.id, 'bypass'), /当前对话正在运行/);
expect((await store.readHeader(session.id)).permissionMode).toBe('ask');
await manager.respondToSandboxBoundary(session.id, {
requestId: 'boundary-1',
decision: 'deny',
});
expect(backend?.responses).toEqual([{ requestId: 'boundary-1', decision: 'deny' }]);
expect((await iterator.next()).value?.type).toBe('sandbox_boundary_decision_ack');
expect(await manager.listActiveInteractions(session.id)).toEqual([]);
while (!(await iterator.next()).done) {}
expect((await store.readHeader(session.id)).status).toBe('active');
});
test('lists an unanswered user question until its answer ack lands', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_000),
});
const session = await manager.createSession(makeInput({ backend: 'fake' }));
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: FAKE_ASK_USER_QUESTION_PROMPT })
[Symbol.asyncIterator]();
let request: SessionEvent | undefined;
while (request?.type !== 'user_question_request') {
const next = await iterator.next();
if (next.done) break;
request = next.value;
}
expect(request?.type).toBe('user_question_request');
// The surface that raised this request may never have been mounted, so the
// request has to be readable back from the kernel (#2072).
const active = await manager.listActiveInteractions(session.id);
expect(active).toEqual([request]);
const requestId = (request as Extract<SessionEvent, { type: 'user_question_request' }>)
.requestId;
// One registry now holds both request kinds, so answering a question as if
// it were a boundary must settle nothing and leave the entry intact.
await expectRejects(
manager.respondToSandboxBoundary(session.id, { requestId, decision: 'deny' }),
/No pending sandbox boundary request/,
);
expect(await manager.listActiveInteractions(session.id)).toEqual([request]);
await manager.respondToUserQuestion(session.id, {
requestId,
answers: ['邀请制', '本周', '是'],
});
let ack: SessionEvent | undefined;
while (ack?.type !== 'user_question_answer_ack') {
const next = await iterator.next();
if (next.done) break;
ack = next.value;
}
expect(ack?.type).toBe('user_question_answer_ack');
expect(await manager.listActiveInteractions(session.id)).toEqual([]);
while (!(await iterator.next()).done) {}
});
test('releases a question abandoned by a stopped turn', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_000),
});
const session = await manager.createSession(makeInput({ backend: 'fake' }));
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: FAKE_ASK_USER_QUESTION_PROMPT })
[Symbol.asyncIterator]();
let request: SessionEvent | undefined;
while (request?.type !== 'user_question_request') {
const next = await iterator.next();
if (next.done) break;
request = next.value;
}
expect(request?.type).toBe('user_question_request');
// A stop settles no answer, so this question never gets its ack. Only the
// turn-end release keeps it from being read back as a prompt for a dead
// run — one nothing could answer, since the backend generation is gone.
await manager.stopSession(session.id, { source: 'stop_button' });
while (!(await iterator.next()).done) {}
expect(await manager.listActiveInteractions(session.id)).toEqual([]);
});
test('reads a completed session back after a sandbox boundary allow', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new SandboxBoundaryWaitBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_500),
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
expect((await iterator.next()).value?.type).toBe('sandbox_boundary_request');
await manager.respondToSandboxBoundary(session.id, {
requestId: 'boundary-1',
decision: 'allow',
});
while (!(await iterator.next()).done) {}
const messages = await manager.getMessages(session.id);
expect(messages.some((message) => message.type === 'user')).toBe(true);
const turns = await manager.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'turn-1')?.status).toBe('completed');
});
// The next unclaimed control fact must not repeat #1607. A complete ledger
// with one event the projection was never taught still reads back — messages,
// turns and every turn-scoped action — and the unclaimed event is reported as
// a diagnostic rather than costing the session that contains it.
test('reads a completed session back with an unclaimed control fact in its ledger', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new UnmappedSessionEventBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_600),
});
const session = await manager.createSession(makeInput());
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'hello',
})) {
// drain
}
const messages = await manager.getMessages(session.id);
expect(messages.filter((message) => message.type === 'user').length).toBe(1);
expect(
messages.some((message) => message.type === 'assistant' && message.text === 'hi back'),
).toBe(true);
const turns = await manager.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'turn-1')?.status).toBe('completed');
const view = await new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
}).getSessionView(session.id);
expect(
view.diagnostics.filter((diagnostic) => diagnostic.code === 'unclaimed_control_fact').length,
).toBe(1);
});
// The counterpart of the soft case, at the caller that enforces the policy:
// an unclaimed event that carries content may have cost a reader a row, so
// getSessionView must still refuse the view rather than serve a lossy one.
test('refuses a session view when an unclaimed event carries content', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new UnmappedSessionEventBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_700),
});
const session = await manager.createSession(makeInput());
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'hello',
})) {
// drain
}
const [run] = await runStore.listSessionRuns(session.id);
await runStore.appendRuntimeEvent(
session.id,
run!.runId,
runtimeEvent({
id: 'unclaimed-content',
sessionId: session.id,
runId: run!.runId,
turnId: 'turn-1',
ts: 4,
role: 'model',
author: 'agent',
content: { kind: 'not_yet_projected', text: 'a reader would have seen this' } as never,
}),
);
await assert.rejects(
new RuntimeReadModel({ runStore, runtimeEventStore: runStore }).getSessionView(session.id),
(error: unknown) =>
error instanceof RuntimeReadModelError &&
error.diagnostics.some((diagnostic) => diagnostic.code === 'unsupported_event'),
);
});
test('complete(stopReason=error) without a prior error event classifies as runtime_error not unknown', async () => {
// Reproduces the DeepSeek-reasoner smoke failure: the backend ended with
// stopReason='error' but never emitted a preceding error event, so the
// run ledger's failureClass was 'unknown'. It should be 'runtime_error'
// so benchmark scoring can distinguish runtime failures from max_tokens.
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'error' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(10_000),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('runtime_error');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.failureClass).toBe('runtime_error');
});
test('marks an explicit step limit incomplete without blocking the session', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'step_limit' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(10_000),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect((await store.readHeader(session.id)).status).toBe('active');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('tool_step_cap_reached');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('tool_step_cap_reached');
const terminal = (await runStore.readRuntimeEvents(session.id, run!.runId)).find(
(event) => event.actions?.endInvocation,
);
expect(terminal?.status).toBe('failed');
expect(terminal?.actions?.stateDelta).toMatchObject({
stopReason: 'step_limit',
failureClass: 'tool_step_cap_reached',
});
});
test('does not let a late complete event overwrite a prior turn error', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new EventBackend(ctx, [
{ type: 'error', recoverable: false, reason: 'tool_failed', message: 'Tool failed' },
{ type: 'complete', stopReason: 'end_turn' },
]),
);
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(10_500) });
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const states = (await store.readMessages(session.id)).filter(
(message) => message.type === 'turn_state' && message.turnId === 'turn-1',
);
expect(states.map((state) => (state.type === 'turn_state' ? state.status : ''))).toEqual([
'running',
'failed',
]);
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('tool_failed');
});
test('stopSession records renderer abort source for diagnostics', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const gate = makeGate();
backends.register('fake', (ctx) => new TestBackend(ctx, gate));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_500) });
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
await manager.stopSession(session.id, { source: 'stop_button' });
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('aborted');
expect(turn?.abortSource).toBe('renderer.stop_button');
const abortNote = (await store.readMessages(session.id)).find(
(message) => message.type === 'system_note' && message.kind === 'abort',
);
expect(abortNote?.type).toBe('system_note');
if (abortNote?.type !== 'system_note') throw new Error('abort note missing');
expect(abortNote.data).toEqual({ source: 'renderer.stop_button' });
});
test('stopSession persists abortSource on a terminal RuntimeEvent emitted during backend stop', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: StopControlledAbortBackend | undefined;
backends.register('fake', (ctx) => {
backend = new StopControlledAbortBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_710),
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
const pendingAbort = iterator.next();
const stopPromise = manager.stopSession(session.id, { source: 'stop_button' });
const abort = await pendingAbort;
expect(abort.value?.type).toBe('abort');
backend?.allowStopReturn();
await stopPromise;
while (!(await iterator.next()).done) {}
const [run] = await runStore.listSessionRuns(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run!.runId);
const terminalEvents = runtimeEvents.filter((event) => event.status === 'aborted');
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.actions?.stateDelta?.abortSource).toBe('renderer.stop_button');
const messages = await manager.getMessages(session.id);
expect(messages.some((message) => message.type === 'user' && message.turnId === 'turn-1')).toBe(
true,
);
expect(messages.find((message) => message.type === 'turn_state')).toEqual({
type: 'turn_state',
id: terminalEvents[0]?.id,
turnId: 'turn-1',
ts: 2,
status: 'aborted',
abortedAt: 2,
abortSource: 'renderer.stop_button',
partialOutputRetained: false,
});
});
test('sendMessage does not emit or persist backend events after the first terminal event', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new EventBackend(ctx, [
{ type: 'text_delta', messageId: 'm1', text: 'before' },
{ type: 'abort', reason: 'user_stop' },
{ type: 'text_delta', messageId: 'm1', text: 'after-terminal' },
{ type: 'complete', stopReason: 'user_stop' },
]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_715),
});
const session = await manager.createSession(makeInput());
const emitted = await collectSessionEvents(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }),
);
const [run] = await runStore.listSessionRuns(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run!.runId);
const turnStates = (await store.readMessages(session.id)).filter(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-1' &&
message.status !== 'running',
);
expect(emitted.map((event) => event.type)).toEqual(['text_delta', 'abort']);
expect(
runtimeEvents
.filter((event) => event.role === 'model' && event.content?.kind === 'text')
.map((event) => (event.content?.kind === 'text' ? event.content.text : '')),
).toEqual(['before']);
const abortedEvents = runtimeEvents.filter((event) => event.status === 'aborted');
expect(abortedEvents).toHaveLength(1);
expect(abortedEvents[0]?.actions?.stateDelta?.abortSource).toBe('user_stop');
expect(run?.status).toBe('cancelled');
expect(run?.abortSource).toBe('user_stop');
expect(turnStates).toHaveLength(1);
expect(turnStates[0]?.type === 'turn_state' ? turnStates[0].status : undefined).toBe('aborted');
expect(turnStates[0]?.type === 'turn_state' ? turnStates[0].abortSource : undefined).toBe(
'user_stop',
);
});
test('sendMessage ignores backend errors thrown after a completed terminal event is recorded', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ThrowAfterTerminalBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_716),
});
const session = await manager.createSession(makeInput());
const emitted = await collectSessionEvents(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }),
);
const [run] = await runStore.listSessionRuns(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run!.runId);
const turnStates = (await store.readMessages(session.id)).filter(
(message) =>
message.type === 'turn_state' &&
message.turnId === 'turn-1' &&
message.status !== 'running',
);
expect(emitted.map((event) => event.type)).toEqual(['text_delta', 'complete']);
expect(run?.status).toBe('completed');
expect(
runtimeEvents
.filter((event) => event.role === 'model' && event.content?.kind === 'text')
.map((event) => (event.content?.kind === 'text' ? event.content.text : '')),
).toEqual(['before']);
expect(runtimeEvents.filter((event) => event.status === 'completed')).toHaveLength(1);
expect(runtimeEvents.filter((event) => event.status === 'failed')).toHaveLength(0);
expect(turnStates).toHaveLength(1);
expect(turnStates[0]?.type === 'turn_state' ? turnStates[0].status : undefined).toBe(
'completed',
);
});
test('stopSession keeps aborted state even if the backend emits a late error', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const gate = makeGate();
backends.register('fake', (ctx) => new LateErrorBackend(ctx, gate));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_720),
});
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
await manager.stopSession(session.id, { source: 'stop_button' });
gate.release();
await iterator.next();
await iterator.next();
await iterator.next();
expect((await store.readHeader(session.id)).status).toBe('aborted');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('aborted');
expect(turn?.abortSource).toBe('renderer.stop_button');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('cancelled');
expect(run?.failureClass).toBeUndefined();
const events = (await runStore.readEvents(session.id, run!.runId)).map((event) => event.type);
expect(events).toContain('run_cancelled');
expect(events.includes('run_failed')).toBe(false);
});
test('durable run ledger records lifecycle trace events and redacts obvious secrets', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TraceBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_750),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.backendKind).toBe('fake');
expect(run?.llmConnectionSlug).toBe('fake');
expect(run?.modelId).toBe('fake-model');
expect(run?.permissionMode).toBe('ask');
expect(run?.status).toBe('completed');
const events = await runStore.readEvents(session.id, run!.runId);
expect(events.map((event) => event.type)).toContain('model_stream_started');
expect(events.map((event) => event.type)).toContain('model_stream_completed');
expect(events.map((event) => event.type)).toContain('run_completed');
expect(JSON.stringify(events).includes('sk-live-secret-token-value')).toBe(false);
});
test('required capture and later attempt still write after an attempt append fails', async () => {
const store = new MemorySessionStore();
const attemptFailureRecorded = makeGate();
const captureOutcomes: string[] = [];
let failAttemptOnce = true;
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type === 'provider_request_attempt_recorded' && failAttemptOnce) {
failAttemptOnce = false;
throw new Error('diagnostic attempt append failed');
}
if (event.type === 'trace_write_failed') attemptFailureRecorded.release();
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new ProviderCaptureAfterAttemptFailureBackend(
ctx,
attemptFailureRecorded.promise,
captureOutcomes,
),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_762),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect(captureOutcomes).toEqual(['fulfilled']);
const [run] = await runStore.listSessionRuns(session.id);
const events = await runStore.readEvents(session.id, run!.runId);
expect(events.some((event) => event.type === 'provider_request_captured')).toBe(true);
expect(events.some((event) => event.id === 'attempt-2')).toBe(true);
});
test('carries a provider attempt failure latch into the terminal run header', async () => {
const store = new MemorySessionStore();
let failAttemptAppend = true;
let failFailureLatch = true;
let failFailureSentinel = true;
const runStore = new MemoryAgentRunStore({
beforeAgentRunUpdate: async (_sessionId, _runId, patch) => {
if (patch.traceWriteError && failFailureLatch) {
failFailureLatch = false;
throw new Error('trace failure latch update failed');
}
},
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type === 'provider_request_attempt_recorded' && failAttemptAppend) {
failAttemptAppend = false;
throw new Error('provider attempt append failed');
}
if (event.type === 'trace_write_failed' && failFailureSentinel) {
failFailureSentinel = false;
throw new Error('trace failure sentinel append failed');
}
},
});
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ProviderRequestTraceBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_763),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('completed');
expect(run?.traceWriteError).toMatch(
/append provider request attempt: provider attempt append failed/,
);
const events = await runStore.readEvents(session.id, run!.runId);
expect(events.some((event) => event.type === 'trace_write_failed')).toBe(false);
});
test('finalizes the run when a required provider capture append fails', async () => {
const store = new MemorySessionStore();
let providerDispatches = 0;
let failCaptureOnce = true;
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type === 'provider_request_captured' && failCaptureOnce) {
failCaptureOnce = false;
throw new Error('required capture append failed');
}
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new ProviderCaptureGateBackend(ctx, () => (providerDispatches += 1)),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_764),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })).catch(
() => {},
);
expect(providerDispatches).toBe(0);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.completedAt).toBeDefined();
});
test('schedules legacy artifact cleanup only after the V2 checkpoint is durable', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const cleanupCalled = makeGate();
let observed:
| {
checkpointId: string;
runtimeEventCount: number;
checkpointWasDurable: boolean;
}
| undefined;
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new HistoryCompactCheckpointBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
cleanupHistoryCompactArtifacts: async (input) => {
const runs = await runStore.listSessionRuns(input.sessionId);
const operationalEvents = await Promise.all(
runs.map((run) => runStore.readEvents(input.sessionId, run.runId)),
);
observed = {
checkpointId: input.checkpoint.checkpointId,
runtimeEventCount: input.runtimeEvents.length,
checkpointWasDurable: operationalEvents
.flat()
.some((event) => event.type === 'history_compact_checkpoint_recorded'),
};
cleanupCalled.release();
},
newId: nextId(),
now: nextNow(12_792),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
await cleanupCalled.promise;
expect(observed?.checkpointId).toBe('hcheckpoint-test');
expect((observed?.runtimeEventCount ?? 0) > 0).toBe(true);
expect(observed?.checkpointWasDurable).toBe(true);
});
test('history compact cleanup includes continuation events without including child agent events', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const cleanupCalled = makeGate();
let observedEventIds: string[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new HistoryCompactCheckpointBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
cleanupHistoryCompactArtifacts: async (input) => {
observedEventIds = input.runtimeEvents.map((event) => event.id);
cleanupCalled.release();
},
newId: nextId(),
now: nextNow(12_794),
});
const session = await manager.createSession(makeInput());
const sourceRunId = 'cleanup-source-run';
const sourceTurnId = 'cleanup-source-turn';
const sourceInvocationId = 'cleanup-source-invocation';
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
status: 'completed',
createdAt: 100,
updatedAt: 101,
completedAt: 101,
}),
[
runtimeEvent({
id: 'cleanup-source-complete',
invocationId: sourceInvocationId,
sessionId: session.id,
runId: sourceRunId,
turnId: sourceTurnId,
ts: 101,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'cleanup-continuation-run',
turnId: 'cleanup-continuation-turn',
status: 'completed',
parentRunId: sourceRunId,
parentTurnId: sourceTurnId,
continuationSource: {
sourceInvocationId,
sourceRunId,
sourceTurnId,
sourceRuntimeEventHighWater: 1,
},
createdAt: 102,
updatedAt: 104,
completedAt: 104,
}),
[
runtimeEvent({
id: 'cleanup-continuation-text',
invocationId: 'cleanup-continuation-invocation',
sessionId: session.id,
runId: 'cleanup-continuation-run',
turnId: 'cleanup-continuation-turn',
ts: 103,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'continued output' },
}),
runtimeEvent({
id: 'cleanup-continuation-complete',
invocationId: 'cleanup-continuation-invocation',
sessionId: session.id,
runId: 'cleanup-continuation-run',
turnId: 'cleanup-continuation-turn',
ts: 104,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'cleanup-child-run',
turnId: 'cleanup-child-turn',
status: 'completed',
parentRunId: sourceRunId,
parentTurnId: sourceTurnId,
createdAt: 105,
updatedAt: 107,
completedAt: 107,
}),
[
runtimeEvent({
id: 'cleanup-child-text',
invocationId: 'cleanup-child-invocation',
sessionId: session.id,
runId: 'cleanup-child-run',
turnId: 'cleanup-child-turn',
ts: 106,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'private child output' },
}),
runtimeEvent({
id: 'cleanup-child-complete',
invocationId: 'cleanup-child-invocation',
sessionId: session.id,
runId: 'cleanup-child-run',
turnId: 'cleanup-child-turn',
ts: 107,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await drain(
manager.sendMessage(session.id, { turnId: 'cleanup-current-turn', text: 'continue' }),
);
await cleanupCalled.promise;
expect(observedEventIds).toContain('cleanup-continuation-text');
expect(observedEventIds).not.toContain('cleanup-child-text');
});
test('shares the latest history compact checkpoint across disposable child backends', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const observedCheckpointIds: Array<string | undefined> = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new HistoryCompactCheckpointCacheProbeBackend(ctx, observedCheckpointIds),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_795),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'parent-turn', text: 'hello' }));
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-turn',
);
if (!parentRun) throw new Error('parent run was not recorded');
runStore.readEventsCalls = 0;
for (const turnId of ['child-turn-1', 'child-turn-2']) {
await drain(
manager.startChildTurn(session.id, {
turnId,
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
}
expect(observedCheckpointIds).toEqual([undefined, 'hcheckpoint-shared', 'hcheckpoint-shared']);
expect(runStore.readEventsCalls).toBe(0);
});
test('does not let a stale child checkpoint move the session cache backward', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const observedCoverage: Array<number | undefined> = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new HistoryCompactCheckpointMonotonicProbeBackend(ctx, observedCoverage),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_796),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'parent-furthest', text: 'hello' }));
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-furthest',
);
if (!parentRun) throw new Error('parent run was not recorded');
for (const turnId of ['child-stale', 'child-observe']) {
await drain(
manager.startChildTurn(session.id, {
turnId,
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
}
expect(observedCoverage).toEqual([undefined, 2, 2]);
const recordedCoverage: number[] = [];
for (const run of await runStore.listSessionRuns(session.id)) {
for (const event of await runStore.readEvents(session.id, run.runId)) {
if (event.type === 'history_compact_checkpoint_recorded') {
recordedCoverage.push(
(event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount,
);
}
}
}
expect(recordedCoverage).toEqual([2]);
});
test('persists an explicit same-coverage successor checkpoint', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const writeOutcomes: string[] = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new SameCoverageCheckpointReplacementProbeBackend(ctx, writeOutcomes),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_796),
});
const session = await manager.createSession(makeInput());
await drain(
manager.sendMessage(session.id, { turnId: 'same-coverage-initial', text: 'hello' }),
);
await drain(
manager.sendMessage(session.id, { turnId: 'same-coverage-replacement', text: 'hello again' }),
);
expect(writeOutcomes).toEqual([
'same-coverage-initial:fulfilled',
'same-coverage-replacement:fulfilled',
]);
const checkpoints: HistoryCompactCheckpoint[] = [];
for (const run of await runStore.listSessionRuns(session.id)) {
for (const event of await runStore.readEvents(session.id, run.runId)) {
if (event.type === 'history_compact_checkpoint_recorded') {
checkpoints.push(event.data?.checkpoint as HistoryCompactCheckpoint);
}
}
}
expect(checkpoints).toHaveLength(2);
expect(checkpoints[1]?.previousCheckpointId).toBe(checkpoints[0]?.checkpointId);
expect(checkpoints[1]?.coverage).toEqual(checkpoints[0]?.coverage);
});
test('serializes accepted checkpoint persistence across parent and child runs', async () => {
const store = new MemorySessionStore();
const parentWriteGate = makeGate();
const parentWriteStarted = makeGate();
const childRecorderCalled = makeGate();
const physicalCoverage: number[] = [];
const recorderReturnedPromises: boolean[] = [];
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type !== 'history_compact_checkpoint_recorded') return;
const coverage = (event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount;
if (coverage === 1) {
parentWriteStarted.release();
await parentWriteGate.promise;
}
physicalCoverage.push(coverage);
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new SerializedCheckpointProbeBackend(ctx, childRecorderCalled, recorderReturnedPromises),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_797),
});
const session = await manager.createSession(makeInput());
const parentTurn = drain(
manager.sendMessage(session.id, { turnId: 'parent-delayed', text: 'hello' }),
);
await parentWriteStarted.promise;
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-delayed',
);
if (!parentRun) throw new Error('parent run was not recorded');
const childTurn = drain(
manager.startChildTurn(session.id, {
turnId: 'child-furthest',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
await childRecorderCalled.promise;
try {
expect(recorderReturnedPromises).toEqual([true]);
} finally {
parentWriteGate.release();
}
await Promise.all([parentTurn, childTurn]);
expect(physicalCoverage).toEqual([1, 2]);
});
test('keeps failed checkpoint writes out of the cache and continues the session write queue', async () => {
const store = new MemorySessionStore();
const parentWriteGate = makeGate();
const parentWriteStarted = makeGate();
const childRecorderCalled = makeGate();
const physicalCoverage: number[] = [];
const observedCoverage: Array<number | undefined> = [];
const writeOutcomes: string[] = [];
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type !== 'history_compact_checkpoint_recorded') return;
const coverage = (event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount;
physicalCoverage.push(coverage);
if (coverage === 1) {
parentWriteStarted.release();
await parentWriteGate.promise;
throw new Error('checkpoint append failed');
}
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new RecoveringCheckpointWriteProbeBackend(
ctx,
childRecorderCalled,
observedCoverage,
writeOutcomes,
),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_798),
});
const session = await manager.createSession(makeInput());
const parentTurn = drain(
manager.sendMessage(session.id, { turnId: 'parent-failing', text: 'hello' }),
);
await parentWriteStarted.promise;
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-failing',
);
if (!parentRun) throw new Error('parent run was not recorded');
const childTurn = drain(
manager.startChildTurn(session.id, {
turnId: 'child-succeeding',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
await childRecorderCalled.promise;
parentWriteGate.release();
await Promise.all([parentTurn, childTurn]);
await drain(
manager.startChildTurn(session.id, {
turnId: 'child-observe',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
expect(observedCoverage).toEqual([undefined, undefined, 2]);
expect(writeOutcomes).toEqual(['parent-failing:rejected', 'child-succeeding:fulfilled']);
expect(physicalCoverage).toEqual([1, 2]);
});
test('rejects checkpoint recording after the current AgentRun store becomes unavailable', async () => {
const store = new MemorySessionStore();
const runStoreUnavailable = makeGate();
const writeOutcomes: string[] = [];
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type === 'run_started') throw new Error('run ledger append failed');
if (event.type === 'trace_write_failed') runStoreUnavailable.release();
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new CheckpointRecorderContractProbeBackend(
ctx,
async () => runStoreUnavailable.promise,
writeOutcomes,
),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_800),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'store-unavailable', text: 'hello' }));
expect(writeOutcomes).toEqual(['store-unavailable:rejected']);
});
test('rejects a stale checkpoint candidate while a further checkpoint is in flight', async () => {
const store = new MemorySessionStore();
const furthestWriteGate = makeGate();
const furthestWriteStarted = makeGate();
const staleRecorderCalled = makeGate();
const writeOutcomes: string[] = [];
const physicalCoverage: number[] = [];
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventAppend: async (_sessionId, _runId, event) => {
if (event.type !== 'history_compact_checkpoint_recorded') return;
const coverage = (event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount;
physicalCoverage.push(coverage);
if (coverage === 2) {
furthestWriteStarted.release();
await furthestWriteGate.promise;
}
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new CheckpointRecorderContractProbeBackend(
ctx,
async (turnId) => {
if (turnId === 'child-stale-in-flight') staleRecorderCalled.release();
},
writeOutcomes,
),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_801),
});
const session = await manager.createSession(makeInput());
const parentTurn = drain(
manager.sendMessage(session.id, { turnId: 'parent-furthest-in-flight', text: 'hello' }),
);
await furthestWriteStarted.promise;
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-furthest-in-flight',
);
if (!parentRun) throw new Error('parent run was not recorded');
const childTurn = drain(
manager.startChildTurn(session.id, {
turnId: 'child-stale-in-flight',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
await staleRecorderCalled.promise;
furthestWriteGate.release();
await Promise.all([parentTurn, childTurn]);
expect(writeOutcomes).toEqual([
'parent-furthest-in-flight:fulfilled',
'child-stale-in-flight:rejected',
]);
expect(physicalCoverage).toEqual([2]);
});
test('waits for the initial durable checkpoint load before accepting a write', async () => {
const store = new MemorySessionStore();
const initialLoadGate = makeGate();
const initialLoadStarted = makeGate();
const staleRecorderCalled = makeGate();
const observedCoverage: Array<number | undefined> = [];
const writeOutcomes: string[] = [];
const runStore = new MemoryAgentRunStore({
beforeAgentRunEventRead: async (_sessionId, runId) => {
if (runId !== 'seed-checkpoint-run') return;
initialLoadStarted.release();
await initialLoadGate.promise;
},
});
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new InitialCheckpointLoadRaceProbeBackend(
ctx,
staleRecorderCalled,
observedCoverage,
writeOutcomes,
),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(12_802),
});
const session = await manager.createSession(makeInput());
const durableCheckpoint = buildHistoryCompactCheckpoint({
sessionId: session.id,
coveredRuntimeEvents: Array.from(
{ length: 10 },
(_, index): RuntimeEvent => ({
id: `durable-source-event-${index}`,
sessionId: session.id,
runId: `durable-source-run-${index}`,
turnId: `durable-source-turn-${index}`,
invocationId: `durable-source-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
),
summary: 'durable checkpoint',
});
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'seed-checkpoint-run',
turnId: 'seed-checkpoint-turn',
status: 'completed',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'seed-checkpoint-run',
turnId: 'seed-checkpoint-turn',
type: 'history_compact_checkpoint_recorded',
ts: 1,
data: { checkpoint: durableCheckpoint },
}),
],
);
const parentTurn = drain(
manager.sendMessage(session.id, { turnId: 'parent-initial-load', text: 'hello' }),
);
await initialLoadStarted.promise;
const parentRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'parent-initial-load',
);
if (!parentRun) throw new Error('parent run was not recorded');
const childTurn = drain(
manager.startChildTurn(session.id, {
turnId: 'child-stale-during-load',
parentRunId: parentRun.runId,
spec: {
id: LOCAL_READ_AGENT_ID,
name: LOCAL_READ_AGENT_DEFINITION.name,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
},
prompt: 'inspect the repo',
}),
);
await staleRecorderCalled.promise;
initialLoadGate.release();
await Promise.all([parentTurn, childTurn]);
expect(observedCoverage).toEqual([10]);
expect(writeOutcomes).toEqual(['child-stale-during-load:rejected']);
const checkpointCoverage: number[] = [];
for (const run of await runStore.listSessionRuns(session.id)) {
for (const event of await runStore.readEvents(session.id, run.runId)) {
if (event.type === 'history_compact_checkpoint_recorded') {
checkpointCoverage.push(
(event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount,
);
}
}
}
expect(checkpointCoverage).toEqual([10]);
});
test('recovers a missing projection before rejecting a shorter cold-start checkpoint', async () => {
const store = new MemorySessionStore();
const runStore = new MissingCheckpointProjectionAgentRunStore();
const writeOutcomes: string[] = [];
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new CheckpointRecorderContractProbeBackend(ctx, async () => {}, writeOutcomes),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_803),
});
const session = await manager.createSession(makeInput());
const durableCheckpoint = buildHistoryCompactCheckpoint({
sessionId: session.id,
coveredRuntimeEvents: Array.from(
{ length: 10 },
(_, index): RuntimeEvent => ({
id: `cold-durable-event-${index}`,
sessionId: session.id,
runId: `cold-durable-run-${index}`,
turnId: `cold-durable-turn-${index}`,
invocationId: `cold-durable-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
),
summary: 'durable checkpoint before projection loss',
});
const durableEvent = makeRunEvent({
sessionId: session.id,
runId: 'cold-seed-run',
turnId: 'cold-seed-turn',
type: 'history_compact_checkpoint_recorded',
ts: 1,
data: { checkpoint: durableCheckpoint },
});
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'cold-seed-run',
turnId: 'cold-seed-turn',
status: 'completed',
}),
[durableEvent],
);
await drain(
manager.sendMessage(session.id, {
turnId: 'cold-stale-after-projection-loss',
text: 'hello',
}),
);
expect(writeOutcomes).toEqual(['cold-stale-after-projection-loss:rejected']);
expect(runStore.repairedProjection?.id).toBe(durableEvent.id);
const checkpointCoverage: number[] = [];
for (const run of await runStore.listSessionRuns(session.id)) {
for (const event of await runStore.readEvents(session.id, run.runId)) {
if (event.type === 'history_compact_checkpoint_recorded') {
checkpointCoverage.push(
(event.data!.checkpoint as HistoryCompactCheckpoint).coverage.eventCount,
);
}
}
}
expect(checkpointCoverage).toEqual([10]);
});
test('startup recovery marks persisted running turns as failed instead of leaving them stuck', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_800) });
const running = await manager.createSession(makeInput({ status: 'running' }));
const waiting = await manager.createSession(makeInput({ status: 'waiting_for_user' }));
const activeStuck = await manager.createSession(makeInput({ status: 'active' }));
const failedThenCompleted = await manager.createSession(makeInput({ status: 'active' }));
const activeDone = await manager.createSession(makeInput({ status: 'active' }));
await store.appendMessages(running.id, [
{ type: 'user', id: 'running-user', turnId: 'running-turn', ts: 10, text: 'still running' },
{
type: 'turn_state',
id: 'running-state',
turnId: 'running-turn',
ts: 11,
status: 'running',
partialOutputRetained: false,
},
]);
await store.appendMessages(waiting.id, [
{ type: 'user', id: 'waiting-user', turnId: 'waiting-turn', ts: 20, text: 'waiting' },
{
type: 'turn_state',
id: 'waiting-state',
turnId: 'waiting-turn',
ts: 21,
status: 'running',
partialOutputRetained: false,
},
]);
await store.appendMessages(activeStuck.id, [
{
type: 'user',
id: 'active-stuck-user',
turnId: 'active-stuck-turn',
ts: 30,
text: 'already active but stuck',
},
{
type: 'turn_state',
id: 'active-stuck-state',
turnId: 'active-stuck-turn',
ts: 31,
status: 'running',
partialOutputRetained: false,
},
]);
await store.appendMessages(failedThenCompleted.id, [
{
type: 'user',
id: 'failed-completed-user',
turnId: 'failed-completed-turn',
ts: 32,
text: 'failed then completed',
},
{
type: 'turn_state',
id: 'failed-completed-running',
turnId: 'failed-completed-turn',
ts: 33,
status: 'running',
partialOutputRetained: false,
},
{
type: 'turn_state',
id: 'failed-completed-failed',
turnId: 'failed-completed-turn',
ts: 34,
status: 'failed',
errorClass: 'tool_failed',
partialOutputRetained: false,
},
{
type: 'turn_state',
id: 'failed-completed-completed',
turnId: 'failed-completed-turn',
ts: 35,
status: 'completed',
partialOutputRetained: false,
},
]);
await store.appendMessages(activeDone.id, [
{ type: 'user', id: 'active-user', turnId: 'active-turn', ts: 30, text: 'done' },
{
type: 'turn_state',
id: 'active-state',
turnId: 'active-turn',
ts: 31,
status: 'completed',
partialOutputRetained: false,
},
]);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([running.id, waiting.id, activeStuck.id, failedThenCompleted.id]);
expect((await store.readHeader(running.id)).status).toBe('active');
expect((await store.readHeader(waiting.id)).status).toBe('active');
expect((await store.readHeader(activeStuck.id)).status).toBe('active');
expect((await store.readHeader(failedThenCompleted.id)).status).toBe('active');
expect((await store.readHeader(activeDone.id)).status).toBe('active');
const runningTurn = (await store.listTurns(running.id)).find(
(turn) => turn.turnId === 'running-turn',
);
const waitingTurn = (await store.listTurns(waiting.id)).find(
(turn) => turn.turnId === 'waiting-turn',
);
const activeStuckTurn = (await store.listTurns(activeStuck.id)).find(
(turn) => turn.turnId === 'active-stuck-turn',
);
const failedThenCompletedTurn = (await store.listTurns(failedThenCompleted.id)).find(
(turn) => turn.turnId === 'failed-completed-turn',
);
const activeTurn = (await store.listTurns(activeDone.id)).find(
(turn) => turn.turnId === 'active-turn',
);
expect(runningTurn?.status).toBe('failed');
expect(runningTurn?.errorClass).toBe('app_restarted');
expect(waitingTurn?.status).toBe('failed');
expect(waitingTurn?.errorClass).toBe('app_restarted');
expect(activeStuckTurn?.status).toBe('failed');
expect(activeStuckTurn?.errorClass).toBe('app_restarted');
expect(failedThenCompletedTurn?.status).toBe('failed');
expect(failedThenCompletedTurn?.errorClass).toBe('tool_failed');
expect(activeTurn?.status).toBe('completed');
});
test('startup recovery writes terminal turn_state for a stale continuation run', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_814),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
status: 'running',
parentRunId: 'source-run',
parentTurnId: 'source-turn',
continuationSource: {
sourceInvocationId: 'source-invocation',
sourceRunId: 'source-run',
sourceTurnId: 'source-turn',
sourceRuntimeEventHighWater: 2,
},
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
type: 'run_started',
ts: 13,
}),
makeRunEvent({
sessionId: session.id,
runId: 'continuation-run',
turnId: 'continuation-turn',
type: 'model_stream_started',
ts: 14,
}),
],
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([session.id]);
const messages = await store.readMessages(session.id);
expect(
messages.find(
(message) => message.type === 'turn_state' && message.turnId === 'continuation-turn',
),
).toMatchObject({
type: 'turn_state',
status: 'failed',
errorClass: 'app_restarted',
parentTurnId: 'source-turn',
});
});
test('startup recovery maps failed aborted and cancelled RuntimeEvent terminal facts consistently', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_817),
});
const failed = await manager.createSession(makeInput({ status: 'running' }));
const aborted = await manager.createSession(makeInput({ status: 'running' }));
const cancelled = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, failed.id, 'failed-turn');
await seedRunningTurn(store, aborted.id, 'aborted-turn');
await seedRunningTurn(store, cancelled.id, 'cancelled-turn');
await seedRun(
runStore,
makeRunHeader({
sessionId: failed.id,
runId: 'failed-run',
turnId: 'failed-turn',
status: 'running',
}),
[
makeRunEvent({
sessionId: failed.id,
runId: 'failed-run',
turnId: 'failed-turn',
type: 'run_started',
ts: 11,
}),
],
);
await seedRun(
runStore,
makeRunHeader({
sessionId: aborted.id,
runId: 'aborted-run',
turnId: 'aborted-turn',
status: 'running',
}),
[
makeRunEvent({
sessionId: aborted.id,
runId: 'aborted-run',
turnId: 'aborted-turn',
type: 'run_started',
ts: 21,
}),
],
);
await seedRun(
runStore,
makeRunHeader({
sessionId: cancelled.id,
runId: 'cancelled-run',
turnId: 'cancelled-turn',
status: 'running',
}),
[
makeRunEvent({
sessionId: cancelled.id,
runId: 'cancelled-run',
turnId: 'cancelled-turn',
type: 'run_started',
ts: 31,
}),
],
);
await runStore.appendRuntimeEvent(
failed.id,
'failed-run',
runtimeEvent({
id: 'rt-failed',
sessionId: failed.id,
runId: 'failed-run',
turnId: 'failed-turn',
ts: 12,
role: 'system',
author: 'system',
status: 'failed',
content: { kind: 'error', reason: 'tool_failed', message: 'Tool failed' },
actions: { endInvocation: true },
}),
);
await runStore.appendRuntimeEvent(
aborted.id,
'aborted-run',
runtimeEvent({
id: 'rt-aborted',
sessionId: aborted.id,
runId: 'aborted-run',
turnId: 'aborted-turn',
ts: 22,
role: 'system',
author: 'system',
status: 'aborted',
actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } },
}),
);
await runStore.appendRuntimeEvent(
cancelled.id,
'cancelled-run',
runtimeEvent({
id: 'rt-cancelled',
sessionId: cancelled.id,
runId: 'cancelled-run',
turnId: 'cancelled-turn',
ts: 32,
role: 'system',
author: 'system',
status: 'cancelled',
actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } },
}),
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([failed.id, aborted.id, cancelled.id]);
expect((await runStore.readRun(failed.id, 'failed-run')).status).toBe('failed');
expect((await runStore.readRun(failed.id, 'failed-run')).failureClass).toBe('tool_failed');
expect((await store.listTurns(failed.id))[0]?.status).toBe('failed');
expect((await store.listTurns(failed.id))[0]?.errorClass).toBe('tool_failed');
expect((await runStore.readRun(aborted.id, 'aborted-run')).status).toBe('cancelled');
expect((await store.listTurns(aborted.id))[0]?.status).toBe('aborted');
expect((await store.listTurns(aborted.id))[0]?.abortSource).toBe('renderer.stop_button');
expect((await runStore.readRun(cancelled.id, 'cancelled-run')).status).toBe('cancelled');
expect((await store.listTurns(cancelled.id))[0]?.status).toBe('aborted');
expect((await store.listTurns(cancelled.id))[0]?.abortSource).toBe('renderer.stop_button');
expect(
(await runStore.readEvents(failed.id, 'failed-run')).map((event) => event.type),
).toContain('run_failed');
expect(
(await runStore.readEvents(aborted.id, 'aborted-run')).map((event) => event.type),
).toContain('run_cancelled');
expect(
(await runStore.readEvents(cancelled.id, 'cancelled-run')).map((event) => event.type),
).toContain('run_cancelled');
});
test('startup recovery refuses cache-completed recovery without a RuntimeEvent terminal fact', async () => {
const unreadableStore = new MemorySessionStore();
const unreadableRunStore = new MemoryAgentRunStore({ failRuntimeEventReads: true });
const incompleteStore = new MemorySessionStore();
const incompleteRunStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const unreadableManager = new SessionManager({
store: unreadableStore,
runStore: unreadableRunStore,
runtimeEventStore: unreadableRunStore,
backends,
newId: nextId(),
now: nextNow(12_818),
});
const incompleteManager = new SessionManager({
store: incompleteStore,
runStore: incompleteRunStore,
runtimeEventStore: incompleteRunStore,
backends,
newId: nextId(),
now: nextNow(12_819),
});
const unreadable = await unreadableManager.createSession(makeInput({ status: 'running' }));
const incomplete = await incompleteManager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(unreadableStore, unreadable.id, 'turn-1');
await unreadableStore.appendMessage(unreadable.id, {
type: 'assistant',
id: 'assistant-1',
turnId: 'turn-1',
ts: 13,
text: 'done',
modelId: 'fake-model',
});
await seedRun(
unreadableRunStore,
makeRunHeader({
sessionId: unreadable.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: unreadable.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_completed',
ts: 12,
}),
],
);
await seedRunningTurn(incompleteStore, incomplete.id, 'turn-1');
await incompleteStore.appendMessage(incomplete.id, {
type: 'assistant',
id: 'assistant-1',
turnId: 'turn-1',
ts: 13,
text: 'done',
modelId: 'fake-model',
});
await seedRun(
incompleteRunStore,
makeRunHeader({
sessionId: incomplete.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: incomplete.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_completed',
ts: 12,
}),
],
);
await incompleteRunStore.appendRuntimeEvent(
incomplete.id,
'run-1',
runtimeEvent({
id: 'rt-incomplete-failed',
sessionId: incomplete.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 14,
role: 'system',
author: 'system',
status: 'failed',
actions: { endInvocation: true },
}),
);
await unreadableManager.recoverInterruptedSessions();
await incompleteManager.recoverInterruptedSessions();
expect((await unreadableRunStore.readRun(unreadable.id, 'run-1')).status).toBe('running');
expect((await unreadableStore.listTurns(unreadable.id))[0]?.status).toBe('running');
expect((await incompleteRunStore.readRun(incomplete.id, 'run-1')).status).toBe('failed');
expect((await incompleteStore.listTurns(incomplete.id))[0]?.status).toBe('failed');
expect(
(await unreadableRunStore.readEvents(unreadable.id, 'run-1')).find(
(event) => event.type === 'run_failed',
),
).toBeUndefined();
expect(
(await incompleteRunStore.readEvents(incomplete.id, 'run-1')).find(
(event) => event.type === 'run_failed',
)?.data?.recoveryReason,
).toBe('model_stream_completed_without_runtime_terminal');
});
test('startup recovery derives the interrupted outcome sink from the runtime store', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
let sessionId = '';
let outcomeCommitFailuresRemaining = 3;
let outcomeCommitAttempts = 0;
const runtimeCommitSink: RuntimeCommitSink = {
commitToolPrepared: async () => {
throw new Error('not used during recovery');
},
commitToolOutcome: async (input) => {
outcomeCommitAttempts += 1;
if (outcomeCommitFailuresRemaining > 0) {
outcomeCommitFailuresRemaining -= 1;
throw new Error('transient outcome commit failure');
}
await runStore.appendRuntimeEvent(sessionId, 'run-1', input.runtimeEvent);
return { created: true, runtimeEventSeq: 4 };
},
};
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: Object.assign(runStore, runtimeCommitSink),
backends,
newId: nextId(),
now: nextNow(12_825),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
sessionId = session.id;
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
toolMode: 'code_mode',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'tool_started',
ts: 12,
}),
],
);
const code = { code: 'return await tools.Read({ path: "a.ts" })' };
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'initial',
sessionId: session.id,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'inspect' },
actions: { runtimeProtocol: { toolBoundary: 't1_after_preflight_v1' } },
}),
);
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'outer-call',
sessionId: session.id,
role: 'model',
author: 'agent',
origin: 'provider',
modelVisibility: 'visible',
content: { kind: 'function_call', id: 'exec-1', name: 'exec', args: code },
refs: { operationId: 'outer-op', toolCallId: 'exec-1' },
}),
);
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'outer-dispatch',
sessionId: session.id,
actions: {
toolDispatch: {
protocol: 't1_after_preflight_v1',
operationId: 'outer-op',
providerToolCallId: 'exec-1',
toolName: 'exec',
canonicalArgsHash: canonicalToolArgsHash('exec', code),
recoveryMode: 'never_auto_retry',
},
},
refs: { operationId: 'outer-op', toolCallId: 'exec-1' },
}),
);
await manager.recoverInterruptedSessions();
assert.equal((await runStore.readRun(session.id, 'run-1')).status, 'running');
assert.equal(outcomeCommitAttempts, 2);
assert.equal(
(await runStore.readRuntimeEvents(session.id, 'run-1')).some(
(event) => event.content?.kind === 'function_response',
),
false,
);
await manager.recoverInterruptedSessions();
assert.equal(outcomeCommitAttempts, 4);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
const response = runtimeEvents.find(
(event) => event.content?.kind === 'function_response' && event.content.id === 'exec-1',
);
assert.equal(response?.content?.kind, 'function_response');
assert.equal(response?.content?.kind === 'function_response' && response.content.isError, true);
assert.deepEqual(
response?.content?.kind === 'function_response' ? response.content.result : undefined,
{
kind: 'json',
value: {
kind: 'code_mode',
status: 'interrupted',
message: 'Code Mode execution was interrupted by runtime recovery.',
},
},
);
assert.equal((await runStore.readRun(session.id, 'run-1')).status, 'failed');
});
test('startup recovery does not leave stale permission waits stuck', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_830),
});
const session = await manager.createSession(makeInput({ status: 'waiting_for_user' }));
await store.createSandboxBoundaryRequest({
sessionId: session.id,
requestId: 'boundary-before-restart',
turnId: 'turn-1',
expansion: { network: { enabled: true } },
justification: 'Fetch a dependency.',
});
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'waiting_for_user',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'permission_requested',
ts: 12,
}),
],
);
await manager.recoverInterruptedSessions();
expect((await store.readHeader(session.id)).status).toBe('active');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
// This turn owned the pending request, so its failure names the closure
// rather than the bare restart.
expect(turn?.errorClass).toBe('sandbox_boundary_closed_by_restart');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('sandbox_boundary_closed_by_restart');
expect(await store.listPendingSandboxBoundaryRequests(session.id)).toEqual([]);
});
test('startup recovery attributes a closure whose RuntimeEvent never landed', async () => {
// The request row commits before the matching RuntimeEvent is published,
// and that append is fail-open. A crash inside that window leaves a ledger
// with no boundary event at all — provenance on the row is what keeps the
// closure attributable (#1612).
const { store, runStore, manager, session } = await seedBoundaryRestartSession({
now: 12_870,
requestId: 'boundary-unanswered',
});
await manager.recoverInterruptedSessions();
expect(await store.listPendingSandboxBoundaryRequests(session.id)).toEqual([]);
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('sandbox_boundary_closed_by_restart');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('sandbox_boundary_closed_by_restart');
const terminalEvent = (await runStore.readRuntimeEvents(session.id, 'run-1')).find(
(event) => event.actions?.endInvocation === true,
);
expect(terminalEvent?.actions?.stateDelta?.sandboxBoundaryClosureReason).toBe('host_restarted');
expect(terminalEvent?.actions?.stateDelta?.sandboxBoundaryRequestIds).toEqual([
'boundary-unanswered',
]);
});
test('startup recovery re-reads a closure it settled before an earlier crash', async () => {
// Stands in for a first recovery that settled the request and then died
// before committing the run's terminal fact: the row is no longer pending,
// so only a durable re-read of the closure can still attribute it.
const { store, runStore, manager, session } = await seedBoundaryRestartSession({
now: 12_880,
requestId: 'boundary-settled-earlier',
});
await store.settleSandboxBoundaryRequest({
sessionId: session.id,
requestId: 'boundary-settled-earlier',
decision: 'deny',
closureReason: 'host_restarted',
});
expect(await store.listPendingSandboxBoundaryRequests(session.id)).toEqual([]);
await manager.recoverInterruptedSessions();
const [turn] = await store.listTurns(session.id);
expect(turn?.errorClass).toBe('sandbox_boundary_closed_by_restart');
const terminalStatesAfterFirst = (await store.readMessages(session.id)).filter(
(message) => message.type === 'turn_state' && message.status === 'failed',
).length;
// A further recovery pass must be a no-op rather than a second closure.
await manager.recoverInterruptedSessions();
const [turnAfterSecond] = await store.listTurns(session.id);
expect(turnAfterSecond?.errorClass).toBe('sandbox_boundary_closed_by_restart');
expect(
(await store.readMessages(session.id)).filter(
(message) => message.type === 'turn_state' && message.status === 'failed',
).length,
).toBe(terminalStatesAfterFirst);
expect((await runStore.listSessionRuns(session.id))[0]?.failureClass).toBe(
'sandbox_boundary_closed_by_restart',
);
});
test('startup recovery keeps the generic restart class for an answered boundary request', async () => {
const { store, runStore, manager, session } = await seedBoundaryRestartSession({
now: 12_890,
requestId: 'boundary-answered',
});
await store.settleSandboxBoundaryRequest({
sessionId: session.id,
requestId: 'boundary-answered',
decision: 'allow',
});
// A fully recorded request/decision pair in the ledger must not be mistaken
// for a closure: the user answered it before the host went away.
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'rt-boundary-request',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 12,
actions: {
stateDelta: {
sandboxBoundaryRequest: {
requestId: 'boundary-answered',
toolUseId: 'tool-1',
justification: 'Fetch a dependency.',
expansion: { network: { enabled: true } },
},
},
},
refs: { toolCallId: 'tool-1' },
}),
);
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'rt-boundary-decision',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 13,
author: 'user',
actions: {
stateDelta: {
sandboxBoundaryDecision: {
requestId: 'boundary-answered',
decision: 'allow',
status: 'approved',
revision: 1,
},
},
},
refs: { toolCallId: 'tool-1' },
}),
);
await manager.recoverInterruptedSessions();
const [turn] = await store.listTurns(session.id);
expect(turn?.errorClass).toBe('app_restarted');
});
test('startup recovery treats terminal AgentRun headers without RuntimeEvent facts as missing terminal events', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_860),
});
const completed = await manager.createSession(makeInput({ status: 'active' }));
const failed = await manager.createSession(makeInput({ status: 'active' }));
const cancelled = await manager.createSession(makeInput({ status: 'active' }));
await seedRun(
runStore,
makeRunHeader({
sessionId: completed.id,
runId: 'completed-run',
turnId: 'completed-turn',
status: 'completed',
completedAt: 20,
}),
[
makeRunEvent({
sessionId: completed.id,
runId: 'completed-run',
turnId: 'completed-turn',
type: 'run_completed',
ts: 20,
}),
],
);
await seedRun(
runStore,
makeRunHeader({
sessionId: failed.id,
runId: 'failed-run',
turnId: 'failed-turn',
status: 'failed',
failureClass: 'tool_failed',
completedAt: 21,
}),
[
makeRunEvent({
sessionId: failed.id,
runId: 'failed-run',
turnId: 'failed-turn',
type: 'run_failed',
ts: 21,
data: { failureClass: 'tool_failed' },
}),
],
);
await seedRun(
runStore,
makeRunHeader({
sessionId: cancelled.id,
runId: 'cancelled-run',
turnId: 'cancelled-turn',
status: 'cancelled',
completedAt: 22,
}),
[
makeRunEvent({
sessionId: cancelled.id,
runId: 'cancelled-run',
turnId: 'cancelled-turn',
type: 'run_cancelled',
ts: 22,
}),
],
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([completed.id, failed.id, cancelled.id]);
const completedRun = await runStore.readRun(completed.id, 'completed-run');
expect(completedRun.status).toBe('failed');
expect(completedRun.failureClass).toBe('missing_terminal_event');
expect(
(await runStore.readEvents(completed.id, 'completed-run')).map((event) => event.type),
).toEqual(['run_completed']);
const completedTerminalEvents = (
await runStore.readRuntimeEvents(completed.id, 'completed-run')
).filter(isTerminalRuntimeEvent);
expect(completedTerminalEvents.map((event) => event.status)).toEqual(['failed']);
expect(completedTerminalEvents[0]?.actions?.stateDelta?.failureClass).toBe(
'missing_terminal_event',
);
const failedRun = await runStore.readRun(failed.id, 'failed-run');
expect(failedRun.status).toBe('failed');
expect(failedRun.failureClass).toBe('missing_terminal_event');
expect((await runStore.readEvents(failed.id, 'failed-run')).map((event) => event.type)).toEqual(
['run_failed'],
);
const failedTerminalEvents = (await runStore.readRuntimeEvents(failed.id, 'failed-run')).filter(
isTerminalRuntimeEvent,
);
expect(failedTerminalEvents.map((event) => event.status)).toEqual(['failed']);
expect(failedTerminalEvents[0]?.actions?.stateDelta?.failureClass).toBe(
'missing_terminal_event',
);
const cancelledRun = await runStore.readRun(cancelled.id, 'cancelled-run');
expect(cancelledRun.status).toBe('failed');
expect(cancelledRun.failureClass).toBe('missing_terminal_event');
expect(
(await runStore.readEvents(cancelled.id, 'cancelled-run')).map((event) => event.type),
).toEqual(['run_cancelled']);
const cancelledTerminalEvents = (
await runStore.readRuntimeEvents(cancelled.id, 'cancelled-run')
).filter(isTerminalRuntimeEvent);
expect(cancelledTerminalEvents.map((event) => event.status)).toEqual(['failed']);
expect(cancelledTerminalEvents[0]?.actions?.stateDelta?.failureClass).toBe(
'missing_terminal_event',
);
});
test('regenerate creates a new sibling turn from an aborted source turn (retry merged)', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const events: PartialEvent[] = [{ type: 'complete', stopReason: 'end_turn' }];
backends.register('fake', (ctx) => new EventBackend(ctx, events));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(13_000),
});
const session = await manager.createSession(makeInput());
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
status: 'cancelled',
completedAt: 102,
}),
[
runtimeEvent({
id: 'source-user',
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'try this' },
}),
runtimeEvent({
id: 'source-abort',
sessionId: session.id,
runId: 'source-run',
turnId: 'source',
ts: 102,
role: 'system',
author: 'system',
status: 'aborted',
actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } },
}),
],
);
await drain(manager.regenerateTurn(session.id, { sourceTurnId: 'source', turnId: 'regen-1' }));
const turns = await manager.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'source')?.status).toBe('aborted');
const regen = turns.find((turn) => turn.turnId === 'regen-1');
expect(regen?.status).toBe('completed');
expect(regen?.regeneratedFromTurnId).toBe('source');
const regenUser = (await store.readMessages(session.id)).find(
(message) => message.type === 'user' && message.turnId === 'regen-1',
);
expect(regenUser?.type === 'user' ? regenUser.text : undefined).toBe('try this');
});
test('branchFromTurn creates a new session with parent lineage and copied message boundary', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_000),
});
const session = await manager.createSession(
makeInput({ name: 'Parent', projectId: 'project-1' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
await drain(manager.sendMessage(session.id, { turnId: 'after', text: 'do not copy' }));
const child = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Child',
});
expect(child.parentSessionId).toBe(session.id);
expect(child.branchOfTurnId).toBe('source');
expect(child.projectId).toBe('project-1');
const childMessages = await store.readMessages(child.id);
expect(
childMessages.some((message) => (message as { turnId?: string }).turnId === 'source'),
).toBe(true);
expect(
childMessages.some((message) => (message as { turnId?: string }).turnId === 'after'),
).toBe(false);
expect(childMessages.some((message) => message.type === 'turn_state')).toBe(false);
});
test('branchFromTurn marks side conversations without changing ordinary branches', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_500),
});
const session = await manager.createSession(makeInput({ name: 'Parent', labels: ['kept'] }));
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
const ordinary = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Ordinary',
});
const side = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Side',
sideConversation: true,
});
expect(ordinary.labels).toEqual(['kept']);
expect(side.labels).toEqual(['kept', SIDE_CONVERSATION_SESSION_LABEL]);
});
test('conversation copies inherit the authoritative execution boundary', async () => {
const store = new AtomicBoundaryMemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_050),
});
const boundaries: ExecutionBoundary[] = [
{
kind: 'managed',
revision: 4,
profile: applySandboxBoundaryExpansion(createWorkspaceWritePermissionProfile(), {
filesystem: {
entries: [{ path: '/approved/release.txt', access: 'write', scope: 'exact' }],
},
}),
},
{ kind: 'external', revision: 3 },
];
for (const [index, boundary] of boundaries.entries()) {
const source = await manager.createSession(makeInput({ name: `Source ${index}` }));
await drain(manager.sendMessage(source.id, { turnId: 'first', text: 'keep' }));
await drain(manager.sendMessage(source.id, { turnId: 'second', text: 'replace' }));
store.forceBoundary(source.id, boundary);
const branch = await manager.branchFromTurn(source.id, { sourceTurnId: 'first' });
const revision = await manager.reviseBeforeTurn(source.id, { sourceTurnId: 'second' });
for (const copy of [branch, revision]) {
expect(await store.readExecutionBoundary(copy.id)).toEqual({
...boundary,
revision: 0,
});
}
}
});
test('branch and revision sessions rewrite tool operation authority facts', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_100),
});
const session = await manager.createSession(makeInput({ name: 'Authority source' }));
const header = await store.readHeader(session.id);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'authority-source-run',
turnId: 'authority-source-turn',
status: 'completed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 3,
completedAt: 3,
}),
[
runtimeEvent({
id: 'authority-source-user',
sessionId: session.id,
invocationId: 'authority-source-invocation',
runId: 'authority-source-run',
turnId: 'authority-source-turn',
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'authority-bearing history' },
}),
runtimeEvent({
id: 'authority-source-dispatch',
sessionId: session.id,
invocationId: 'authority-source-invocation',
runId: 'authority-source-run',
turnId: 'authority-source-turn',
ts: 2,
role: 'system',
author: 'system',
actions: {
toolDispatch: {
protocol: 't1_after_preflight_v1',
operationId: 'authority-operation',
providerToolCallId: 'authority-call',
toolName: 'Write',
canonicalArgsHash: `sha256:${'a'.repeat(64)}`,
recoveryMode: 'reconcile',
},
},
refs: { toolCallId: 'authority-call', operationId: 'authority-operation' },
}),
runtimeEvent({
id: 'authority-source-terminal',
sessionId: session.id,
invocationId: 'authority-source-invocation',
runId: 'authority-source-run',
turnId: 'authority-source-turn',
ts: 3,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'authority-later-run',
turnId: 'authority-later-turn',
status: 'completed',
cwd: header.cwd,
createdAt: 4,
updatedAt: 5,
completedAt: 5,
}),
[
runtimeEvent({
id: 'authority-later-user',
sessionId: session.id,
invocationId: 'authority-later-invocation',
runId: 'authority-later-run',
turnId: 'authority-later-turn',
ts: 4,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'later boundary' },
}),
runtimeEvent({
id: 'authority-later-terminal',
sessionId: session.id,
invocationId: 'authority-later-invocation',
runId: 'authority-later-run',
turnId: 'authority-later-turn',
ts: 5,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const copies = [
await manager.branchFromTurn(session.id, {
sourceTurnId: 'authority-source-turn',
name: 'Tool branch',
}),
await manager.branchBeforeTurn(session.id, {
sourceTurnId: 'authority-later-turn',
name: 'Tool branch before later turn',
}),
await manager.reviseBeforeTurn(session.id, {
sourceTurnId: 'authority-later-turn',
}),
];
for (const copy of copies) {
const [targetRun] = await runStore.listSessionRuns(copy.id);
assert.ok(targetRun);
const targetEvents = await runStore.readRuntimeEvents(copy.id, targetRun.runId);
const dispatch = targetEvents.find((event) => event.actions?.toolDispatch);
const targetOperationId = dispatch?.actions?.toolDispatch?.operationId;
assert.ok(targetOperationId);
assert.notEqual(targetOperationId, 'authority-operation');
assert.equal(dispatch?.refs?.operationId, targetOperationId);
assert.equal(targetRun.sessionId, copy.id);
}
});
test('fails before creating branch or revision sessions for child continuation authority', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(15_150),
});
const session = await manager.createSession(makeInput({ name: 'Legacy continuation' }));
const header = await store.readHeader(session.id);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'continuation-parent-run',
turnId: 'continuation-parent-turn',
status: 'completed',
cwd: header.cwd,
createdAt: 1,
updatedAt: 2,
completedAt: 2,
}),
[
runtimeEvent({
id: 'continuation-parent-user',
sessionId: session.id,
invocationId: 'continuation-parent-invocation',
runId: 'continuation-parent-run',
turnId: 'continuation-parent-turn',
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'retain parent and child closure' },
}),
runtimeEvent({
id: 'continuation-parent-terminal',
sessionId: session.id,
invocationId: 'continuation-parent-invocation',
runId: 'continuation-parent-run',
turnId: 'continuation-parent-turn',
ts: 2,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'legacy-continuation-run',
turnId: 'legacy-child-turn',
parentRunId: 'continuation-parent-run',
agentId: 'child-agent',
status: 'completed',
cwd: header.cwd,
continuationSource: {
sourceInvocationId: 'legacy-source-invocation',
sourceRunId: 'legacy-source-run',
sourceTurnId: 'legacy-source-turn',
sourceRuntimeEventHighWater: 1,
},
createdAt: 3,
updatedAt: 4,
completedAt: 4,
}),
[
runtimeEvent({
id: 'legacy-continuation-user',
sessionId: session.id,
invocationId: 'legacy-continuation-run',
runId: 'legacy-continuation-run',
turnId: 'legacy-child-turn',
ts: 3,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'legacy continuation history' },
}),
runtimeEvent({
id: 'legacy-continuation-terminal',
sessionId: session.id,
invocationId: 'legacy-continuation-run',
runId: 'legacy-continuation-run',
turnId: 'legacy-child-turn',
ts: 4,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'continuation-later-run',
turnId: 'continuation-later-turn',
status: 'completed',
cwd: header.cwd,
createdAt: 5,
updatedAt: 6,
completedAt: 6,
}),
[
runtimeEvent({
id: 'continuation-later-user',
sessionId: session.id,
invocationId: 'continuation-later-invocation',
runId: 'continuation-later-run',
turnId: 'continuation-later-turn',
ts: 5,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'later boundary' },
}),
runtimeEvent({
id: 'continuation-later-terminal',
sessionId: session.id,
invocationId: 'continuation-later-invocation',
runId: 'continuation-later-run',
turnId: 'continuation-later-turn',
ts: 6,
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const attempts = [
() =>
manager.branchFromTurn(session.id, {
sourceTurnId: 'continuation-parent-turn',
name: 'Blocked child continuation branch',
}),
() =>
manager.branchBeforeTurn(session.id, {
sourceTurnId: 'continuation-later-turn',
name: 'Blocked child continuation branch before',
}),
() =>
manager.reviseBeforeTurn(session.id, {
sourceTurnId: 'continuation-later-turn',
}),
];
for (const attempt of attempts) {
await expectRejects(attempt(), /typed identity rewriting/i);
}
expect((await store.list()).map((candidate) => candidate.id)).toEqual([session.id]);
});
test('hydrates an inherited running ShellRun with its source-session owner', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const ref = 'maka://runtime/background-tasks/pty-parent';
const sourceSnapshot: ShellRunSnapshotResult = {
kind: 'shell_run',
ref,
mode: 'pty',
status: 'running',
cwd: '/tmp/workspace',
cmd: 'interactive',
startedAt: 1,
updatedAt: 2,
revision: 2,
output: {
mode: 'pty',
screen: 'ready',
scrollback: '',
cols: 80,
rows: 24,
cursor: { x: 5, y: 0, visible: true },
alternateScreen: false,
truncated: false,
redacted: false,
},
};
let ownerAvailable = true;
const shellRuns = {
async listSessionUpdates() {
return [];
},
async getSessionUpdate() {
return undefined;
},
async inspectResource(sessionId: string, candidateRef: string) {
if (ownerAvailable && candidateRef === ref && sessionId === 'session-1')
return sourceSnapshot;
const error = new Error('missing') as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
},
} as unknown as ShellRunProcessManager;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
shellRuns,
newId: nextId(),
now: nextNow(15_250),
});
const parent = await manager.createSession(makeInput({ name: 'Parent' }));
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: parent.id,
runId: 'source-run',
turnId: 'source',
status: 'completed',
createdAt: 1,
updatedAt: 4,
completedAt: 4,
}),
[
runtimeEvent({
id: 'user-1',
sessionId: parent.id,
runId: 'source-run',
turnId: 'source',
ts: 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'start' },
}),
runtimeEvent({
id: 'call-1',
sessionId: parent.id,
runId: 'source-run',
turnId: 'source',
ts: 2,
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'bash-1',
name: 'Bash',
args: { command: 'interactive', run_in_background: true, pty: true },
},
refs: { toolCallId: 'bash-1' },
}),
runtimeEvent({
id: 'result-1',
sessionId: parent.id,
runId: 'source-run',
turnId: 'source',
ts: 3,
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'bash-1',
name: 'Bash',
result: {
kind: 'shell_run',
ref,
mode: 'pty',
status: 'running',
cwd: '/tmp/workspace',
cmd: 'interactive',
startedAt: 1,
updatedAt: 1,
revision: 1,
},
isError: false,
},
refs: { toolCallId: 'bash-1' },
}),
runtimeEvent({
id: 'complete-1',
sessionId: parent.id,
runId: 'source-run',
turnId: 'source',
ts: 4,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: parent.id,
runId: 'later-run',
turnId: 'later',
status: 'completed',
createdAt: 5,
updatedAt: 6,
completedAt: 6,
}),
[
runtimeEvent({
id: 'later-user',
sessionId: parent.id,
runId: 'later-run',
turnId: 'later',
ts: 5,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'later' },
}),
runtimeEvent({
id: 'later-complete',
sessionId: parent.id,
runId: 'later-run',
turnId: 'later',
ts: 6,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
],
);
const child = await manager.branchFromTurn(parent.id, {
sourceTurnId: 'source',
name: 'Child',
});
const revision = await manager.reviseBeforeTurn(parent.id, { sourceTurnId: 'later' });
const updates = await manager.listShellRunUpdates(child.id);
expect(updates).toHaveLength(1);
expect(updates[0]?.sessionId).toBe(child.id);
expect(updates[0]?.ownership).toEqual({
kind: 'source_owned',
sourceSessionId: parent.id,
ownerSessionId: parent.id,
});
expect(updates[0]?.sourceToolCallId).toBe('bash-1');
expect(updates[0]?.result).toEqual(sourceSnapshot);
expect(await manager.getShellRunUpdate(child.id, sourceSnapshot.ref)).toEqual(updates[0]);
expect(
await manager.getShellRunUpdate(child.id, 'maka://runtime/background-tasks/missing-shell'),
).toBeNull();
const revisionUpdates = await manager.listShellRunUpdates(revision.id);
expect(revisionUpdates).toHaveLength(1);
expect(revisionUpdates[0]?.ownership).toEqual({
kind: 'source_owned',
sourceSessionId: parent.id,
ownerSessionId: parent.id,
});
expect(revisionUpdates[0]?.result).toEqual(sourceSnapshot);
ownerAvailable = false;
await store.remove(parent.id);
const danglingUpdates = await manager.listShellRunUpdates(child.id);
expect(danglingUpdates).toHaveLength(1);
expect(danglingUpdates[0]?.sessionId).toBe(child.id);
expect(danglingUpdates[0]?.ownership).toEqual({
kind: 'source_unavailable',
sourceSessionId: parent.id,
});
expect(danglingUpdates[0]?.result.status).toBe('running');
expect(danglingUpdates[0]?.result.output).toBe(undefined);
expect(await manager.getShellRunUpdate(child.id, sourceSnapshot.ref)).toEqual(
danglingUpdates[0],
);
});
test('branchFromTurn preserves parent thinking, collaboration, and orchestration modes', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const contexts: BackendFactoryContext[] = [];
backends.register('fake', (ctx) => {
contexts.push(ctx);
return new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(15_500),
});
const session = await manager.createSession(
makeInput({
name: 'Parent',
thinkingLevel: 'high',
collaborationMode: 'plan',
orchestrationMode: 'swarm',
}),
);
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
const child = await manager.branchFromTurn(session.id, {
sourceTurnId: 'source',
name: 'Child',
});
expect(child.thinkingLevel).toBe('high');
expect(child.collaborationMode).toBe('plan');
expect(child.orchestrationMode).toBe('swarm');
expect((await store.readHeader(child.id)).thinkingLevel).toBe('high');
expect((await store.readHeader(child.id)).collaborationMode).toBe('plan');
expect((await store.readHeader(child.id)).orchestrationMode).toBe('swarm');
await drain(manager.sendMessage(child.id, { turnId: 'child-turn', text: 'continue' }));
expect(contexts.find((ctx) => ctx.sessionId === child.id)?.header.thinkingLevel).toBe('high');
});
test('branchBeforeTurn keeps everything strictly before the turn, dropping it and later turns', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(16_000),
});
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await drain(manager.sendMessage(session.id, { turnId: 'first', text: 'keep me' }));
await drain(manager.sendMessage(session.id, { turnId: 'second', text: 'drop me' }));
const child = await manager.branchBeforeTurn(session.id, {
sourceTurnId: 'second',
name: 'Child',
});
expect(child.parentSessionId).toBe(session.id);
expect(child.branchOfTurnId).toBe('second');
const childMessages = await store.readMessages(child.id);
expect(
childMessages.some((message) => (message as { turnId?: string }).turnId === 'first'),
).toBe(true);
expect(
childMessages.some((message) => (message as { turnId?: string }).turnId === 'second'),
).toBe(false);
expect(childMessages.some((message) => message.type === 'turn_state')).toBe(false);
// The dropped turn is untouched in the original session.
const parentMessages = await store.readMessages(session.id);
expect(
parentMessages.some((message) => (message as { turnId?: string }).turnId === 'second'),
).toBe(true);
});
test('branchBeforeTurn on the first turn yields an empty-context branch that can continue', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(16_500),
});
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await drain(manager.sendMessage(session.id, { turnId: 'only', text: 'the one prompt' }));
const child = await manager.branchBeforeTurn(session.id, {
sourceTurnId: 'only',
name: 'Child',
});
expect(child.parentSessionId).toBe(session.id);
const childMessages = await store.readMessages(child.id);
// Only the session_start note — no copied user/assistant messages.
expect(
childMessages.some((message) => message.type === 'user' || message.type === 'assistant'),
).toBe(false);
expect(childMessages.some((message) => message.type === 'system_note')).toBe(true);
// The empty branch is a normal session: the next prompt runs fine.
await drain(manager.sendMessage(child.id, { turnId: 'child-turn', text: 'restart' }));
expect((await store.readMessages(child.id)).some((message) => message.type === 'user')).toBe(
true,
);
});
test('reviseBeforeTurn creates an in-conversation version without ordinary branch lineage', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(16_700),
});
const session = await manager.createSession(
makeInput({
name: 'Conversation',
projectId: 'project-1',
collaborationMode: 'plan',
orchestrationMode: 'swarm',
}),
);
await manager.setFlagged(session.id, true);
await drain(manager.sendMessage(session.id, { turnId: 'first', text: 'keep me' }));
await drain(manager.sendMessage(session.id, { turnId: 'second', text: 'replace me' }));
const version2 = await manager.reviseBeforeTurn(session.id, { sourceTurnId: 'second' });
expect(version2.name).toBe('Conversation');
expect(version2.projectId).toBe('project-1');
expect(version2.isFlagged).toBe(true);
expect(version2.collaborationMode).toBe('plan');
expect(version2.orchestrationMode).toBe('swarm');
expect(version2.parentSessionId).toBeUndefined();
expect(version2.branchOfTurnId).toBeUndefined();
expect(version2.revisionRootSessionId).toBe(session.id);
expect(version2.revisionParentSessionId).toBe(session.id);
expect(version2.revisionOfTurnId).toBe('second');
expect(version2.revisionIndex).toBe(2);
expect(version2.revisionState).toBe('preparing');
expect((await store.readHeader(version2.id)).collaborationMode).toBe('plan');
expect((await store.readHeader(version2.id)).orchestrationMode).toBe('swarm');
await drain(
manager.sendMessage(
version2.id,
{ turnId: 'edited-second', text: 'replacement' },
{
onRunStarted: async () => {
await manager.commitRevisionVersion(version2.id);
},
},
),
);
expect((await store.readHeader(version2.id)).revisionState).toBe('committed');
const messages = await store.readMessages(version2.id);
expect(messages.some((message) => (message as { turnId?: string }).turnId === 'first')).toBe(
true,
);
expect(messages.some((message) => (message as { turnId?: string }).turnId === 'second')).toBe(
false,
);
const version3 = await manager.reviseBeforeTurn(version2.id, { sourceTurnId: 'first' });
expect(version3.projectId).toBe('project-1');
expect(version3.revisionRootSessionId).toBe(session.id);
expect(version3.revisionParentSessionId).toBe(version2.id);
expect(version3.revisionIndex).toBe(3);
expect(version3.revisionState).toBe('preparing');
expect(version3.parentSessionId).toBeUndefined();
});
test('startup recovery removes empty preparing revisions and commits admitted edits', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(16_900),
});
const root = await manager.createSession(makeInput({ name: 'Recovery root' }));
await drain(manager.sendMessage(root.id, { turnId: 'first', text: 'original' }));
const empty = await manager.reviseBeforeTurn(root.id, { sourceTurnId: 'first' });
expect(empty.revisionState).toBe('preparing');
await manager.recoverInterruptedSessions();
let removedError: unknown;
try {
await store.readHeader(empty.id);
} catch (error) {
removedError = error;
}
expect(removedError instanceof Error ? removedError.message : String(removedError)).toContain(
'Unknown session',
);
const admitted = await manager.reviseBeforeTurn(root.id, { sourceTurnId: 'first' });
await drain(manager.sendMessage(admitted.id, { turnId: 'edited', text: 'edited prompt' }));
expect((await store.readHeader(admitted.id)).revisionState).toBe('preparing');
await manager.recoverInterruptedSessions();
expect((await store.readHeader(admitted.id)).revisionState).toBe('committed');
});
test('branchBeforeTurn rejects an unknown turn', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(16_800),
});
const session = await manager.createSession(makeInput({ name: 'Parent' }));
await drain(manager.sendMessage(session.id, { turnId: 'only', text: 'the one prompt' }));
let branchError: unknown;
try {
await manager.branchBeforeTurn(session.id, { sourceTurnId: 'nope' });
} catch (error) {
branchError = error;
}
expect(branchError instanceof Error ? branchError.message : String(branchError)).toContain(
'Cannot branch before unknown turn',
);
});
});
describe('SessionManager steering and followup queues', () => {
function steeringManager() {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
return { manager, store };
}
// Run a turn and invoke `duringFirstDelta` synchronously the first time the
// turn streams text — the point at which a real user would type while the
// agent works. Returns every streamed event.
async function runTurnWith(
manager: SessionManager,
sessionId: string,
turnId: string,
duringFirstDelta: () => void,
): Promise<SessionEvent[]> {
const events: SessionEvent[] = [];
let fired = false;
for await (const event of manager.sendMessage(sessionId, { turnId, text: 'hello' })) {
events.push(event);
if (!fired && event.type === 'text_delta') {
fired = true;
duringFirstDelta();
}
}
return events;
}
test('hosted root runs consume the Host owner and release it exactly once', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const identities: RuntimeMessageRunIdentity[] = [];
const acked: string[] = [];
const nacked: string[] = [];
let pulled = false;
let releases = 0;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
messageAuthority: {
bindRun: (identity) => {
identities.push(identity);
return {
...identity,
pull: () => {
if (pulled) return [];
pulled = true;
return [
{
id: 'host-lease-1',
messageId: 'host-message-1',
content: { text: 'host steer', displayText: 'visible host steer' },
},
];
},
ack: (leaseIds) => acked.push(...leaseIds),
nack: (leaseIds) => nacked.push(...leaseIds),
release: () => {
releases += 1;
},
};
},
},
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const events = await drainAll(
manager.sendMessage(session.id, { turnId: 'turn-host-message', text: 'start' }),
);
const [run] = await runStore.listSessionRuns(session.id);
expect(identities).toEqual([
{ sessionId: session.id, turnId: 'turn-host-message', runId: run?.runId },
]);
expect(acked).toEqual(['host-lease-1']);
expect(nacked).toEqual([]);
expect(releases).toBe(1);
expect(
events.some(
(event) =>
event.type === 'steering_message' &&
event.messageId === 'host-message-1' &&
event.content.displayText === 'visible host steer',
),
).toBe(true);
expect(events.some((event) => event.type === 'queue_update')).toBe(false);
for (const operation of [
() => manager.steer(session.id, 'runtime mirror'),
() => manager.queueMessage(session.id, 'runtime mirror'),
() => manager.drainFollowup(session.id),
() => manager.retractQueue(session.id),
]) {
let error: unknown;
try {
operation();
} catch (caught) {
error = caught;
}
expect(error instanceof RuntimeMessageAuthorityInvariantError).toBe(true);
}
});
test('hosted Interaction binds the durable Run identity and closes before release', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const identities: RuntimeInteractionRunIdentity[] = [];
const lifecycle: string[] = [];
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
interactionAuthority: {
bindRun: (identity) => {
identities.push(identity);
return {
...identity,
acceptSandboxBoundaryRequest: async () => {},
acceptUserQuestionRequest: async () => {},
close: async (reason) => {
lifecycle.push(`close:${reason}`);
},
release: () => lifecycle.push('release'),
};
},
},
canonicalPermissionOutcomes: noCanonicalPermissionOutcomes,
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drainAll(
manager.sendMessage(session.id, { turnId: 'turn-host-interaction', text: 'go' }),
);
const [run] = await runStore.listSessionRuns(session.id);
expect(identities).toEqual([
{ sessionId: session.id, turnId: 'turn-host-interaction', runId: run?.runId },
]);
expect(lifecycle).toEqual(['close:turn_terminal', 'release']);
});
test('hosted stopped question abandonment preserves stop closure before release', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(ctx));
const lifecycle: string[] = [];
let question: RuntimeUserQuestionContinuation | undefined;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
interactionAuthority: {
bindRun: (identity) => ({
...identity,
acceptSandboxBoundaryRequest: async () => {},
acceptUserQuestionRequest: async ({ continuation }) => {
question = continuation;
},
close: async (reason) => {
lifecycle.push(`close:${reason}`);
await question?.applyClosure(reason);
lifecycle.push('local-settled');
},
release: () => lifecycle.push('release'),
}),
},
canonicalPermissionOutcomes: noCanonicalPermissionOutcomes,
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'ask' }),
);
const iterator = manager
.sendMessage(session.id, {
turnId: 'turn-host-question-abandoned',
text: FAKE_ASK_USER_QUESTION_PROMPT,
})
[Symbol.asyncIterator]();
let request: SessionEvent | undefined;
while (request?.type !== 'user_question_request') {
const next = await iterator.next();
if (next.done) break;
request = next.value;
}
expect(request?.type).toBe('user_question_request');
await manager.stopSession(session.id, { source: 'stop_button' });
expect(lifecycle).toEqual(['close:turn_stopped', 'local-settled']);
await iterator.return?.(undefined);
expect(lifecycle).toEqual(['close:turn_stopped', 'local-settled', 'release']);
});
test('hosted RuntimeKernel rejects a backend request without an admission receipt', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new UnadmittedQuestionBackend(ctx));
let releases = 0;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
interactionAuthority: {
bindRun: (identity) => ({
...identity,
acceptSandboxBoundaryRequest: async () => {},
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {
releases += 1;
},
}),
},
canonicalPermissionOutcomes: noCanonicalPermissionOutcomes,
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'ask' }),
);
let failure: unknown;
try {
await drainAll(manager.sendMessage(session.id, { turnId: 'turn-forged', text: 'start' }));
} catch (error) {
failure = error;
}
expect(failure instanceof RuntimeInteractionInvariantError).toBe(true);
expect(releases).toBe(1);
});
test('hosted owner is released when backend execution fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ThrowBeforeTerminalBackend(ctx));
let releases = 0;
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
messageAuthority: {
bindRun: (identity) => ({
...identity,
pull: () => [],
ack: () => {},
nack: () => {},
release: () => {
releases += 1;
},
}),
},
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
let failure: unknown;
try {
await drainAll(
manager.sendMessage(session.id, { turnId: 'turn-host-failed', text: 'start' }),
);
} catch (error) {
failure = error;
}
expect((failure as Error).message).toBe('backend failed before terminal');
expect(releases).toBe(1);
});
test('a failed turn begin never leaks a steering owner', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let failBuilds = 1;
backends.register('fake', (ctx) => {
if (failBuilds > 0) {
failBuilds -= 1;
throw new Error('backend build failed');
}
return new FakeBackend(ctx);
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
let failed: unknown;
try {
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-fail',
text: 'hello',
})) {
// drain
}
} catch (error) {
failed = error;
}
expect((failed as Error).message).toBe('backend build failed');
// The failed begin must not have left a live owner: steering falls back
// instead of queueing a message no run will ever consume.
expect(manager.steer(session.id, 'orphaned')).toEqual({ kind: 'fallback' });
expect(manager.queueMessage(session.id, 'orphaned too')).toEqual({ kind: 'fallback' });
// A later successful turn establishes ownership normally.
let outcome: QueueEnqueueOutcome | undefined;
const events = await runTurnWith(manager, session.id, 'turn-2', () => {
outcome = manager.steer(session.id, 'now consumed');
});
expect(outcome?.kind).toBe('queued');
expect(
events.some(
(event) => event.type === 'steering_message' && event.content.text === 'now consumed',
),
).toBe(true);
});
test('an overlapping turn cannot drain steering queued for the current owner', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: GatedSteeringBackend | undefined;
backends.register('fake', (ctx) => {
backend = new GatedSteeringBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const first = drainAll(manager.sendMessage(session.id, { turnId: 'turn-a', text: 'first' }));
await waitUntil(() => backend?.gates.has('turn-a') === true);
const second = drainAll(manager.sendMessage(session.id, { turnId: 'turn-b', text: 'second' }));
await waitUntil(() => backend?.gates.has('turn-b') === true);
// turn-b established ownership last, so the steer targets it.
expect(manager.steer(session.id, 'for the owner').kind).toBe('queued');
// The stale turn's pull hook fails the identity check and drains nothing.
backend?.release('turn-a');
const firstEvents = await first;
expect(backend?.pulls.get('turn-a')).toEqual([[]]);
expect(firstEvents.some((event) => event.type === 'steering_message')).toBe(false);
// The owner drains exactly the queued message.
backend?.release('turn-b');
const secondEvents = await second;
expect(backend?.pulls.get('turn-b')).toEqual([['for the owner']]);
expect(
secondEvents.some(
(event) => event.type === 'steering_message' && event.content.text === 'for the owner',
),
).toBe(true);
});
test('a pulled lease is past the retract point: retract excludes it and it delivers exactly once', async () => {
// Round-5 F1/D1: pull() is the single atomic commit point. Once leased,
// the message belongs to this turn's delivery — a retract during the
// (slow) durable append returns only still-queued text, never the
// in-flight lease; otherwise the retracted text would ALSO be executed by
// the provider once the append lands (refill + execute = two copies).
const gate = makeGate();
const parked = makeGate();
class GatedRuntimeEventStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
parked.release();
await gate.promise;
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new GatedRuntimeEventStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
expect(manager.steer(sessionId, 'urgent steer').kind).toBe('queued');
},
);
const turnEvents: SessionEvent[] = [];
const turn = (async () => {
for await (const event of manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' })) {
turnEvents.push(event);
}
})();
await parked.promise;
// The steering append has not committed: the next provider request must
// not have started while the message is not durable.
await new Promise((resolve) => setTimeout(resolve, 25));
expect(model.doStreamCalls.length).toBe(1);
// Pulled means committed to this turn: retract returns nothing.
expect(manager.retractQueue(session.id)).toBe('');
gate.release();
await turn;
// The message delivered exactly once: in the next provider request…
expect(model.doStreamCalls.length).toBe(2);
expect(JSON.stringify(model.doStreamCalls[1]?.prompt).includes('urgent steer')).toBe(true);
// …echoed once in the stream/ledger…
expect(turnEvents.filter((event) => event.type === 'steering_message').length).toBe(1);
// …and owned by no queue afterwards.
expect(manager.drainFollowup(session.id)).toBe(null);
expect(manager.retractQueue(session.id)).toBe('');
});
test('an abort never converts a durably appended steering message into a redelivery', async () => {
// Round-5 F1/D3: abort does not settle a pushed lease — settlement is
// decided only by the persistence fact. Here the append is parked when
// the stop arrives; once it commits, the message belongs to the ledger
// (history replay presents it to the next turn) and must NOT also be
// nacked into the followup queue, which would put the same directive in
// the account twice.
const gate = makeGate();
const parked = makeGate();
class GatedRuntimeEventStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
parked.release();
await gate.promise;
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new GatedRuntimeEventStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
expect(manager.steer(sessionId, 'urgent steer').kind).toBe('queued');
},
);
const turn = (async () => {
try {
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'go',
})) {
// drain
}
} catch {
// the abort may end the stream abruptly
}
})();
await parked.promise;
void manager.stopSession(session.id, { source: 'stop_button' });
// Let the abort reach the backend's durability wait while the append is
// still parked — the exact window where an abort-settles-the-lease bug
// nacks a message that then also commits to the ledger.
await new Promise((resolve) => setTimeout(resolve, 25));
gate.release();
// Teardown converges: the parked append commits, the lease settles, and
// the aborted send terminates without hanging.
await turn;
// The dying request was never sent…
expect(model.doStreamCalls.length).toBe(1);
// …the ledger owns the message (exactly one durable steering event)…
const runs = await runStore.listSessionRuns(session.id);
const steeringEvents: RuntimeEvent[] = [];
for (const run of runs) {
const events = await runStore.readRuntimeEvents(session.id, run.runId);
steeringEvents.push(
...events.filter(
(event) =>
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true,
),
);
}
expect(steeringEvents.length).toBe(1);
// …and no queue redelivers it.
expect(manager.drainFollowup(session.id)).toBe(null);
expect(manager.retractQueue(session.id)).toBe('');
});
test('a nack that lands after the owner released folds into the followup queue, not an ownerless steering queue', async () => {
// Round-5 F3: turn A's append fails only after turn B took over and
// released. A's nack can no longer target A (it will never pull again) —
// the text's only safe home is the followup queue, exactly where a
// release-time fold would have put it.
const gate = makeGate();
const parked = makeGate();
class ParkThenFailStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
parked.release();
await gate.promise;
throw new Error('steering append failed');
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new ParkThenFailStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
manager.steer(sessionId, 'urgent steer');
},
);
const turnA = (async () => {
try {
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'go',
})) {
// drain
}
} catch {
// the failed append ends the stream abruptly
}
})();
await parked.promise;
// Turn B takes ownership and releases it while A is parked.
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-2',
text: 'second',
})) {
// drain
}
gate.release();
await turnA;
// The failed message is redeliverable exactly once, via followup.
expect(manager.drainFollowup(session.id)).toBe('urgent steer');
expect(manager.retractQueue(session.id)).toBe('');
});
test('steer falls back when no RuntimeEventStore is configured', async () => {
// Round-5 F4: without a runtime event ledger, the steering durability ack
// has nothing to anchor to — the fail-closed persist contract cannot be
// honored. The fallback path opens a fresh turn whose user message is
// persisted by the SessionStore, keeping the same durability guarantee.
const store = new MemorySessionStore();
const backends = new BackendRegistry();
let backend: GatedSteeringBackend | undefined;
backends.register('fake', (ctx) => {
backend = new GatedSteeringBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const turn = drainAll(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }));
await waitUntil(() => backend?.gates.has('turn-1') === true);
// A live turn exists, but steering cannot be made durable: fall back.
expect(manager.steer(session.id, 'no ledger')).toEqual({ kind: 'fallback' });
// Followups are unaffected — they open a normal turn anyway.
expect(manager.queueMessage(session.id, 'later').kind).toBe('queued');
backend?.gates.get('turn-1')?.release();
backend?.pullDone.get('turn-1')?.release();
await turn;
});
test('a failed steering append nacks the lease back to the queue and the request never carries it', async () => {
// Fail-CLOSED persistence: the steering append throws, the ack judgment
// propagates the failure (no fail-open swallow), the lease is nacked back
// to the queue (folded into followup at release), and neither the ledger
// nor the projection carries the undelivered message.
class FailingSteeringStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
throw new Error('steering append failed');
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new FailingSteeringStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
expect(manager.steer(sessionId, 'urgent steer').kind).toBe('queued');
},
);
let failed: unknown;
try {
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'go',
})) {
// drain
}
} catch (error) {
failed = error;
}
expect(failed instanceof Error).toBe(true);
// The dying request never carried the steering: no second provider call.
expect(model.doStreamCalls.length).toBe(1);
// Nacked back to the queue and folded into followup at release — the
// text is redeliverable, not lost.
expect(manager.drainFollowup(session.id)).toBe('urgent steer');
// Ledger and projection agree: the message was never persisted.
const messages = await manager.getMessages(session.id);
expect(
messages.some((message) => message.type === 'user' && message.text === 'urgent steer'),
).toBe(false);
});
test('an overlapping turn cannot turn a delivered lease into a followup redelivery', async () => {
// Round-4 V1: turn A leases the steer and parks in the (gated) durable
// append; turn B starts meanwhile and takes the owner slot. A's append
// then commits and A's provider request carries the message — so A's ack
// MUST still settle the lease (it is keyed by issuer, not by the current
// owner), and B's teardown must not fold A's in-flight lease into the
// followup queue, which would redeliver an already-executed directive.
const gate = makeGate();
const parked = makeGate();
class GatedRuntimeEventStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
parked.release();
await gate.promise;
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new GatedRuntimeEventStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
manager.steer(sessionId, 'urgent steer');
},
);
const turnAEvents: SessionEvent[] = [];
const turnA = (async () => {
try {
for await (const event of manager.sendMessage(session.id, {
turnId: 'turn-1',
text: 'go',
})) {
turnAEvents.push(event);
}
} catch {
// A gated teardown may end the stream abruptly.
}
})();
await parked.promise;
// Turn B runs to completion while A is parked mid-lease.
for await (const _event of manager.sendMessage(session.id, {
turnId: 'turn-2',
text: 'second',
})) {
// drain
}
expect(model.doStreamCalls.length).toBe(2);
gate.release();
await turnA;
// A's post-steer request went out carrying the directive exactly once…
expect(model.doStreamCalls.length).toBe(3);
expect(JSON.stringify(model.doStreamCalls[2]?.prompt).includes('urgent steer')).toBe(true);
// …B's request never did…
expect(JSON.stringify(model.doStreamCalls[1]?.prompt).includes('urgent steer')).toBe(false);
// …the ledger echoes it exactly once…
expect(turnAEvents.filter((event) => event.type === 'steering_message').length).toBe(1);
// …and NOTHING redelivers it: the delivered lease was acked by its
// issuer, so no queue still holds the text.
expect(manager.drainFollowup(session.id)).toBe(null);
expect(manager.retractQueue(session.id)).toBe('');
});
test('a backend-forged queue_update never reaches the ledger or observers', async () => {
// Round-6 R3: the kernel is the only legal producer of queue_update (it
// pushes them directly into the turn stream). A backend that yields one
// is forging authoritative queue state; the flow drops it at the ingress
// — not mapped, not forwarded, not persisted.
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ForgingQueueBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const events = await drainAll(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }),
);
// Nothing was enqueued in this turn, so ANY queue_update in the stream
// is the forged one leaking through.
expect(events.some((event) => event.type === 'queue_update')).toBe(false);
const runs = await runStore.listSessionRuns(session.id);
const runtimeEvents = (
await Promise.all(runs.map((run) => runStore.readRuntimeEvents(session.id, run.runId)))
).flat();
expect(
runtimeEvents.some(
(event) =>
(event.actions?.stateDelta as { queueUpdate?: unknown } | undefined)?.queueUpdate !==
undefined,
),
).toBe(false);
});
test('provider retry progress reaches observers without becoming a durable runtime fact', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ProviderRetryProgressBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const events = await drainAll(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }),
);
expect(
events.filter((event) => event.type === 'provider_retry').map((event) => event.phase),
).toEqual(['scheduled', 'started']);
const runs = await runStore.listSessionRuns(session.id);
const runtimeEvents = (
await Promise.all(runs.map((run) => runStore.readRuntimeEvents(session.id, run.runId)))
).flat();
expect(
runtimeEvents.some(
(event) =>
(event.actions?.stateDelta as { providerRetry?: unknown } | undefined)?.providerRetry !==
undefined,
),
).toBe(false);
});
test('an append error after the write landed settles by the ledger read-back, not a duplicate nack', async () => {
// Round-6 R5: appendRuntimeEvent can fail AFTER the bytes landed (e.g. a
// close error). Treating every append error as not-durable would nack a
// message the ledger already owns — history replay plus the followup
// redelivery equals a double. The ambiguous failure is settled by reading
// the ledger back: present ⇒ durable ⇒ ack path.
class WriteThenThrowStore extends MemoryAgentRunStore {
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
if (
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true
) {
await super.appendRuntimeEvent(sessionId, runId, event);
throw new Error('close failed after the write landed');
}
return super.appendRuntimeEvent(sessionId, runId, event);
}
}
const runStore = new WriteThenThrowStore();
const model = steeringToolThenDoneModel();
const { manager, session } = await steeringDeliverySession(
runStore,
model,
(manager, sessionId) => {
manager.steer(sessionId, 'urgent steer');
},
);
const turnEvents: SessionEvent[] = [];
for await (const event of manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' })) {
turnEvents.push(event);
}
// Delivered exactly once: the next request carries it…
expect(model.doStreamCalls.length).toBe(2);
expect(JSON.stringify(model.doStreamCalls[1]?.prompt).includes('urgent steer')).toBe(true);
// …the ledger owns exactly one copy…
const runs = await runStore.listSessionRuns(session.id);
const steeringEvents: RuntimeEvent[] = [];
for (const run of runs) {
const events = await runStore.readRuntimeEvents(session.id, run.runId);
steeringEvents.push(
...events.filter(
(event) =>
event.content?.kind === 'text' &&
(event.content as { steering?: boolean }).steering === true,
),
);
}
expect(steeringEvents.length).toBe(1);
// …and no queue redelivers it.
expect(manager.drainFollowup(session.id)).toBe(null);
expect(manager.retractQueue(session.id)).toBe('');
});
test('stranded steering emits a final queue snapshot when it folds into the followup queue', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
let backend: GatedSteeringBackend | undefined;
backends.register('fake', (ctx) => {
backend = new GatedSteeringBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const turn = drainAll(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }));
await waitUntil(() => backend?.gates.has('turn-1') === true);
backend?.gates.get('turn-1')?.release();
// The turn's only step boundary has already pulled (empty)…
await waitUntil(() => backend?.pulls.has('turn-1') === true);
// …so this steer is stranded: no step is left to consume it.
expect(manager.steer(session.id, 'late').kind).toBe('queued');
backend?.pullDone.get('turn-1')?.release();
const events = await turn;
// The stranded → followup migration is a queue change; the LAST snapshot
// in the stream reflects it, not the stale pre-fold state.
const updates = events.filter(
(event): event is Extract<SessionEvent, { type: 'queue_update' }> =>
event.type === 'queue_update',
);
expect(updates.at(-1)?.steering).toEqual([]);
expect(updates.at(-1)?.followup).toEqual(['late']);
// And the followup queue is the authoritative owner of the text.
expect(manager.drainFollowup(session.id)).toBe('late');
});
});
async function drainAll(iterable: AsyncIterable<SessionEvent>): Promise<SessionEvent[]> {
const events: SessionEvent[] = [];
for await (const event of iterable) events.push(event);
return events;
}
async function waitUntil(predicate: () => boolean | Promise<boolean>): Promise<void> {
for (let i = 0; i < 500 && !(await predicate()); i += 1) {
await new Promise((resolve) => setTimeout(resolve, 2));
}
expect(await predicate()).toBe(true);
}
/** Mock model: first request calls the Probe tool, second finishes with text. */
function steeringToolThenDoneModel(): MockLanguageModelV4 {
const usage = {
inputTokens: { total: 100, noCache: 100, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 10, text: 10, reasoning: 0 },
};
const model: MockLanguageModelV4 = new MockLanguageModelV4({
doStream: async () => {
const call = model.doStreamCalls.length;
const chunks: LanguageModelV4StreamPart[] =
call === 1
? [
{ type: 'stream-start', warnings: [] },
{
type: 'tool-call',
toolCallId: 'tool-1',
toolName: 'Probe',
input: JSON.stringify({ q: 'x' }),
},
{ type: 'finish', finishReason: { unified: 'tool-calls', raw: 'tool_calls' }, usage },
]
: [
{ type: 'stream-start', warnings: [] },
{ type: 'text-start', id: 'text-1' },
{ type: 'text-delta', id: 'text-1', delta: 'done' },
{ type: 'text-end', id: 'text-1' },
{ type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage },
];
return {
stream: simulateReadableStream({ chunks, initialDelayInMs: null, chunkDelayInMs: null }),
};
},
});
return model;
}
/**
* A SessionManager wired to a REAL AiSdkBackend over a mock model, so the
* full steering delivery chain (kernel lease -> backend durability wait ->
* AgentRun fail-closed persist) is exercised. `duringTool` runs inside the
* first step's tool execution — the moment a real user steers.
*/
async function steeringDeliverySession(
runStore: MemoryAgentRunStore,
model: MockLanguageModelV4,
duringTool: (manager: SessionManager, sessionId: string) => Promise<void> | void,
) {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
let manager!: SessionManager;
let sessionId = '';
backends.register('fake', (ctx) =>
createTestAiSdkBackend({
sessionId: ctx.sessionId,
header: ctx.header,
appendMessage: async () => {},
connection: {
slug: 'mock-main',
providerType: 'anthropic',
defaultModel: 'mock-model-id',
},
apiKey: 'sk-test',
modelId: 'mock-model-id',
modelFactory: () => model,
tools: [
{
name: 'Probe',
description: 'Probe description',
parameters: z.object({ q: z.string() }),
impl: async () => {
await duringTool(manager, sessionId);
return { ok: true };
},
},
],
loadTurnRuntimeEvents: ctx.loadTurnRuntimeEvents,
allowMidTurnHistoryCompaction: ctx.allowMidTurnHistoryCompaction,
newId: nextId(),
now: nextNow(1),
}),
);
const managerDeps = {
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(1_000),
};
manager = new SessionManager(managerDeps);
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
sessionId = session.id;
return { manager, session, store };
}
/**
* Parks each send behind a per-turn gate, pulls steering exactly once after
* release, then parks again behind a post-pull gate before finishing — a
* deterministic harness for the owner-identity rule and for enqueues that
* land after the final step boundary (stranded steering).
*/
class GatedSteeringBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
readonly gates = new Map<string, Gate>();
readonly pullDone = new Map<string, Gate>();
readonly pulls = new Map<string, string[][]>();
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const gate = makeGate();
const afterPull = makeGate();
this.gates.set(input.turnId, gate);
this.pullDone.set(input.turnId, afterPull);
await gate.promise;
const leases = input.pullSteering?.() ?? [];
const record = this.pulls.get(input.turnId) ?? [];
record.push(leases.map((lease) => lease.content.text));
this.pulls.set(input.turnId, record);
let seq = 0;
for (const lease of leases) {
seq += 1;
yield {
type: 'steering_message',
id: `${input.turnId}-steer-${seq}`,
turnId: input.turnId,
ts: seq,
messageId: lease.messageId,
content: lease.content,
};
}
// Delivery for this fake is the echo itself; ack the leases.
input.ackSteering?.(leases.map((lease) => lease.id));
await afterPull.promise;
yield {
type: 'text_complete',
id: `${input.turnId}-final`,
turnId: input.turnId,
ts: 10,
messageId: `${input.turnId}-m`,
text: 'ok',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 11,
stopReason: 'end_turn',
};
}
/** Release both of a turn's gates (start + post-pull). */
release(turnId: string): void {
this.gates.get(turnId)?.release();
this.pullDone.get(turnId)?.release();
}
async stop(): Promise<void> {
for (const turnId of this.gates.keys()) this.release(turnId);
}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class DelegatingRuntimeKernel implements RuntimeKernelLike {
readonly starts: Array<{
sessionId: string;
input: Parameters<RuntimeKernelLike['startTurn']>[1];
}> = [];
readonly stopped: string[] = [];
readonly permissionResponses: string[] = [];
activeRuns = false;
failNextDispose = false;
disposed: string[] = [];
cachedHeaders: SessionHeader[] = [];
constructor(private readonly events: readonly SessionEvent[] = []) {}
claimExecution(sessionId: string): ReturnType<RuntimeKernelLike['claimExecution']> {
const stopController = new AbortController();
return {
sessionId,
stopSignal: stopController.signal,
isStopRequested: () => false,
release: () => {},
};
}
async runSessionAdmissionMutation<T>(
_sessionIds: readonly string[],
operation: () => Promise<T> | T,
): Promise<T> {
return operation();
}
async runSessionQuiescentMutation<T>(
_sessionIds: readonly string[],
operation: () => Promise<T> | T,
): Promise<T> {
return operation();
}
async *startTurn(
sessionId: string,
input: Parameters<RuntimeKernelLike['startTurn']>[1],
): AsyncIterable<SessionEvent> {
this.starts.push({ sessionId, input });
for (const event of this.events) {
yield event;
}
}
async *startChildTurn(
sessionId: string,
input: Parameters<RuntimeKernelLike['startChildTurn']>[1],
): AsyncIterable<SessionEvent> {
this.starts.push({
sessionId,
input: {
turnId: input.turnId,
text: input.prompt,
parentRunId: input.parentRunId,
agentName: input.spec.name,
},
});
for (const event of this.events) {
yield event;
}
}
async *compactSession(
sessionId: string,
input: Parameters<RuntimeKernelLike['compactSession']>[1] = {},
): AsyncIterable<SessionEvent> {
this.starts.push({ sessionId, input: { turnId: input.turnId ?? 'compact-turn', text: '' } });
for (const event of this.events) {
yield event;
}
}
async preflightContextCompaction(): Promise<void> {}
async stopSession(sessionId: string): Promise<void> {
this.stopped.push(sessionId);
}
async respondToSandboxBoundary(
sessionId: string,
_response: Parameters<RuntimeKernelLike['respondToSandboxBoundary']>[1],
): Promise<void> {
this.permissionResponses.push(sessionId);
}
steer(): QueueEnqueueOutcome {
return { kind: 'fallback' };
}
queueMessage(): QueueEnqueueOutcome {
return { kind: 'fallback' };
}
drainFollowup(): string | null {
return null;
}
retractQueue(): string {
return '';
}
hasActiveRuns(): boolean {
return this.activeRuns;
}
updateCachedHeader(_sessionId: string, header: SessionHeader): void {
this.cachedHeaders.push(header);
}
async invalidateBackend(sessionId: string): Promise<void> {
if (!this.activeRuns) this.disposed.push(sessionId);
}
async invalidateCachedBackends(): Promise<void> {
// This test double does not retain backend instances between calls.
}
async disposeBackend(sessionId: string): Promise<void> {
if (this.failNextDispose) {
this.failNextDispose = false;
throw new Error('backend disposal failed');
}
this.disposed.push(sessionId);
}
}
class UnadmittedQuestionBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'user_question_request',
id: 'unadmitted-event',
turnId: input.turnId,
ts: 1,
requestId: 'unadmitted-request',
toolUseId: 'unadmitted-tool',
questions: [
{
question: 'Continue?',
options: [{ label: 'Yes' }, { label: 'No' }],
},
],
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(): Promise<void> {}
async dispose(): Promise<void> {}
}
class TestBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
readonly sendInputs: BackendSendInput[] = [];
stopCalls = 0;
readonly stopModes: BackendStopMode[] = [];
constructor(
private readonly ctx: BackendFactoryContext,
private readonly gate?: Gate,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.sendInputs.push(input);
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'ok',
};
await this.gate?.promise;
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'end_turn',
};
}
async stop(
_reason: 'user_stop' | 'redirect',
mode: BackendStopMode = 'immediate',
): Promise<void> {
this.stopCalls += 1;
this.stopModes.push(mode);
}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {
if (this.ctx.store instanceof MemorySessionStore) {
this.ctx.store.disposeCount += 1;
}
}
}
class PermissionBroadcastBackend extends TestBackend {
permissionResponses = 0;
override async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {
this.permissionResponses += 1;
}
}
class RetryStopBackend extends TestBackend {
constructor(
ctx: BackendFactoryContext,
private readonly stopGate: Gate,
) {
super(ctx, stopGate);
}
override async stop(): Promise<void> {
this.stopCalls += 1;
if (this.stopCalls === 1) throw new Error('first stop failed');
this.stopGate.release();
}
}
class ConcurrentStopBackend extends TestBackend {
constructor(
ctx: BackendFactoryContext,
private readonly stopStarted: Gate,
private readonly releaseStop: Gate,
) {
super(ctx, releaseStop);
}
override async stop(): Promise<void> {
this.stopCalls += 1;
this.stopStarted.release();
await this.releaseStop.promise;
}
}
class CountingStopBackend extends TestBackend {
override async stop(): Promise<void> {
this.stopCalls += 1;
}
}
class FinalTextTestBackend extends TestBackend {
override async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.sendInputs.push(input);
yield {
type: 'text_complete',
id: `${input.turnId}-final`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'ok',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'end_turn',
};
}
}
class CountingFinalTextBackend extends FinalTextTestBackend {
constructor(
ctx: BackendFactoryContext,
private readonly onSend: () => void,
) {
super(ctx);
}
override async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.onSend();
yield* super.send(input);
}
}
class CompactingTestBackend extends TestBackend {
constructor(
ctx: BackendFactoryContext,
private readonly compactCalls: Array<{ turnId: string; runtimeContextCount: number }>,
) {
super(ctx);
}
async compactHistory(input: { turnId: string; runtimeContext: readonly RuntimeEvent[] }) {
this.compactCalls.push({
turnId: input.turnId,
runtimeContextCount: input.runtimeContext.length,
});
return compactHistoryResult();
}
}
class LifecycleChildBackend extends CompactingTestBackend {
override async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.sendInputs.push(input);
yield {
type: 'tool_start',
id: `${input.turnId}-tool-start`,
turnId: input.turnId,
ts: 1,
toolUseId: `${input.turnId}-read`,
toolName: 'Read',
args: { path: 'README.md' },
};
yield {
type: 'tool_result',
id: `${input.turnId}-tool-result`,
turnId: input.turnId,
ts: 2,
toolUseId: `${input.turnId}-read`,
isError: false,
content: { kind: 'text', text: 'README body' },
};
yield {
type: 'token_usage',
id: `${input.turnId}-usage`,
turnId: input.turnId,
ts: 3,
input: 10,
output: 5,
total: 15,
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
}
class BlockingCompactBackend extends TestBackend {
stopCalls = 0;
constructor(
ctx: BackendFactoryContext,
private readonly options: {
compactGate: Gate;
onCompactStart: (backend: BlockingCompactBackend) => void;
},
) {
super(ctx);
}
async compactHistory(_input: { turnId: string; runtimeContext: readonly RuntimeEvent[] }) {
this.options.onCompactStart(this);
await this.options.compactGate.promise;
return compactHistoryResult();
}
override async stop(): Promise<void> {
this.stopCalls += 1;
}
}
class FailOpenCompactingBackend extends TestBackend {
async compactHistory(_input: { turnId: string; runtimeContext: readonly RuntimeEvent[] }) {
return compactHistoryFailOpenResult();
}
}
class ActiveTurnBackend extends TestBackend {
constructor(
ctx: BackendFactoryContext,
private readonly options: {
turnStarted: Gate;
sendGate: Gate;
compactCalls: Array<{ turnId: string; runtimeContextCount: number }>;
},
) {
super(ctx, options.sendGate);
}
override async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.options.turnStarted.release();
yield* super.send(input);
}
async compactHistory(input: { turnId: string; runtimeContext: readonly RuntimeEvent[] }) {
this.options.compactCalls.push({
turnId: input.turnId,
runtimeContextCount: input.runtimeContext.length,
});
return compactHistoryResult();
}
}
function compactHistoryResult() {
return {
contextBudget: {
enabled: true,
policyName: 'unit-budget',
estimatedTokensBefore: 1000,
estimatedTokensAfter: 400,
keptTurns: 1,
droppedTurns: 1,
keptEvents: 1,
droppedEvents: 1,
compactionDecisions: [
{
stage: 'priorReplay' as const,
sourceKind: 'runtimeEvents' as const,
decision: 'replaced' as const,
boundaryKind: 'historyCompact',
estimatedTokensSaved: 600,
},
],
},
};
}
function compactHistoryFailOpenResult() {
return {
contextBudget: {
enabled: true,
policyName: 'unit-budget',
estimatedTokensBefore: 1000,
estimatedTokensAfter: 400,
keptTurns: 1,
droppedTurns: 1,
keptEvents: 1,
droppedEvents: 1,
historyCompactWriteFailures: 1,
compactionDecisions: [
{
stage: 'priorReplay' as const,
sourceKind: 'runtimeEvents' as const,
decision: 'failedOpen' as const,
boundaryKind: 'historyCompact',
failOpenReason: 'write_failed',
},
],
},
};
}
class HighVolumeDeltaBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
ctx: BackendFactoryContext,
private readonly chunkCount: number,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const messageId = `${input.turnId}-m`;
for (let index = 0; index < this.chunkCount; index += 1) {
yield {
type: 'text_delta',
id: `${input.turnId}-delta-${index}`,
turnId: input.turnId,
ts: index + 1,
messageId,
text: `chunk-${String(index).padStart(3, '0')}:${'x'.repeat(32)}\n`,
};
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: this.chunkCount + 1,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class TextCompleteBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const messageId = `${input.turnId}-m`;
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 7_000,
messageId,
text: 'ok',
};
yield {
type: 'text_complete',
id: `${input.turnId}-text-complete`,
turnId: input.turnId,
ts: 7_001,
messageId,
text: 'ok',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 7_002,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class LateErrorBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly gate: Gate,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'ok',
};
await this.gate.promise;
yield {
type: 'error',
id: `${input.turnId}-error`,
turnId: input.turnId,
ts: 2,
recoverable: false,
reason: 'late_error',
message: 'late backend error',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {
if (this.ctx.store instanceof MemorySessionStore) {
this.ctx.store.disposeCount += 1;
}
}
}
class StopControlledAbortBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
private releaseAbort: () => void = () => {};
private releaseStop: () => void = () => {};
private readonly abortGate = new Promise<void>((resolve) => {
this.releaseAbort = resolve;
});
private readonly stopGate = new Promise<void>((resolve) => {
this.releaseStop = resolve;
});
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'ok',
};
await this.abortGate;
yield {
type: 'abort',
id: `${input.turnId}-abort`,
turnId: input.turnId,
ts: 2,
reason: 'user_stop',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'user_stop',
};
}
async stop(): Promise<void> {
this.releaseAbort();
await this.stopGate;
}
allowStopReturn(): void {
this.releaseStop();
}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
type PartialEvent =
| Omit<Extract<SessionEvent, { type: 'text_delta' }>, 'id' | 'turnId' | 'ts'>
| Omit<Extract<SessionEvent, { type: 'permission_request' }>, 'id' | 'turnId' | 'ts'>
| Omit<Extract<SessionEvent, { type: 'complete' }>, 'id' | 'turnId' | 'ts'>
| Omit<Extract<SessionEvent, { type: 'error' }>, 'id' | 'turnId' | 'ts'>
| Omit<Extract<SessionEvent, { type: 'abort' }>, 'id' | 'turnId' | 'ts'>;
class TurnScriptBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
readonly sendInputs: BackendSendInput[] = [];
constructor(
private readonly ctx: BackendFactoryContext,
private readonly turns: PartialEvent[][],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.sendInputs.push(input);
const events = this.turns[this.sendInputs.length - 1] ?? [
{ type: 'text_delta', messageId: `${input.turnId}-m`, text: 'ok' },
{ type: 'complete', stopReason: 'end_turn' },
];
let index = 0;
for (const event of events) {
index += 1;
yield {
...event,
id: `${input.turnId}-${index}`,
turnId: input.turnId,
ts: index,
} as SessionEvent;
}
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class EventBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly events: PartialEvent[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
let index = 0;
for (const event of this.events) {
index += 1;
yield {
...event,
id: `${input.turnId}-${index}`,
turnId: input.turnId,
ts: index,
} as SessionEvent;
}
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class SandboxBoundaryWaitBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
readonly responses: SandboxBoundaryResponse[] = [];
private readonly responseGate = makeGate();
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'sandbox_boundary_request',
id: `${input.turnId}-request`,
turnId: input.turnId,
ts: 1,
requestId: 'boundary-1',
toolUseId: 'tool-1',
justification: 'Write the requested export.',
expansion: {
filesystem: {
entries: [{ path: '/tmp/export.txt', access: 'write', scope: 'exact' }],
},
},
};
await this.responseGate.promise;
const response = this.responses[0]!;
yield {
type: 'sandbox_boundary_decision_ack',
id: `${input.turnId}-decision`,
turnId: input.turnId,
ts: 2,
requestId: response.requestId,
toolUseId: 'tool-1',
decision: response.decision,
status: response.decision === 'allow' ? 'approved' : 'denied',
revision: response.decision === 'allow' ? 1 : 0,
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {
this.responseGate.release();
}
async respondToSandboxBoundary(response: SandboxBoundaryResponse): Promise<void> {
this.responses.push(response);
this.responseGate.release();
}
async dispose(): Promise<void> {}
}
/**
* Emits a SessionEvent variant the mapping has never been taught, wrapped in a
* turn that produces a real assistant message. That is what a future variant
* looks like from the read model's side: AiSdkFlow's exhaustiveness guard turns
* it into a control-only RuntimeEvent, and the ledger keeps it.
*/
class UnmappedSessionEventBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'text_complete',
id: `${input.turnId}-text`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-message`,
text: 'hi back',
};
yield {
type: 'not_yet_mapped',
id: `${input.turnId}-unmapped`,
turnId: input.turnId,
ts: 2,
} as unknown as SessionEvent;
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(): Promise<void> {}
async dispose(): Promise<void> {}
}
class ThrowAfterTerminalBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'before',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 2,
stopReason: 'end_turn',
};
throw new Error('cleanup after terminal failed');
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ThrowBeforeTerminalBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 1,
messageId: `${input.turnId}-m`,
text: 'before',
};
throw new Error('backend failed before terminal');
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class PartialAbortBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(private readonly ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
await this.ctx.store.appendMessage(this.sessionId, {
type: 'assistant',
id: `${input.turnId}-assistant`,
turnId: input.turnId,
ts: 12_001,
text: 'partial answer',
modelId: 'fake-model',
});
yield {
type: 'abort',
id: `${input.turnId}-abort`,
turnId: input.turnId,
ts: 12_002,
reason: 'user_stop',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class TraceBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(private readonly ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.ctx.recordRunTrace?.({
id: `${input.turnId}-trace-start`,
sessionId: this.sessionId,
turnId: input.turnId,
ts: 1,
phase: 'model',
type: 'model_stream_started',
message: 'Model stream started with Bearer sk-live-secret-token-value',
data: {
activeTools: ['Read'],
credential: 'sk-live-secret-token-value',
},
});
yield {
type: 'text_delta',
id: `${input.turnId}-delta`,
turnId: input.turnId,
ts: 2,
messageId: `${input.turnId}-m`,
text: 'ok',
};
this.ctx.recordRunTrace?.({
id: `${input.turnId}-trace-usage`,
sessionId: this.sessionId,
turnId: input.turnId,
ts: 3,
phase: 'model',
type: 'model_stream_completed',
message: 'Model stream completed',
data: { finishReason: 'stop' },
});
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ProviderRequestTraceBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(private readonly ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
await this.ctx.recordProviderRequestCapture?.({
schemaVersion: 2,
traceId: 'provider-trace-1',
captureId: 'capture-1',
turnId: input.turnId,
step: 0,
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:request',
requestPayloadWithoutProviderOptionsHash: 'sha256:shared-request',
requestBytes: 100,
segments: [],
artifactId: 'artifact-capture',
});
await this.ctx.recordProviderRequestAttempt?.({
traceId: 'provider-trace-1',
attemptId: 'attempt-1',
turnId: input.turnId,
step: 0,
attempt: 1,
captureId: 'capture-1',
captureArtifactId: 'artifact-capture',
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:request',
requestBytes: 100,
segments: Array.from({ length: 75 }, (_, index) => ({
kind: 'message' as const,
index,
cacheable: true,
hash: `sha256:${index}`,
bytes: 1,
})),
startedAt: 1,
completedAt: 2,
status: 'completed',
finishReason: 'stop',
latencyMs: 1,
});
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ProviderCaptureGateBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly dispatch: () => void,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
await this.ctx.recordProviderRequestCapture?.({
schemaVersion: 2,
traceId: 'provider-trace-gated',
captureId: 'capture-gated',
turnId: input.turnId,
step: 0,
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:gated',
requestPayloadWithoutProviderOptionsHash: 'sha256:shared-gated',
requestBytes: 100,
segments: [],
artifactId: 'artifact-gated',
});
this.dispatch();
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ProviderCaptureAfterAttemptFailureBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly attemptFailureRecorded: Promise<void>,
private readonly captureOutcomes: string[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
await this.ctx.recordProviderRequestAttempt?.({
traceId: 'provider-trace-1',
attemptId: 'attempt-1',
turnId: input.turnId,
step: 0,
attempt: 1,
captureId: 'capture-1',
captureArtifactId: 'artifact-capture-1',
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:request-1',
requestBytes: 100,
segments: [],
startedAt: 1,
completedAt: 2,
status: 'completed',
latencyMs: 1,
});
await this.attemptFailureRecorded;
try {
await this.ctx.recordProviderRequestCapture?.({
schemaVersion: 2,
traceId: 'provider-trace-1',
captureId: 'capture-2',
turnId: input.turnId,
step: 1,
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:request-2',
requestPayloadWithoutProviderOptionsHash: 'sha256:shared-request-2',
requestBytes: 120,
segments: [],
artifactId: 'artifact-capture-2',
});
this.captureOutcomes.push('fulfilled');
} catch {
this.captureOutcomes.push('rejected');
}
await this.ctx.recordProviderRequestAttempt?.({
traceId: 'provider-trace-1',
attemptId: 'attempt-2',
turnId: input.turnId,
step: 1,
attempt: 1,
captureId: 'capture-2',
captureArtifactId: 'artifact-capture-2',
providerId: 'fake',
modelId: 'fake-model',
requestHash: 'sha256:request-2',
requestBytes: 120,
segments: [],
startedAt: 2,
completedAt: 3,
status: 'completed',
latencyMs: 1,
});
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ActiveCompactBlockBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(private readonly ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
this.ctx.recordActiveFullCompactBlock?.(
activeCompactBlockFixture(this.sessionId, input.turnId),
);
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class HistoryCompactCheckpointBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(private readonly ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const checkpoint = buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents: [
{
id: 'source-event',
sessionId: this.sessionId,
runId: 'source-run',
turnId: 'source-turn',
invocationId: 'source-invocation',
ts: 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'source' },
},
],
summary: 'persist the bounded checkpoint',
});
this.ctx.recordHistoryCompactCheckpoint?.(
{ ...checkpoint, checkpointId: 'hcheckpoint-test' },
input.turnId,
);
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class HistoryCompactCheckpointCacheProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly observedCheckpointIds: Array<string | undefined>,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const loaded = await this.ctx.loadHistoryCompactCheckpoint?.();
this.observedCheckpointIds.push(loaded?.checkpointId);
if (input.turnId === 'parent-turn') {
const checkpoint = buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents: [
{
id: 'shared-source-event',
sessionId: this.sessionId,
runId: 'shared-source-run',
turnId: 'shared-source-turn',
invocationId: 'shared-source-invocation',
ts: 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'source' },
},
],
summary: 'share this checkpoint across session backends',
});
this.ctx.recordHistoryCompactCheckpoint?.(
{ ...checkpoint, checkpointId: 'hcheckpoint-shared' },
input.turnId,
);
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class HistoryCompactCheckpointMonotonicProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly observedCoverage: Array<number | undefined>,
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const loaded = await this.ctx.loadHistoryCompactCheckpoint?.();
this.observedCoverage.push(loaded?.coverage.eventCount);
const coverage =
input.turnId === 'parent-furthest' ? 2 : input.turnId === 'child-stale' ? 1 : 0;
if (coverage > 0) {
const coveredRuntimeEvents = Array.from(
{ length: coverage },
(_, index): RuntimeEvent => ({
id: `monotonic-source-event-${index}`,
sessionId: this.sessionId,
runId: `monotonic-source-run-${index}`,
turnId: `monotonic-source-turn-${index}`,
invocationId: `monotonic-source-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
);
await this.ctx
.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: `${input.turnId} checkpoint`,
}),
input.turnId,
)
.catch(() => {});
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class SameCoverageCheckpointReplacementProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly writeOutcomes: string[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const current = await this.ctx.loadHistoryCompactCheckpoint?.();
const coveredRuntimeEvents: RuntimeEvent[] = [
{
id: 'same-coverage-source-event',
sessionId: this.sessionId,
runId: 'same-coverage-source-run',
turnId: 'same-coverage-source-turn',
invocationId: 'same-coverage-source-invocation',
ts: 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'same source' },
},
];
try {
await this.ctx.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: `${input.turnId} summary`,
...(current ? { previousCheckpointId: current.checkpointId } : {}),
}),
input.turnId,
);
this.writeOutcomes.push(`${input.turnId}:fulfilled`);
} catch {
this.writeOutcomes.push(`${input.turnId}:rejected`);
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class SerializedCheckpointProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly childRecorderCalled: Gate,
private readonly recorderReturnedPromises: boolean[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const coverage = input.turnId === 'parent-delayed' ? 1 : 2;
const coveredRuntimeEvents = Array.from(
{ length: coverage },
(_, index): RuntimeEvent => ({
id: `serialized-source-event-${index}`,
sessionId: this.sessionId,
runId: `serialized-source-run-${index}`,
turnId: `serialized-source-turn-${index}`,
invocationId: `serialized-source-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
);
const write = this.ctx.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: `${input.turnId} checkpoint`,
}),
input.turnId,
);
if (input.turnId === 'child-furthest') {
this.recorderReturnedPromises.push(
Boolean(write && typeof (write as PromiseLike<void>).then === 'function'),
);
this.childRecorderCalled.release();
}
await write;
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class RecoveringCheckpointWriteProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly childRecorderCalled: Gate,
private readonly observedCoverage: Array<number | undefined>,
private readonly writeOutcomes: string[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
const loaded = await this.ctx.loadHistoryCompactCheckpoint?.();
this.observedCoverage.push(loaded?.coverage.eventCount);
if (input.turnId !== 'child-observe') {
const coverage = input.turnId === 'parent-failing' ? 1 : 2;
const coveredRuntimeEvents = Array.from(
{ length: coverage },
(_, index): RuntimeEvent => ({
id: `recovering-source-event-${index}`,
sessionId: this.sessionId,
runId: `recovering-source-run-${index}`,
turnId: `recovering-source-turn-${index}`,
invocationId: `recovering-source-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
);
const write = this.ctx.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: `${input.turnId} checkpoint`,
}),
input.turnId,
);
if (input.turnId === 'child-succeeding') this.childRecorderCalled.release();
try {
await write;
this.writeOutcomes.push(`${input.turnId}:fulfilled`);
} catch {
this.writeOutcomes.push(`${input.turnId}:rejected`);
}
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class CheckpointRecorderContractProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly beforeRecord: (turnId: string) => Promise<void>,
private readonly writeOutcomes: string[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
await this.beforeRecord(input.turnId);
const coverage = input.turnId === 'parent-furthest-in-flight' ? 2 : 1;
const coveredRuntimeEvents = Array.from(
{ length: coverage },
(_, index): RuntimeEvent => ({
id: `contract-source-event-${index}`,
sessionId: this.sessionId,
runId: `contract-source-run-${index}`,
turnId: `contract-source-turn-${index}`,
invocationId: `contract-source-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
);
try {
await this.ctx.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: `${input.turnId} checkpoint`,
}),
input.turnId,
);
this.writeOutcomes.push(`${input.turnId}:fulfilled`);
} catch {
this.writeOutcomes.push(`${input.turnId}:rejected`);
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class InitialCheckpointLoadRaceProbeBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(
private readonly ctx: BackendFactoryContext,
private readonly staleRecorderCalled: Gate,
private readonly observedCoverage: Array<number | undefined>,
private readonly writeOutcomes: string[],
) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
if (input.turnId === 'parent-initial-load') {
const loaded = await this.ctx.loadHistoryCompactCheckpoint?.();
this.observedCoverage.push(loaded?.coverage.eventCount);
} else {
const coveredRuntimeEvents = Array.from(
{ length: 5 },
(_, index): RuntimeEvent => ({
id: `stale-load-race-event-${index}`,
sessionId: this.sessionId,
runId: `stale-load-race-run-${index}`,
turnId: `stale-load-race-turn-${index}`,
invocationId: `stale-load-race-invocation-${index}`,
ts: index + 1,
partial: false,
role: 'user',
author: 'user',
content: { kind: 'text', text: `source ${index}` },
}),
);
const write = this.ctx.recordHistoryCompactCheckpoint?.(
buildHistoryCompactCheckpoint({
sessionId: this.sessionId,
coveredRuntimeEvents,
summary: 'stale checkpoint during initial load',
}),
input.turnId,
);
this.staleRecorderCalled.release();
try {
await write;
this.writeOutcomes.push(`${input.turnId}:fulfilled`);
} catch {
this.writeOutcomes.push(`${input.turnId}:rejected`);
}
}
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
function activeCompactBlockFixture(sessionId: string, turnId: string): ActiveFullCompactBlock {
return {
kind: 'maka.active_full_compact_block',
version: 1,
blockId: 'afcompact-test',
sessionId,
turnId,
createdAt: 12_775,
highWaterName: 'test-high-water',
highWaterSeq: 2,
trigger: {
reason: 'high_water',
stepNumber: 2,
estimatedTokensBefore: 100,
thresholdTokens: 50,
},
coverage: {
turnIds: [turnId],
runtimeEventIds: ['runtime-1'],
providerMessageSourceIds: ['provider-message:0'],
toolCallIds: ['tool-1'],
contentKinds: ['text'],
bodySha256: ['a'.repeat(64)],
},
summary: {
schemaVersion: 1,
text: 'persist the full active compact block',
processState: ['qemu-system-x86_64 pid=4242'],
},
limitations: ['test fixture'],
sourceRefs: [
{
kind: 'runtime_event',
sourceId: 'provider-message:0',
messageIndex: 0,
sessionId,
turnId,
runtimeEventId: 'runtime-1',
toolCallId: 'tool-1',
contentKind: 'text',
bodySha256: 'a'.repeat(64),
},
],
estimatedTokens: 42,
};
}
class MemorySessionStore implements SessionStore {
private headers = new Map<string, SessionHeader>();
private messages = new Map<string, StoredMessage[]>();
private executionBoundaries = new Map<string, ExecutionBoundary>();
private sandboxBoundaryRequests = new Map<string, SandboxBoundaryRequest>();
readonly failReadMessagesFor = new Set<string>();
readonly failNextReadMessagesFor = new Map<string, number>();
readonly failListTurnsFor = new Set<string>();
readonly failUpdateHeaderFor = new Set<string>();
readonly interleaveBeforeMarkSessionReadWriteFor = new Map<string, () => Promise<void> | void>();
failNextAppendMessage: ((message: StoredMessage) => boolean) | undefined;
failAfterNextAppendMessage: ((message: StoredMessage) => boolean) | undefined;
disposeCount = 0;
nextReadHeaderGate: { started: Gate; release: Gate } | undefined;
nextGraphOperatorProvisionGate: { started: Gate; release: Gate } | undefined;
generatedTitleAttempted: Gate | undefined;
async createSubagent(
input: CreateSessionInput,
initialBoundary?: ExecutionBoundary,
): Promise<{ header: SessionHeader; created: boolean }> {
const parent = input.subagentParent;
const spawn = input.subagentSpawn;
if (!parent || !input.subagentRuntime || !spawn) {
throw new Error('Missing child-session metadata');
}
const existing = Array.from(this.headers.values()).find((header) => {
const candidate = header.subagentParent;
return (
candidate?.parentSessionId === parent.parentSessionId &&
candidate.spawnedBy.parentRunId === parent.spawnedBy.parentRunId &&
candidate.spawnedBy.toolCallId === parent.spawnedBy.toolCallId &&
candidate.swarm?.swarmId === parent.swarm?.swarmId &&
candidate.swarm?.itemId === parent.swarm?.itemId
);
});
if (existing) {
if (
existing.subagentSpawn?.requestFingerprint !== spawn.requestFingerprint ||
existing.subagentParent?.spawnedBy.parentTurnId !== parent.spawnedBy.parentTurnId
) {
throw new Error('Child-session spawn identity was reused for different work');
}
return { header: existing, created: false };
}
return { header: await this.create(input, initialBoundary), created: true };
}
async createAgentGraphOperator(
input: CreateSessionInput,
request: AgentGraphOperatorProvisionRequest,
_expectedRevision: number,
initialBoundary?: ExecutionBoundary,
): Promise<{ header: SessionHeader } & AgentGraphOperatorProvisionResult> {
const gate = this.nextGraphOperatorProvisionGate;
if (gate) {
this.nextGraphOperatorProvisionGate = undefined;
gate.started.release();
await gate.release.promise;
}
const header = await this.create(input, initialBoundary);
return {
header,
created: true,
provision: {
...request,
edges: request.edges.map((edge) => ({ ...edge })),
targetSessionId: header.id,
provisionedAt: 1,
},
};
}
async create(
input: CreateSessionInput,
initialBoundary?: ExecutionBoundary,
): Promise<SessionHeader> {
const header: SessionHeader = {
id: `session-${this.headers.size + 1}`,
workspaceRoot: '/tmp/workspace',
cwd: input.cwd,
...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
createdAt: 1,
lastUsedAt: 1,
name: input.name ?? 'New Chat',
titleIsManual: false,
isFlagged: false,
labels: input.labels ?? [],
isArchived: false,
status: input.status ?? 'active',
...(input.blockedReason ? { blockedReason: input.blockedReason } : {}),
statusUpdatedAt: 1,
...(input.parentSessionId ? { parentSessionId: input.parentSessionId } : {}),
...(input.branchOfTurnId ? { branchOfTurnId: input.branchOfTurnId } : {}),
...(input.subagentParent ? { subagentParent: input.subagentParent } : {}),
...(input.subagentRuntime ? { subagentRuntime: input.subagentRuntime } : {}),
...(input.subagentSpawn ? { subagentSpawn: input.subagentSpawn } : {}),
...(input.subagentWorkspace ? { subagentWorkspace: input.subagentWorkspace } : {}),
...(input.revisionRootSessionId
? { revisionRootSessionId: input.revisionRootSessionId }
: {}),
...(input.revisionParentSessionId
? { revisionParentSessionId: input.revisionParentSessionId }
: {}),
...(input.revisionOfTurnId ? { revisionOfTurnId: input.revisionOfTurnId } : {}),
...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}),
...(input.revisionState ? { revisionState: input.revisionState } : {}),
hasUnread: false,
backend: input.backend,
llmConnectionSlug: input.llmConnectionSlug,
connectionLocked: false,
model: input.model ?? 'fake-model',
...(input.thinkingLevel !== undefined ? { thinkingLevel: input.thinkingLevel } : {}),
permissionMode: input.permissionMode,
collaborationMode: input.collaborationMode ?? 'agent',
orchestrationMode: input.orchestrationMode ?? 'default',
schemaVersion: 1,
};
this.headers.set(header.id, header);
this.messages.set(header.id, []);
this.executionBoundaries.set(
header.id,
initialBoundary
? { ...initialBoundary, revision: 0 }
: createGenesisExecutionBoundary(header.permissionMode),
);
return header;
}
async setExecutionBoundaryKind(
sessionId: string,
kind: 'managed' | 'bypass',
projection?: {
permissionMode: SessionHeader['permissionMode'];
labels?: readonly string[];
},
) {
const current = await this.readHeader(sessionId);
const permissionMode =
projection?.permissionMode ??
(kind === 'bypass'
? 'bypass'
: current.permissionMode === 'bypass'
? 'ask'
: current.permissionMode);
await this.updateHeader(sessionId, {
permissionMode,
...(projection?.labels ? { labels: [...projection.labels] } : {}),
});
const boundary = createGenesisExecutionBoundary(permissionMode);
this.executionBoundaries.set(sessionId, boundary);
return boundary;
}
async readExecutionBoundary(sessionId: string): Promise<ExecutionBoundary> {
const boundary = this.executionBoundaries.get(sessionId);
if (!boundary) throw new Error(`Unknown session ${sessionId}`);
return boundary;
}
async createSandboxBoundaryRequest(
input: CreateSandboxBoundaryRequest,
): Promise<SandboxBoundaryRequest> {
const boundary = await this.readExecutionBoundary(input.sessionId);
const request: SandboxBoundaryRequest = {
...input,
status: 'pending',
baseRevision: boundary.revision,
createdAt: 1,
};
this.sandboxBoundaryRequests.set(`${input.sessionId}:${input.requestId}`, request);
return request;
}
async listPendingSandboxBoundaryRequests(sessionId: string): Promise<SandboxBoundaryRequest[]> {
return [...this.sandboxBoundaryRequests.values()].filter(
(request) => request.sessionId === sessionId && request.status === 'pending',
);
}
async listSandboxBoundaryRestartClosures(sessionId: string): Promise<SandboxBoundaryRequest[]> {
return [...this.sandboxBoundaryRequests.values()].filter(
(request) => request.sessionId === sessionId && isSandboxBoundaryRestartClosure(request),
);
}
async settleSandboxBoundaryRequest(
input: SettleSandboxBoundaryRequest,
): Promise<SandboxBoundarySettlement> {
const key = `${input.sessionId}:${input.requestId}`;
const request = this.sandboxBoundaryRequests.get(key);
if (!request) throw new Error(`Unknown sandbox boundary request ${input.requestId}`);
const settled =
request.status === 'pending'
? {
...request,
status: input.decision === 'allow' ? ('approved' as const) : ('denied' as const),
settledAt: 2,
...(input.closureReason ? { outcomeReason: input.closureReason } : {}),
}
: request;
this.sandboxBoundaryRequests.set(key, settled);
return {
request: settled,
boundary: await this.readExecutionBoundary(input.sessionId),
changed: false,
};
}
async list(_filter?: SessionListFilter): Promise<SessionSummary[]> {
return Array.from(this.headers.values()).map(headerToSummary);
}
async readHeader(sessionId: string): Promise<SessionHeader> {
const gate = this.nextReadHeaderGate;
if (gate) {
this.nextReadHeaderGate = undefined;
gate.started.release();
await gate.release.promise;
}
const header = this.headers.get(sessionId);
if (!header) {
const error = new Error(`Unknown session ${sessionId}`) as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
}
await this.runMarkSessionReadInterleave(sessionId);
return header;
}
async readMessages(sessionId: string): Promise<StoredMessage[]> {
const remainingFailures = this.failNextReadMessagesFor.get(sessionId) ?? 0;
if (remainingFailures > 0) {
if (remainingFailures === 1) this.failNextReadMessagesFor.delete(sessionId);
else this.failNextReadMessagesFor.set(sessionId, remainingFailures - 1);
throw new Error(`Cannot read messages for ${sessionId}`);
}
if (this.failReadMessagesFor.has(sessionId))
throw new Error(`Cannot read messages for ${sessionId}`);
return [...(this.messages.get(sessionId) ?? [])];
}
async listTurns(sessionId: string): Promise<TurnRecord[]> {
if (this.failListTurnsFor.has(sessionId)) throw new Error(`Cannot list turns for ${sessionId}`);
return deriveTurnRecords(await this.readMessages(sessionId));
}
async appendMessage(sessionId: string, message: StoredMessage): Promise<void> {
await this.appendMessages(sessionId, [message]);
}
async appendMessages(sessionId: string, messages: StoredMessage[]): Promise<void> {
if (this.failNextAppendMessage && messages.some(this.failNextAppendMessage)) {
this.failNextAppendMessage = undefined;
throw new Error('append message failed');
}
this.messages.set(sessionId, [...(this.messages.get(sessionId) ?? []), ...messages]);
if (this.failAfterNextAppendMessage && messages.some(this.failAfterNextAppendMessage)) {
this.failAfterNextAppendMessage = undefined;
throw new Error('append message failed');
}
}
async updateHeader(sessionId: string, patch: Partial<SessionHeader>): Promise<SessionHeader> {
if (this.failUpdateHeaderFor.has(sessionId))
throw new Error(`Cannot update header for ${sessionId}`);
const current = await this.readHeader(sessionId);
const next = { ...current, ...patch };
this.headers.set(sessionId, next);
return next;
}
async markSessionReadThrough(sessionId: string, readThroughTs: number): Promise<SessionHeader> {
await this.runMarkSessionReadInterleave(sessionId);
if (this.failUpdateHeaderFor.has(sessionId))
throw new Error(`Cannot update header for ${sessionId}`);
const current = await this.readHeader(sessionId);
if (!current.hasUnread) return current;
if (current.lastMessageAt !== undefined && current.lastMessageAt > readThroughTs)
return current;
const next = { ...current, hasUnread: false };
this.headers.set(sessionId, next);
return next;
}
private async runMarkSessionReadInterleave(sessionId: string): Promise<void> {
const hook = this.interleaveBeforeMarkSessionReadWriteFor.get(sessionId);
if (!hook) return;
this.interleaveBeforeMarkSessionReadWriteFor.delete(sessionId);
await hook();
}
async archive(sessionId: string): Promise<void> {
await this.updateHeader(sessionId, {
isArchived: true,
status: 'archived',
statusUpdatedAt: 1,
});
}
async unarchive(sessionId: string): Promise<void> {
await this.updateHeader(sessionId, {
isArchived: false,
status: 'active',
blockedReason: undefined,
statusUpdatedAt: 1,
});
}
async setFlagged(sessionId: string, isFlagged: boolean): Promise<void> {
await this.updateHeader(sessionId, { isFlagged });
}
async rename(sessionId: string, name: string): Promise<void> {
await this.updateHeader(sessionId, { name, titleIsManual: true });
}
async setGeneratedTitleIfAbsent(sessionId: string, title: string): Promise<SessionHeader | null> {
const current = await this.readHeader(sessionId);
this.generatedTitleAttempted?.release();
if (current.titleIsManual || current.name !== 'New Chat') return null;
return this.updateHeader(sessionId, { name: title });
}
async remove(sessionId: string): Promise<void> {
this.headers.delete(sessionId);
this.messages.delete(sessionId);
this.executionBoundaries.delete(sessionId);
}
}
class VersionedConfigurationMemorySessionStore extends MemorySessionStore {
private readonly revisions = new Map<string, number>();
nextConfigurationUpdateGate: { started: Gate; release: Gate } | undefined;
override async create(
input: CreateSessionInput,
initialBoundary?: ExecutionBoundary,
): Promise<SessionHeader> {
const header = await super.create(input, initialBoundary);
this.revisions.set(header.id, 1);
return header;
}
async readHeaderRecordSnapshot(sessionId: string): Promise<VersionedSessionHeader> {
return {
header: await this.readHeader(sessionId),
revision: this.revisions.get(sessionId) ?? 1,
committedAt: 1,
};
}
async updateSessionConfiguration(
sessionId: string,
input: SessionConfigurationStoreUpdate,
): Promise<VersionedSessionHeader> {
const gate = this.nextConfigurationUpdateGate;
if (gate) {
this.nextConfigurationUpdateGate = undefined;
gate.started.release();
await gate.release.promise;
}
const revision = this.revisions.get(sessionId) ?? 1;
if (revision !== input.expectedVersion) {
throw new Error('injected configuration revision conflict');
}
await super.setExecutionBoundaryKind(
sessionId,
input.configuration.permissionMode === 'bypass' ? 'bypass' : 'managed',
{
permissionMode: input.configuration.permissionMode,
labels: input.configuration.labels,
},
);
const header = await super.updateHeader(sessionId, {
...input.configuration,
labels: [...input.configuration.labels],
...(input.lifecycle.kind === 'clear_connection_block'
? {
status: 'active',
blockedReason: undefined,
statusUpdatedAt: input.lifecycle.statusUpdatedAt,
}
: {}),
});
this.revisions.set(sessionId, revision + 1);
return { header, revision: revision + 1, committedAt: revision + 1 };
}
async updateHeaderVersioned(
sessionId: string,
patch: Partial<SessionHeader>,
expectedRevision: number,
): Promise<VersionedSessionHeader> {
const revision = this.revisions.get(sessionId) ?? 1;
if (revision !== expectedRevision) {
throw new SessionConfigurationRevisionConflictError(expectedRevision, revision);
}
const header = await super.updateHeader(sessionId, patch);
this.revisions.set(sessionId, revision + 1);
return { header, revision: revision + 1, committedAt: revision + 1 };
}
}
class AtomicBoundaryMemorySessionStore extends MemorySessionStore {
failAppends = false;
readonly boundaryCalls: Array<{
sessionId: string;
kind: 'managed' | 'bypass';
projection:
| {
permissionMode: SessionHeader['permissionMode'];
labels?: readonly string[];
}
| undefined;
}> = [];
private readonly boundaries = new Map<string, ExecutionBoundary>();
private projectingBoundary = false;
forceBoundary(sessionId: string, boundary: ExecutionBoundary): void {
this.boundaries.set(sessionId, boundary);
}
override async readExecutionBoundary(sessionId: string): Promise<ExecutionBoundary> {
return this.boundaries.get(sessionId) ?? super.readExecutionBoundary(sessionId);
}
async setExecutionBoundaryKind(
sessionId: string,
kind: 'managed' | 'bypass',
projection?: {
permissionMode: SessionHeader['permissionMode'];
labels?: readonly string[];
},
) {
this.boundaryCalls.push({ sessionId, kind, projection });
const current = await this.readHeader(sessionId);
const permissionMode =
projection?.permissionMode ??
(kind === 'bypass'
? 'bypass'
: current.permissionMode === 'bypass'
? 'ask'
: current.permissionMode);
this.projectingBoundary = true;
try {
await super.updateHeader(sessionId, {
permissionMode,
...(projection?.labels ? { labels: [...projection.labels] } : {}),
});
} finally {
this.projectingBoundary = false;
}
const boundary = {
...createGenesisExecutionBoundary(permissionMode),
revision: 1,
};
this.boundaries.set(sessionId, boundary);
return boundary;
}
override async updateHeader(
sessionId: string,
patch: Partial<SessionHeader>,
): Promise<SessionHeader> {
if (!this.projectingBoundary && Object.hasOwn(patch, 'permissionMode')) {
throw new Error('permissionMode must be projected by the boundary transition');
}
return super.updateHeader(sessionId, patch);
}
override async appendMessage(sessionId: string, message: StoredMessage): Promise<void> {
if (this.failAppends) throw new Error('audit append failed');
return super.appendMessage(sessionId, message);
}
}
class MemoryAgentRunStore
implements AgentRunStore, RuntimeEventStore, RuntimeContinuationAuthorityStore
{
readonly continuationAuthorityCapability = RUNTIME_CONTINUATION_AUTHORITY_V1;
listSessionRunsCalls = 0;
readEventsCalls = 0;
private headers = new Map<string, AgentRunHeader>();
private events = new Map<string, AgentRunEvent[]>();
private runtimeEvents = new Map<string, RuntimeEvent[]>();
private continuationClaims = new Map<string, ContinuationClaimV1>();
private continuationStartKinds = new Map<string, 'runtime_admission' | 'claim_repair'>();
private runtimeEventAppendCount = 0;
private rootTurnAdmissions = new Map<
string,
{ runId: string; userMessageId: string | null; execution: RootExecutionDescriptor }
>();
constructor(
private readonly options: {
failRuntimeEventAppends?: boolean;
failRuntimeEventAppendAfter?: number;
failRuntimeEventReads?: boolean;
failContinuationClaimReads?: boolean;
failUpdateRunOnce?: boolean;
failUpdateRunStatusOnce?: AgentRunHeader['status'];
failContinuationCreate?: boolean;
beforeListSessionRuns?: (sessionId: string) => Promise<void> | void;
beforeRuntimeEventRead?: (sessionId: string, runId: string) => Promise<void> | void;
beforeRuntimeEventAppend?: (
sessionId: string,
runId: string,
event: RuntimeEvent,
options?: { durable?: boolean },
) => Promise<void> | void;
beforeRunRead?: (sessionId: string, runId: string) => Promise<void> | void;
beforeAgentRunEventAppend?: (
sessionId: string,
runId: string,
event: AgentRunEvent,
) => Promise<void> | void;
beforeAgentRunEventRead?: (sessionId: string, runId: string) => Promise<void> | void;
beforeAgentRunUpdate?: (
sessionId: string,
runId: string,
patch: Partial<AgentRunHeader>,
) => Promise<void> | void;
} = {},
) {}
async createRun(header: AgentRunHeader): Promise<AgentRunHeader> {
if (this.options.failContinuationCreate && header.continuationSource) {
throw new Error('continuation claim create failed');
}
this.headers.set(key(header.sessionId, header.runId), { ...header });
return { ...header };
}
async updateRun(
sessionId: string,
runId: string,
patch: Partial<AgentRunHeader>,
): Promise<AgentRunHeader> {
await this.options.beforeAgentRunUpdate?.(sessionId, runId, patch);
if (this.options.failUpdateRunOnce) {
this.options.failUpdateRunOnce = false;
throw new Error('update run failed');
}
if (patch.status && patch.status === this.options.failUpdateRunStatusOnce) {
this.options.failUpdateRunStatusOnce = undefined;
throw new Error('update run failed');
}
const current = await this.readRun(sessionId, runId);
const next = { ...current, ...patch, sessionId, runId };
this.headers.set(key(sessionId, runId), next);
return { ...next };
}
async readRun(sessionId: string, runId: string): Promise<AgentRunHeader> {
await this.options.beforeRunRead?.(sessionId, runId);
const header = this.headers.get(key(sessionId, runId));
if (!header) {
const error = new Error(`Unknown run ${runId}`) as NodeJS.ErrnoException;
error.code = 'ENOENT';
throw error;
}
return { ...header };
}
async listSessionRuns(sessionId: string): Promise<AgentRunHeader[]> {
this.listSessionRunsCalls += 1;
await this.options.beforeListSessionRuns?.(sessionId);
return Array.from(this.headers.values())
.filter((header) => header.sessionId === sessionId)
.sort((a, b) => a.createdAt - b.createdAt || a.runId.localeCompare(b.runId))
.map((header) => ({ ...header }));
}
seedRootTurnAdmission(
sessionId: string,
turnId: string,
admission: { runId: string; userMessageId: string | null; execution: RootExecutionDescriptor },
): void {
this.rootTurnAdmissions.set(key(sessionId, turnId), admission);
}
async readRootTurnAdmission(
sessionId: string,
turnId: string,
): Promise<
{ runId: string; userMessageId: string | null; execution: RootExecutionDescriptor } | undefined
> {
return this.rootTurnAdmissions.get(key(sessionId, turnId));
}
async appendEvent(sessionId: string, runId: string, event: AgentRunEvent): Promise<void> {
await this.options.beforeAgentRunEventAppend?.(sessionId, runId, event);
const eventKey = key(sessionId, runId);
this.events.set(eventKey, [...(this.events.get(eventKey) ?? []), copyEvent(event)]);
}
async readEvents(sessionId: string, runId: string): Promise<AgentRunEvent[]> {
this.readEventsCalls += 1;
await this.options.beforeAgentRunEventRead?.(sessionId, runId);
return (this.events.get(key(sessionId, runId)) ?? []).map(copyEvent);
}
async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
options?: { durable?: boolean },
): Promise<void> {
await this.options.beforeRuntimeEventAppend?.(sessionId, runId, event, options);
if (this.options.failRuntimeEventAppends) throw new Error('runtime event append failed');
this.runtimeEventAppendCount += 1;
if (
this.options.failRuntimeEventAppendAfter !== undefined &&
this.runtimeEventAppendCount > this.options.failRuntimeEventAppendAfter
) {
this.options.failRuntimeEventAppendAfter = undefined;
throw new Error('runtime event append failed');
}
const eventKey = key(sessionId, runId);
this.runtimeEvents.set(eventKey, [
...(this.runtimeEvents.get(eventKey) ?? []),
copyRuntimeEvent(event),
]);
}
async ensureTerminalRuntimeEventDurable(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
const existing = (this.runtimeEvents.get(key(sessionId, runId)) ?? []).find(
(candidate) => candidate.id === event.id,
);
if (!existing) {
await this.appendRuntimeEvent(sessionId, runId, event);
return;
}
if (JSON.stringify(existing) !== JSON.stringify(event)) {
throw new Error(`RuntimeEvent ${event.id} does not match the durable ledger record`);
}
}
async readRuntimeEvents(sessionId: string, runId: string): Promise<RuntimeEvent[]> {
if (this.options.failRuntimeEventReads) throw new Error('runtime event read failed');
await this.options.beforeRuntimeEventRead?.(sessionId, runId);
return (this.runtimeEvents.get(key(sessionId, runId)) ?? []).map(copyRuntimeEvent);
}
async readImmutableRuntimeEvents(sessionId: string, runId: string): Promise<RuntimeEvent[]> {
if (this.options.failRuntimeEventReads) throw new Error('runtime event read failed');
await this.options.beforeRuntimeEventRead?.(sessionId, runId);
return (this.runtimeEvents.get(key(sessionId, runId)) ?? [])
.filter((event) => event.partial !== true)
.map(copyRuntimeEvent);
}
replaceRuntimeEvent(
sessionId: string,
runId: string,
eventId: string,
replacement: RuntimeEvent,
): void {
const eventKey = key(sessionId, runId);
const events = this.runtimeEvents.get(eventKey) ?? [];
const index = events.findIndex((event) => event.id === eventId);
if (index < 0) throw new Error(`Unknown RuntimeEvent ${eventId}`);
const next = [...events];
next[index] = copyRuntimeEvent(replacement);
this.runtimeEvents.set(eventKey, next);
}
async readImmutableRuntimePrefix(input: {
sessionId: string;
runId: string;
upToEventSeq?: number;
}) {
const events = await this.readImmutableRuntimeEvents(input.sessionId, input.runId);
const selected =
input.upToEventSeq === undefined ? events : events.slice(0, input.upToEventSeq);
const first = selected[0];
if (!first || selected.length !== (input.upToEventSeq ?? selected.length)) {
throw new Error('immutable RuntimeEvent prefix is unavailable');
}
return buildImmutableRuntimePrefix(
{
sessionId: first.sessionId,
invocationId: first.invocationId,
runId: first.runId,
turnId: first.turnId,
},
selected.map((event, index) => ({ eventSeq: index + 1, event })),
);
}
async claimContinuation(input: { claim: ContinuationClaimV1 }) {
const claim = decodeContinuationClaim(input.claim);
const existing = this.continuationClaims.get(claim.boundaryDigest);
if (existing) return { kind: 'existing' as const, claim: existing };
const conflict = [...this.continuationClaims.values()].find(
(candidate) =>
candidate.target.invocationId === claim.target.invocationId ||
candidate.target.runId === claim.target.runId ||
(candidate.target.sessionId === claim.target.sessionId &&
candidate.target.turnId === claim.target.turnId),
);
if (conflict) return { kind: 'conflict' as const, claim: conflict };
this.continuationClaims.set(claim.boundaryDigest, claim);
return { kind: 'acquired' as const, claim };
}
async readContinuationClaimByBoundary(
boundaryDigest: RuntimeBoundaryDigest,
): Promise<ContinuationClaimV1 | undefined> {
return this.continuationClaims.get(boundaryDigest);
}
async readContinuationClaimStateByBoundary(boundaryDigest: RuntimeBoundaryDigest) {
if (this.options.failContinuationClaimReads) {
throw new Error('continuation authority read failed');
}
const claim = this.continuationClaims.get(boundaryDigest);
if (!claim) return undefined;
const startEventId = this.runtimeEvents
.get(key(claim.target.sessionId, claim.target.runId))
?.find((event) => event.actions?.continuationStart?.claimId === claim.claimId)?.id;
const startKind = this.continuationStartKinds.get(boundaryDigest);
return {
claim,
...(startEventId && startKind ? { startEventId, startKind } : {}),
};
}
async listContinuationClaimsForRecovery(sessionId: string) {
if (this.options.failContinuationClaimReads) {
throw new Error('continuation authority read failed');
}
const states = [];
for (const claim of this.continuationClaims.values()) {
if (claim.target.sessionId !== sessionId) continue;
states.push((await this.readContinuationClaimStateByBoundary(claim.boundaryDigest))!);
}
return states;
}
async commitContinuationStart(input: { claim: ContinuationClaimV1; event: RuntimeEvent }) {
return this.commitContinuationStartOfKind(input, 'runtime_admission');
}
async commitContinuationRepairStart(input: { claim: ContinuationClaimV1; event: RuntimeEvent }) {
return this.commitContinuationStartOfKind(input, 'claim_repair');
}
private async commitContinuationStartOfKind(
input: { claim: ContinuationClaimV1; event: RuntimeEvent },
startKind: 'runtime_admission' | 'claim_repair',
) {
if (input.event.actions?.continuationStart?.provenance !== startKind) {
throw new Error('continuation start kind mismatch');
}
const existing = await this.readContinuationClaimStateByBoundary(input.claim.boundaryDigest);
if (existing?.startEventId) {
if (existing.startEventId !== input.event.id || existing.startKind !== startKind) {
throw new Error('continuation claim already has a different start');
}
return { created: false, runtimeEventSeq: 1 };
}
await this.appendRuntimeEvent(
input.claim.target.sessionId,
input.claim.target.runId,
input.event,
);
this.continuationStartKinds.set(input.claim.boundaryDigest, startKind);
return { created: true, runtimeEventSeq: 1 };
}
async readSessionRuntimeEvents(sessionId: string): Promise<RuntimeEvent[]> {
const ordered: Array<{ event: RuntimeEvent; runId: string; eventIndex: number }> = [];
for (const [eventKey, events] of this.runtimeEvents.entries()) {
const [eventSessionId, runId] = eventKey.split(':');
if (eventSessionId !== sessionId || !runId) continue;
events.forEach((event, eventIndex) =>
ordered.push({ event: copyRuntimeEvent(event), runId, eventIndex }),
);
}
ordered.sort(
(a, b) =>
a.event.ts - b.event.ts ||
a.runId.localeCompare(b.runId) ||
a.eventIndex - b.eventIndex ||
a.event.id.localeCompare(b.event.id),
);
return ordered.map((item) => item.event);
}
}
class ContinuationClaimBarrierRunStore extends MemoryAgentRunStore {
private continuationClaimBarrierArmed = false;
private markContinuationClaimRead: (() => void) | undefined;
private releaseContinuationClaimReadWaiter: (() => void) | undefined;
private readonly continuationClaimRead = new Promise<void>((resolve) => {
this.markContinuationClaimRead = resolve;
});
private readonly continuationClaimRelease = new Promise<void>((resolve) => {
this.releaseContinuationClaimReadWaiter = resolve;
});
armContinuationClaimBarrier(): void {
this.continuationClaimBarrierArmed = true;
}
async waitForContinuationClaimRead(): Promise<void> {
await this.continuationClaimRead;
}
releaseContinuationClaimRead(): void {
this.releaseContinuationClaimReadWaiter?.();
}
override async listSessionRuns(sessionId: string): Promise<AgentRunHeader[]> {
const snapshot = await super.listSessionRuns(sessionId);
if (!this.continuationClaimBarrierArmed) return snapshot;
this.continuationClaimBarrierArmed = false;
this.markContinuationClaimRead?.();
await this.continuationClaimRelease;
return snapshot;
}
}
/** Yields a forged queue_update before completing — round-6 R3's attacker. */
class ForgingQueueBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'queue_update',
id: 'forged-queue-update',
turnId: input.turnId,
ts: 1,
steering: ['forged pending message'],
followup: [],
};
yield {
type: 'text_complete',
id: `${input.turnId}-final`,
turnId: input.turnId,
ts: 2,
messageId: `${input.turnId}-m`,
text: 'ok',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 3,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ProviderRetryProgressBackend implements AgentBackend {
readonly kind = 'fake' as const;
readonly sessionId: string;
constructor(ctx: BackendFactoryContext) {
this.sessionId = ctx.sessionId;
}
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
yield {
type: 'provider_retry',
id: 'retry-scheduled',
turnId: input.turnId,
ts: 1,
phase: 'scheduled',
attempt: 2,
maxAttempts: 10,
delayMs: 1_000,
reason: 'rate_limit',
};
yield {
type: 'provider_retry',
id: 'retry-started',
turnId: input.turnId,
ts: 2,
phase: 'started',
attempt: 2,
maxAttempts: 10,
reason: 'rate_limit',
};
yield {
type: 'text_complete',
id: `${input.turnId}-final`,
turnId: input.turnId,
ts: 3,
messageId: `${input.turnId}-m`,
text: 'ok',
};
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToSandboxBoundary(_decision: SandboxBoundaryResponse): Promise<void> {}
async dispose(): Promise<void> {}
}
class ReverseOrderedAgentRunStore extends MemoryAgentRunStore {
override async listSessionRuns(sessionId: string): Promise<AgentRunHeader[]> {
return (await super.listSessionRuns(sessionId)).reverse();
}
}
class OrderingAgentRunStore extends MemoryAgentRunStore {
operations: string[] = [];
override async updateRun(
sessionId: string,
runId: string,
patch: Partial<AgentRunHeader>,
): Promise<AgentRunHeader> {
const next = await super.updateRun(sessionId, runId, patch);
if (patch.status === 'completed') this.operations.push('completedRunHeader');
return next;
}
override async appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
await super.appendRuntimeEvent(sessionId, runId, event);
if (isTerminalRuntimeEvent(event)) this.operations.push('terminalRuntimeEvent');
}
}
class MissingCheckpointProjectionAgentRunStore extends MemoryAgentRunStore {
repairedProjection: AgentRunEvent | null | undefined;
async readEventProjection(): Promise<undefined> {
return undefined;
}
async repairEventProjection(
_sessionId: string,
_type: AgentRunEvent['type'],
event: AgentRunEvent | null,
): Promise<void> {
this.repairedProjection = event;
}
}
class MemoryRuntimeEventStore implements RuntimeEventStore {
private runtimeEvents = new Map<string, RuntimeEvent[]>();
constructor(
private readonly options: {
failRuntimeEventAppends?: boolean;
failRuntimeEventReads?: boolean;
} = {},
) {}
async appendRuntimeEvent(sessionId: string, runId: string, event: RuntimeEvent): Promise<void> {
if (this.options.failRuntimeEventAppends) throw new Error('runtime event append failed');
const eventKey = key(sessionId, runId);
this.runtimeEvents.set(eventKey, [
...(this.runtimeEvents.get(eventKey) ?? []),
copyRuntimeEvent(event),
]);
}
async ensureTerminalRuntimeEventDurable(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void> {
const existing = (this.runtimeEvents.get(key(sessionId, runId)) ?? []).find(
(candidate) => candidate.id === event.id,
);
if (!existing) {
await this.appendRuntimeEvent(sessionId, runId, event);
return;
}
if (JSON.stringify(existing) !== JSON.stringify(event)) {
throw new Error(`RuntimeEvent ${event.id} does not match the durable ledger record`);
}
}
async readRuntimeEvents(sessionId: string, runId: string): Promise<RuntimeEvent[]> {
if (this.options.failRuntimeEventReads) throw new Error('runtime event read failed');
return (this.runtimeEvents.get(key(sessionId, runId)) ?? []).map(copyRuntimeEvent);
}
async readSessionRuntimeEvents(sessionId: string): Promise<RuntimeEvent[]> {
const ordered: Array<{ event: RuntimeEvent; runId: string; eventIndex: number }> = [];
for (const [eventKey, events] of this.runtimeEvents.entries()) {
const [eventSessionId, runId] = eventKey.split(':');
if (eventSessionId !== sessionId || !runId) continue;
events.forEach((event, eventIndex) =>
ordered.push({ event: copyRuntimeEvent(event), runId, eventIndex }),
);
}
ordered.sort(
(a, b) =>
a.event.ts - b.event.ts ||
a.runId.localeCompare(b.runId) ||
a.eventIndex - b.eventIndex ||
a.event.id.localeCompare(b.event.id),
);
return ordered.map((item) => item.event);
}
}
interface Gate {
promise: Promise<void>;
release(): void;
}
function makeGate(): Gate {
let release: () => void = () => {};
const promise = new Promise<void>((resolve) => {
release = resolve;
});
return { promise, release };
}
function makeTestRuntimePolicyGate(): {
runBackendActivation<T>(operation: () => Promise<T> | T): Promise<T>;
runMutation<T>(operation: () => Promise<T>): Promise<T>;
} {
const activeActivations = new Set<Promise<void>>();
return {
async runBackendActivation<T>(operation: () => Promise<T> | T): Promise<T> {
const completion = makeGate();
activeActivations.add(completion.promise);
try {
return await operation();
} finally {
completion.release();
activeActivations.delete(completion.promise);
}
},
async runMutation<T>(operation: () => Promise<T>): Promise<T> {
await Promise.all([...activeActivations]);
return operation();
},
};
}
function hostedRootAuthority(): RuntimeHostedRootAuthority {
return {
bindRun: (identity) => ({
...identity,
pull: () => [],
ack: () => {},
nack: () => {},
release: () => {},
}),
executeRoot: async (input) => {
for await (const event of input.start({
runId: input.runId,
userMessageId: input.userMessageId,
onRunStarted: () => input.onReady?.(),
})) {
input.onEvent?.(event);
}
},
stopRoot: async () => {},
stopSession: async () => {},
};
}
function noOpShellRunProcessManager(): ShellRunProcessManager {
return {
async terminateSession(sessionId: string) {
return { sessionId, token: Symbol('test') };
},
async commitSessionClose() {},
rollbackSessionClose() {},
resumeSession() {},
} as unknown as ShellRunProcessManager;
}
function makeInput(overrides: Partial<CreateSessionInput> = {}): CreateSessionInput {
return {
cwd: '/tmp/cwd',
backend: 'fake',
llmConnectionSlug: 'fake',
model: 'fake-model',
permissionMode: 'ask',
name: 'Session',
labels: [],
...overrides,
};
}
function createGraphOperatorSession(
store: MemorySessionStore,
parentSessionId: string,
): Promise<SessionHeader> {
return store.create(
makeInput({
name: 'Graph operator',
permissionMode: 'explore',
collaborationMode: 'agent',
orchestrationMode: 'default',
subagentParent: {
kind: 'subagent',
parentSessionId,
spawnedBy: {
parentRunId: 'supervisor-run',
parentTurnId: 'supervisor-turn',
toolCallId: 'graph-operator-create',
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: 1,
definitionVersion: LOCAL_READ_AGENT_DEFINITION.definitionVersion,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
profile: LOCAL_READ_AGENT_PROFILE,
systemPrompt: LOCAL_READ_AGENT_DEFINITION.systemPrompt,
toolNames: [...LOCAL_READ_AGENT_DEFINITION.tools],
categoryPolicy: {},
permissionCeiling: 'ask',
},
}),
);
}
function graphIntentClaim(
overrides: Partial<AgentGraphIntentClaim> = {},
prompt = 'test graph prompt',
): AgentGraphIntentClaim {
const claim: AgentGraphIntentClaim = {
schemaVersion: 1,
claimId: `graph_claim_${'a'.repeat(32)}`,
graphId: 'graph-1',
intentId: `graph_intent_${'b'.repeat(32)}`,
intentFingerprint: '',
readinessContextFingerprint: `sha256:${'d'.repeat(64)}`,
targetOperatorId: 'operator-1',
targetSessionId: 'session-child',
targetTurnId: 'graph-turn',
targetRunId: 'graph-run',
claimedAt: 10,
...overrides,
};
const intent = graphRunnableIntentForClaim(claim);
return Object.freeze({
...claim,
intentFingerprint:
overrides.intentFingerprint ??
fingerprintAgentGraphRunnableIntent({
intent,
executionInput: { prompt },
}),
});
}
function graphExecutionInput(claim: AgentGraphIntentClaim, prompt: string) {
const intent = graphRunnableIntentForClaim(claim);
const storedClaim = Object.freeze({ ...claim });
const claimStore: AgentGraphIntentClaimStore = {
async claimAgentGraphIntent() {
throw new Error('test claim store is read-only');
},
async readAgentGraphIntentClaim(graphId, intentId) {
return graphId === storedClaim.graphId && intentId === storedClaim.intentId
? storedClaim
: undefined;
},
async listAgentGraphIntentClaims(graphId) {
return !graphId || graphId === storedClaim.graphId ? [storedClaim] : [];
},
};
return {
claimStore,
intent,
graphId: claim.graphId,
intentId: claim.intentId,
prompt,
};
}
function graphRunnableIntentForClaim(claim: AgentGraphIntentClaim): AgentGraphRunnableIntent {
return {
schemaVersion: 1,
intentId: claim.intentId,
graphId: claim.graphId,
readinessContextFingerprint: claim.readinessContextFingerprint,
policyFingerprint: `sha256:${'e'.repeat(64)}`,
readinessId: 'readiness-1',
operatorId: claim.targetOperatorId,
targetSessionId: claim.targetSessionId,
policyKind: 'map',
triggerRouteIds: ['route-1'],
triggerRecordIds: ['record-1'],
};
}
async function createQueuedGraphScenario(firstAbortSignal?: AbortSignal) {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const activeGate = makeGate();
const firstReady = makeGate();
const stopStarted = makeGate();
let backend!: TestBackend;
backends.register('fake', (ctx) => {
backend = new (class extends TestBackend {
override async stop(
reason: 'user_stop' | 'redirect',
mode: BackendStopMode = 'immediate',
): Promise<void> {
await super.stop(reason, mode);
stopStarted.release();
activeGate.release();
}
})(ctx, activeGate);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(70),
});
const parent = await manager.createSession(makeInput());
const child = await createGraphOperatorSession(store, parent.id);
const firstClaim = graphIntentClaim({ targetSessionId: child.id }, 'first activation');
const secondClaim = graphIntentClaim(
{
claimId: `graph_claim_${'e'.repeat(32)}`,
intentId: `graph_intent_${'f'.repeat(32)}`,
targetSessionId: child.id,
targetTurnId: 'graph-turn-2',
targetRunId: 'graph-run-2',
},
'queued activation',
);
const thirdClaim = graphIntentClaim(
{
claimId: `graph_claim_${'1'.repeat(32)}`,
intentId: `graph_intent_${'2'.repeat(32)}`,
targetSessionId: child.id,
targetTurnId: 'graph-turn-3',
targetRunId: 'graph-run-3',
},
'third activation',
);
const first = manager.runClaimedAgentGraphIntent({
...graphExecutionInput(firstClaim, 'first activation'),
...(firstAbortSignal ? { abortSignal: firstAbortSignal } : {}),
onReady: () => firstReady.release(),
});
await firstReady.promise;
return {
store,
runStore,
manager,
child,
backend,
activeGate,
stopStarted,
first,
claims: [firstClaim, secondClaim, thirdClaim] as const,
};
}
function hostedGraphExecutionCapability(
claims: AgentGraphIntentClaimStore,
runs: MemoryAgentRunStore,
) {
return {
readAgentGraphIntentClaim: (graphId: string, intentId: string) =>
claims.readAgentGraphIntentClaim(graphId, intentId),
async readRootTurnAdmissionIdentity(sessionId: string, turnId: string) {
const admission = await runs.readRootTurnAdmission(sessionId, turnId);
return admission
? { runId: admission.runId, userMessageId: admission.userMessageId }
: undefined;
},
};
}
function testTool(name: string): MakaTool {
return {
name,
description: `${name} test tool`,
parameters: {},
impl: async () => ({ ok: true }),
};
}
function makeRunHeader(overrides: Partial<AgentRunHeader> = {}): AgentRunHeader {
return {
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
status: 'running',
backendKind: 'fake',
llmConnectionSlug: 'fake',
modelId: 'fake-model',
cwd: '/tmp/cwd',
permissionMode: 'ask',
createdAt: 10,
updatedAt: 10,
...overrides,
};
}
function makeRunEvent(overrides: Partial<EmittedAgentRunEvent> = {}): EmittedAgentRunEvent {
return {
type: 'run_started',
id: `${overrides.runId ?? 'run-1'}-${overrides.type ?? 'run_started'}-${overrides.ts ?? 10}`,
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
ts: 10,
...overrides,
};
}
function makeManagerForReadCutover(
store: MemorySessionStore,
runStore: AgentRunStore & RuntimeEventStore,
): SessionManager {
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
return new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(6_755),
});
}
async function seedRuntimeReadTurn(input: {
store: MemorySessionStore;
runStore: AgentRunStore & RuntimeEventStore;
sessionId: string;
turnId: string;
runId: string;
userText: string;
assistantText: string;
legacyIdPrefix: string;
}): Promise<{ legacyMessages: StoredMessage[]; projectedMessages: StoredMessage[] }> {
const header = makeRunHeader({
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
status: 'completed',
createdAt: 100,
updatedAt: 103,
completedAt: 103,
});
const events = [
runtimeEvent({
id: `${input.runId}-user-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: input.userText },
refs: { storedMessageId: `${input.runId}-projected-user` },
}),
runtimeEvent({
id: `${input.runId}-assistant-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: input.assistantText },
refs: { storedMessageId: `${input.runId}-projected-assistant` },
}),
runtimeEvent({
id: `${input.runId}-complete-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: 103,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
];
const legacyMessages: StoredMessage[] = [
{
type: 'user',
id: `${input.legacyIdPrefix}-user`,
turnId: input.turnId,
ts: 101,
text: input.userText,
},
{
type: 'assistant',
id: `${input.legacyIdPrefix}-assistant`,
turnId: input.turnId,
ts: 102,
text: input.assistantText,
modelId: 'fake-model',
},
{
type: 'turn_state',
id: `${input.legacyIdPrefix}-state`,
turnId: input.turnId,
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
];
const projectedMessages: StoredMessage[] = [
{
type: 'user',
id: `${input.runId}-projected-user`,
turnId: input.turnId,
ts: 101,
text: input.userText,
},
{
type: 'assistant',
id: `${input.runId}-projected-assistant`,
turnId: input.turnId,
ts: 102,
text: input.assistantText,
modelId: 'fake-model',
},
{
type: 'turn_state',
id: `${input.runId}-complete-event`,
turnId: input.turnId,
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
];
await input.store.appendMessages(input.sessionId, legacyMessages);
await seedRuntimeRun(input.runStore, header, events);
return { legacyMessages, projectedMessages };
}
async function seedRuntimeReadTurnWithHeader(input: {
store: MemorySessionStore;
runStore: AgentRunStore & RuntimeEventStore;
sessionId: string;
turnId: string;
runId: string;
userText: string;
assistantText: string;
legacyIdPrefix: string;
header: Partial<AgentRunHeader>;
tsBase: number;
}): Promise<void> {
const header = makeRunHeader({
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
status: 'completed',
createdAt: input.tsBase,
updatedAt: input.tsBase + 3,
completedAt: input.tsBase + 3,
...input.header,
});
const events = [
runtimeEvent({
id: `${input.runId}-user-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: input.tsBase + 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: input.userText },
refs: { storedMessageId: `${input.runId}-projected-user` },
}),
runtimeEvent({
id: `${input.runId}-assistant-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: input.tsBase + 2,
role: 'model',
author: 'agent',
content: { kind: 'text', text: input.assistantText },
refs: { storedMessageId: `${input.runId}-projected-assistant` },
}),
runtimeEvent({
id: `${input.runId}-complete-event`,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
ts: input.tsBase + 3,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
];
await input.store.appendMessages(input.sessionId, [
{
type: 'user',
id: `${input.legacyIdPrefix}-user`,
turnId: input.turnId,
ts: input.tsBase + 1,
text: input.userText,
},
{
type: 'assistant',
id: `${input.legacyIdPrefix}-assistant`,
turnId: input.turnId,
ts: input.tsBase + 2,
text: input.assistantText,
modelId: 'fake-model',
},
{
type: 'turn_state',
id: `${input.legacyIdPrefix}-state`,
turnId: input.turnId,
ts: input.tsBase + 3,
status: 'completed',
...(input.header.parentTurnId ? { parentTurnId: input.header.parentTurnId } : {}),
...(input.header.retriedFromTurnId
? { retriedFromTurnId: input.header.retriedFromTurnId }
: {}),
...(input.header.regeneratedFromTurnId
? { regeneratedFromTurnId: input.header.regeneratedFromTurnId }
: {}),
...(input.header.branchOfTurnId ? { branchOfTurnId: input.header.branchOfTurnId } : {}),
...(input.header.parentSessionId ? { parentSessionId: input.header.parentSessionId } : {}),
partialOutputRetained: true,
},
]);
await seedRuntimeRun(input.runStore, header, events);
}
async function seedRun(
runStore: AgentRunStore,
header: AgentRunHeader,
events: EmittedAgentRunEvent[],
): Promise<void> {
await runStore.createRun(header);
for (const event of events) {
await runStore.appendEvent(header.sessionId, header.runId, event);
}
}
async function seedRuntimeRun(
runStore: AgentRunStore & RuntimeEventStore,
header: AgentRunHeader,
events: RuntimeEvent[],
): Promise<void> {
await runStore.createRun(header);
for (const event of events) {
await runStore.appendRuntimeEvent(header.sessionId, header.runId, event);
}
}
function runtimeEvent(overrides: Partial<RuntimeEvent>): RuntimeEvent {
return {
id: 'rt-event',
invocationId: 'inv-1',
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
ts: 100,
partial: false,
role: 'system',
author: 'system',
...overrides,
};
}
async function seedCanonicalPermissionRun(
runStore: MemoryAgentRunStore,
header: AgentRunHeader,
includeLedgerRequest = true,
): Promise<void> {
const events = [
runtimeEvent({
id: 'permission-user-canonical',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 99,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'write the file' },
}),
...(includeLedgerRequest
? [
runtimeEvent({
id: 'permission-request-canonical',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 100,
actions: {
permissionRequest: {
kind: 'tool_permission',
requestId: 'request-canonical',
toolUseId: 'tool-canonical',
toolName: 'Write',
category: 'file_write',
reason: 'file_write',
args: { path: '/tmp/file' },
rememberForTurnAllowed: true,
hint: 'write approval',
},
},
refs: { toolCallId: 'tool-canonical' },
}),
]
: []),
runtimeEvent({
id: 'permission-answer-canonical',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 126,
author: 'user',
actions: {
permissionAnswerAccepted: { requestId: 'request-canonical' },
},
refs: { toolCallId: 'tool-canonical' },
}),
runtimeEvent({
id: 'permission-terminal-canonical',
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
ts: 130,
status: 'completed',
actions: { endInvocation: true },
}),
];
await seedRuntimeRun(runStore, header, events);
}
function canonicalPermissionRecord(
header: AgentRunHeader,
overrides: Partial<CanonicalPermissionOutcomeRecord> = {},
): CanonicalPermissionOutcomeRecord {
return {
sessionId: header.sessionId,
runId: header.runId,
turnId: header.turnId,
requestId: 'request-canonical',
request: {
kind: 'permission',
toolUseId: 'tool-canonical',
prompt: {
kind: 'tool_permission',
toolName: 'Write',
category: 'file_write',
reason: 'file_write',
review: { kind: 'path', operation: 'write', path: '/tmp/file' },
rememberForTurnAllowed: true,
},
},
outcome: {
kind: 'permission_answer',
decision: 'deny',
rememberForTurn: false,
reviewer: 'user',
committedAt: 125,
},
...overrides,
};
}
const noCanonicalPermissionOutcomes: CanonicalPermissionOutcomeReader = {
readPermissionOutcome: async () => undefined,
};
function testInteractionAuthority(): RuntimeInteractionAuthority {
return {
bindRun: (identity) => ({
...identity,
acceptSandboxBoundaryRequest: async () => {},
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {},
}),
};
}
/**
* A session interrupted while one turn held a sandbox boundary request: the
* request row exists with its turn/run provenance, the run is still
* non-terminal, and the ledger holds nothing about the boundary. Tests layer
* their own settlement or ledger events on top.
*/
async function seedBoundaryRestartSession(input: {
now: number;
requestId: string;
requestTurnId?: string;
requestRunId?: string;
interactionAuthority?: RuntimeInteractionAuthority;
}): Promise<{
store: MemorySessionStore;
runStore: MemoryAgentRunStore;
manager: SessionManager;
session: SessionSummary;
}> {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new TestBackend(ctx));
const managerDeps = {
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(input.now),
};
const manager = input.interactionAuthority
? new SessionManager({
...managerDeps,
interactionAuthority: input.interactionAuthority,
canonicalPermissionOutcomes: noCanonicalPermissionOutcomes,
})
: new SessionManager(managerDeps);
const session = await manager.createSession(makeInput({ status: 'waiting_for_user' }));
await store.createSandboxBoundaryRequest({
sessionId: session.id,
requestId: input.requestId,
turnId: input.requestTurnId ?? 'turn-1',
runId: input.requestRunId ?? 'run-1',
expansion: { network: { enabled: true } },
justification: 'Fetch a dependency.',
});
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'waiting_for_user',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'run_started',
ts: 11,
}),
],
);
return { store, runStore, manager, session };
}
async function seedRunningTurn(
store: MemorySessionStore,
sessionId: string,
turnId: string,
): Promise<void> {
await store.appendMessages(sessionId, [
{ type: 'user', id: `${turnId}-user`, turnId, ts: 9, text: 'interrupted turn' },
{
type: 'turn_state',
id: `${turnId}-state`,
turnId,
ts: 10,
status: 'running',
partialOutputRetained: false,
},
]);
}
function nextId(): () => string {
let id = 0;
return () => `id-${++id}`;
}
function nextNow(start: number): () => number {
let ts = start;
return () => ++ts;
}
async function inspectStableContinuationSafety() {
return {
workspaceIdentity: 'workspace-1',
backgroundOperationsSettled: true,
availableToolNames: [] as string[],
};
}
async function drain(iterable: AsyncIterable<unknown>): Promise<void> {
for await (const _event of iterable) {
// consume
}
}
async function collectSessionEvents(
iterable: AsyncIterable<SessionEvent>,
): Promise<SessionEvent[]> {
const events: SessionEvent[] = [];
for await (const event of iterable) {
events.push(event);
}
return events;
}
async function expectRejects(promise: Promise<unknown>, pattern: RegExp): Promise<void> {
try {
await promise;
} catch (err) {
expect(err instanceof Error ? err.message : String(err)).toMatch(pattern);
return;
}
throw new Error('Expected promise to reject');
}
function key(sessionId: string, runId: string): string {
return `${sessionId}:${runId}`;
}
function copyEvent(event: AgentRunEvent): AgentRunEvent {
return {
...event,
...(event.data ? { data: { ...event.data } } : {}),
};
}
function copyRuntimeEvent(event: RuntimeEvent): RuntimeEvent {
return JSON.parse(JSON.stringify(event)) as RuntimeEvent;
}