blob: c634ab5e7b4c7c3e4d399f19e70af999a675b7ed [file]
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { setTimeout as timerDelay } from 'node:timers/promises';
import { createHash } from 'node:crypto';
import {
DEEP_RESEARCH_SESSION_LABEL,
deriveTurnRecords,
isSessionInlineRun,
isTerminalRuntimeEvent,
} from '@maka/core';
import type {
CreateSessionInput,
PermissionMode,
QueueEnqueueOutcome,
AgentGraphIntentClaim,
AgentGraphIntentClaimStore,
AgentGraphOperatorProvisionRequest,
AgentGraphOperatorProvisionResult,
AgentRunEvent,
AgentRunHeader,
AgentRunStore,
RuntimeEvent,
RuntimeEventStore,
RootExecutionDescriptor,
SessionEvent,
SessionHeader,
SessionListFilter,
SessionSummary,
ShellRunSnapshotResult,
StoredMessage,
TurnRecord,
} from '@maka/core';
import type {
BackendSendInput,
BackendStopMode,
PermissionDecision,
} from '@maka/core/backend-types';
import { expect } from '../test-helpers.js';
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
import { z } from 'zod';
import type { LlmConnection } from '@maka/core';
import { AiSdkBackend } from '../ai-sdk-backend.js';
import { PermissionEngine } from '../permission-engine.js';
import {
BackendRegistry,
SessionManager,
headerToSummary,
type BackendFactoryContext,
type SessionStore,
} from '../session-manager.js';
import { 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/backend-types';
import type { MakaTool } from '../tool-runtime.js';
import type { ShellRunProcessManager } from '../shell-run-manager.js';
import type { InvocationResult } from '../invocation-context.js';
import type { ActiveFullCompactBlock } from '../active-full-compact.js';
import {
buildHistoryCompactCheckpoint,
type HistoryCompactCheckpoint,
} from '../history-compact-checkpoint.js';
import {
AGENT_WORKSPACE_WORKTREE,
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 {
buildExpertAgentId,
getExpertTeam,
materializeExpertAgentDefinition,
} from '../expert-catalog.js';
import { AGENT_TEAM_CHILD_TOOL_NAMES } from '../agent-team-tool-names.js';
import {
RuntimeMessageAuthorityInvariantError,
type RuntimeHostedRootAuthority,
type RuntimeMessageRunIdentity,
} from '../message-authority.js';
import {
RuntimeInteractionInvariantError,
type CanonicalPermissionOutcomeReader,
type CanonicalPermissionOutcomeRecord,
type RuntimeInteractionAuthority,
type RuntimeInteractionRunIdentity,
type RuntimePermissionContinuation,
type RuntimeUserQuestionContinuation,
} from '../interaction-authority.js';
import {
claimAgentGraphRunnableIntent,
fingerprintAgentGraphRunnableIntent,
} from '../stream-graph-admission.js';
import type { AgentGraphRunnableIntent } from '../stream-graph-readiness.js';
test('session summaries preserve an explicit no-project association', async () => {
const store = new MemorySessionStore();
const header = await store.create(makeInput({ projectId: null }));
expect(headerToSummary(header).projectId).toBe(null);
});
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('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('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: ['Read', 'Glob', 'Grep', 'Write', 'Edit', 'Bash'].map(testTool),
worktreeChildExecutor: {
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 () => {},
},
newId: nextId(),
now: nextNow(40),
});
const parent = await manager.createSession(
makeInput({
cwd: '/tmp/project',
projectId: 'project-1',
permissionMode: 'bypass',
}),
);
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('bypass');
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,
);
});
});
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 retry rejects an existing Run with conflicting durable message content', 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: 'conflicting-hosted-turn',
targetRunId: 'conflicting-hosted-run',
},
'expected durable prompt',
);
await runStore.createRun(
makeRunHeader({
sessionId: child.id,
runId: claim.targetRunId,
turnId: claim.targetTurnId,
status: 'completed',
completedAt: 35,
permissionMode: 'explore',
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
}),
);
runStore.seedRootTurnAdmission(child.id, claim.targetTurnId, {
runId: claim.targetRunId,
userMessageId: 'conflicting-hosted-message',
execution: {
kind: 'claimed_agent_graph_intent',
claim,
agentId: LOCAL_READ_AGENT_ID,
agentName: LOCAL_READ_AGENT_DEFINITION.name,
},
});
await store.appendMessage(child.id, {
type: 'user',
id: 'conflicting-hosted-message',
turnId: claim.targetTurnId,
ts: 35,
text: 'different durable prompt',
});
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(35),
});
await expectRejects(
manager.runClaimedAgentGraphIntent(graphExecutionInput(claim, 'expected durable prompt')),
/does not match its durable UserMessage/,
);
expect(hostedExecutions).toBe(1);
expect(backendBuilds).toBe(0);
const userMessages = (await store.readMessages(child.id)).filter(
(message) => message.type === 'user',
);
expect(userMessages).toHaveLength(1);
expect(userMessages[0]).toMatchObject({
id: 'conflicting-hosted-message',
text: 'different durable prompt',
});
});
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.closePendingHostedLinkedChildAdmission({
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(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: { read: 'allow' },
permissionCeiling: 'ask',
});
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 expectRejects(
manager.setPermissionMode(result.childSessionId, 'execute'),
/exceeds its "ask" ceiling/,
);
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('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('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;
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 respondToPermission(): Promise<void> {},
async dispose(): Promise<void> {},
};
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
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 retried = await manager.retryChildAgent(parent.id, {
parentRunId: parentRun.runId,
sourceRunId: resumed.runId!,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: resumed.runId!,
},
});
expect(retried.status).toBe('completed');
expect(retried.childSessionId).toBe(child.childSessionId);
expect(retried.retriedFromRunId).toBe(resumed.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(resumed.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 respondToPermission(): Promise<void> {},
async dispose(): Promise<void> {},
};
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
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: { ...LOCAL_READ_AGENT_DEFINITION.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;
const originalPolicy = LOCAL_READ_AGENT_DEFINITION.categoryPolicy;
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.';
LOCAL_READ_AGENT_DEFINITION.categoryPolicy = { read: 'block' };
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;
LOCAL_READ_AGENT_DEFINITION.categoryPolicy = originalPolicy;
}
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: { ...LOCAL_READ_AGENT_DEFINITION.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', () => {
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('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('manual compaction begin failure after reservation has no unhandled claim rejection', async () => {
const store = new MemorySessionStore();
const updateHeader = store.updateHeader.bind(store);
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(16_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const beginFailure = new Error('compact running status rejected after reservation');
let failCompactStart = true;
store.updateHeader = async (sessionId, patch) => {
if (failCompactStart && patch.status === 'running') {
failCompactStart = false;
throw beginFailure;
}
return await updateHeader(sessionId, patch);
};
await expectRejects(
collectSessionEvents(
manager.compactSession(session.id, { turnId: 'turn-compact-begin-failure' }),
),
/compact running status rejected after reservation/,
);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(compactCalls).toEqual([]);
});
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 Error).toBe(true);
expect(String((compactError as Error).message)).toMatch(/turn is running|wait for the turn/);
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('backend refresh waits for an active turn and rebuilds on the following turn', async () => {
const store = new MemorySessionStore();
const sendGate = makeGate();
const turnStarted = makeGate();
let builds = 0;
const backends = new BackendRegistry();
backends.register('fake', (ctx) => {
builds += 1;
return new ActiveTurnBackend(ctx, { turnStarted, sendGate, compactCalls: [] });
});
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(26_000),
});
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
const firstTurn = drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hi' }));
await turnStarted.promise;
const refresh = manager.refreshIdleBackends();
expect(store.disposeCount).toBe(0);
sendGate.release();
await firstTurn;
await refresh;
expect(store.disposeCount).toBe(1);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'again' }));
expect(builds).toBe(2);
});
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('updates header, rebuilds active backend, and writes an audit note', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const builtModes: PermissionMode[] = [];
backends.register('fake', (ctx) => {
builtModes.push(ctx.header.permissionMode);
return new TestBackend(ctx);
});
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(1_000) });
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect(builtModes).toEqual(['ask']);
const summary = await manager.setPermissionMode(session.id, 'execute');
expect(summary.permissionMode).toBe('execute');
expect((await store.readHeader(session.id)).permissionMode).toBe('execute');
expect(store.disposeCount).toBe(1);
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: 'ask', to: 'execute' });
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'again' }));
expect(builtModes).toEqual(['ask', 'execute']);
});
test('rejects mode changes 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(2_000) });
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
await expectRejects(manager.setPermissionMode(session.id, 'explore'), /当前对话正在运行/);
expect((await store.readHeader(session.id)).permissionMode).toBe('ask');
gate.release();
await iterator.next();
await iterator.next();
});
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('no-op mode changes do not append duplicate audit notes', 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(3_000) });
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const summary = await manager.setPermissionMode(session.id, 'ask');
expect(summary.permissionMode).toBe('ask');
expect((await store.readMessages(session.id)).length).toBe(0);
});
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('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('metadata-only updates keep the active backend instance', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const built: string[] = [];
backends.register('fake', (ctx) => {
built.push(ctx.header.name);
return new TestBackend(ctx);
});
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(6_000) });
const session = await manager.createSession(makeInput({ name: 'Before' }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
await manager.updateSession(session.id, { name: 'After' });
expect(store.disposeCount).toBe(0);
expect((await store.readHeader(session.id)).titleIsManual).toBe(true);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'again' }));
expect(built).toEqual(['Before']);
});
test('name updates cannot clear the manual-title marker', async () => {
const store = new MemorySessionStore();
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: nextId(),
now: nextNow(6_100),
});
const session = await manager.createSession(makeInput({ name: 'New Chat' }));
await manager.updateSession(session.id, { name: 'Manual title', titleIsManual: false });
expect((await store.readHeader(session.id)).titleIsManual).toBe(true);
});
test('sendMessage delegates through RuntimeKernel while preserving the SessionEvent stream', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
const runtimeKernel = new DelegatingRuntimeKernel([
{
type: 'text_delta',
id: 'delegated-delta',
turnId: 'turn-1',
ts: 1,
messageId: 'm-1',
text: 'hello',
},
{
type: 'complete',
id: 'delegated-complete',
turnId: 'turn-1',
ts: 2,
stopReason: 'end_turn',
},
]);
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(6_250),
runtimeKernel,
});
const session = await manager.createSession(makeInput());
const sessionEvents = await collectSessionEvents(
manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }),
);
expect(runtimeKernel.starts).toEqual([
{ sessionId: session.id, input: { turnId: 'turn-1', text: 'hello' } },
]);
expect(sessionEvents.map((event) => event.id)).toEqual([
'delegated-delta',
'delegated-complete',
]);
expect(sessionEvents.map((event) => event.type)).toEqual(['text_delta', 'complete']);
});
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('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('fake', (ctx) => {
backend = new FinalTextTestBackend(ctx);
return backend;
});
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
inspectContinuationSafety: inspectStableContinuationSafety,
onContinuationLifecycleEvent: (event) => {
lifecycleEvents.push(event);
},
newId: nextId(),
now: nextNow(6_550),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
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',
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',
});
const continuationEvents = await runStore.readRuntimeEvents(
session.id,
plan.continuation.runId,
);
expect(continuationEvents[0]?.actions?.stateDelta).toEqual({ continuationStart: true });
expect(continuationEvents[0]?.refs).toMatchObject({
sourceInvocationId,
sourceRunId,
sourceTurnId,
sourceRuntimeEventHighWater: sourceEvents.length,
});
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']);
});
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)),
/already has a continuation/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 or commit a terminal header when continuation-start persistence fails', 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,
});
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 },
},
];
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('app_restarted');
expect(recoveredEvents.filter(isTerminalRuntimeEvent)).toHaveLength(1);
expect(recoveredEvents.at(-1)?.actions?.stateDelta).toMatchObject({
recovered: true,
recoveryReason: 'run_interrupted',
failureClass: 'app_restarted',
});
});
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;
await manager.stopSession(session.id, { source: 'stop_button' });
releaseContinuationCommit.release();
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?.stateDelta?.continuationStart === true),
).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('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('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('reading messages keeps the session unread marker as a pure query', 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_630),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await store.updateHeader(session.id, { hasUnread: true });
await manager.getMessages(session.id);
expect((await store.readHeader(session.id)).hasUnread).toBe(true);
});
test('markSessionRead clears the session unread marker', 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_631) });
const session = await manager.createSession(makeInput());
await store.updateHeader(session.id, { hasUnread: true, lastMessageAt: 200 });
await manager.markSessionRead(session.id, 200);
expect((await store.readHeader(session.id)).hasUnread).toBe(false);
});
test('markSessionRead keeps unread when a newer message arrives after the read boundary', 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_632) });
const session = await manager.createSession(makeInput());
await store.updateHeader(session.id, { hasUnread: true, lastMessageAt: 250 });
await manager.markSessionRead(session.id, 200);
expect((await store.readHeader(session.id)).hasUnread).toBe(true);
});
test('markSessionRead keeps unread when a newer message finalizes between the read check and write', 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_633) });
const session = await manager.createSession(makeInput());
await store.updateHeader(session.id, { hasUnread: true, lastMessageAt: 200 });
store.interleaveBeforeMarkSessionReadWriteFor.set(session.id, async () => {
await store.updateHeader(session.id, { hasUnread: true, lastMessageAt: 250 });
});
await manager.markSessionRead(session.id, 200);
const header = await store.readHeader(session.id);
expect(header.lastMessageAt).toBe(250);
expect(header.hasUnread).toBe(true);
});
test('markSessionRead rejects when the unread header write fails', 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_634) });
const session = await manager.createSession(makeInput());
await store.updateHeader(session.id, { hasUnread: true, lastMessageAt: 200 });
store.failUpdateHeaderFor.add(session.id);
await expectRejects(manager.markSessionRead(session.id, 200), /Cannot update header/);
store.failUpdateHeaderFor.delete(session.id);
expect((await store.readHeader(session.id)).hasUnread).toBe(true);
});
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 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('getMessages prefers RuntimeEvent-projected messages when legacy rows are present', 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: 'runtime question',
assistantText: 'runtime answer',
legacyIdPrefix: 'legacy',
});
const messages = await manager.getMessages(session.id);
expect(messages).toEqual(seeded.projectedMessages);
expect(
JSON.stringify(messages.map((message) => message.id)) ===
JSON.stringify(seeded.legacyMessages.map((message) => message.id)),
).toBe(false);
});
test('getMessages does not scan runs before reading a complete runtime ledger', 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: 'question',
assistantText: 'answer',
legacyIdPrefix: 'legacy',
});
await manager.getMessages(session.id);
expect(runStore.listSessionRunsCalls).toBe(1);
});
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', 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 converts hosted permission outcome read failure to a hard diagnostic', async () => {
const runStore = new MemoryAgentRunStore();
const header = makeRunHeader({ status: 'completed' });
await seedCanonicalPermissionRun(runStore, header);
await assert.rejects(
new RuntimeReadModel({
runStore,
runtimeEventStore: runStore,
canonicalPermissionOutcomes: {
readPermissionOutcome: async () => {
throw new Error('interaction store read failed');
},
},
}).getSessionView(header.sessionId),
(error: unknown) =>
error instanceof RuntimeReadModelError &&
error.diagnostics.some(
(diagnostic) =>
diagnostic.code === 'incomplete_event' &&
diagnostic.message.includes('readPermissionOutcome failed'),
),
);
});
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 repairs multiple missing terminal facts before returning the runtime view', 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-1', turnId: 'turn-1', ts: 101, text: 'question 1' },
{
type: 'assistant',
id: 'legacy-assistant-1',
turnId: 'turn-1',
ts: 102,
text: 'answer 1',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state-1',
turnId: 'turn-1',
ts: 103,
status: 'completed',
partialOutputRetained: true,
},
{ type: 'user', id: 'legacy-user-2', turnId: 'turn-2', ts: 201, text: 'question 2' },
{
type: 'assistant',
id: 'legacy-assistant-2',
turnId: 'turn-2',
ts: 202,
text: 'answer 2',
modelId: 'fake-model',
},
{
type: 'turn_state',
id: 'legacy-state-2',
turnId: 'turn-2',
ts: 203,
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-1',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 101,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question 1' },
}),
runtimeEvent({
id: 'rt-assistant-1',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 102,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer 1' },
}),
],
);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-2',
turnId: 'turn-2',
status: 'completed',
createdAt: 200,
updatedAt: 203,
completedAt: 203,
}),
[
runtimeEvent({
id: 'rt-user-2',
sessionId: session.id,
runId: 'run-2',
turnId: 'turn-2',
ts: 201,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'question 2' },
}),
runtimeEvent({
id: 'rt-assistant-2',
sessionId: session.id,
runId: 'run-2',
turnId: 'turn-2',
ts: 202,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer 2' },
}),
],
);
const messages = await manager.getMessages(session.id);
const run1TerminalEvents = (await runStore.readRuntimeEvents(session.id, 'run-1')).filter(
isTerminalRuntimeEvent,
);
const run2TerminalEvents = (await runStore.readRuntimeEvents(session.id, 'run-2')).filter(
isTerminalRuntimeEvent,
);
expect(
messages.filter((message) => message.type === 'turn_state').map((message) => message.turnId),
).toEqual(['turn-1', 'turn-2']);
expect(run1TerminalEvents).toHaveLength(1);
expect(run1TerminalEvents[0]?.refs?.storedMessageId).toBe('legacy-state-1');
expect(run2TerminalEvents).toHaveLength(1);
expect(run2TerminalEvents[0]?.refs?.storedMessageId).toBe('legacy-state-2');
});
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 does not trust completed legacy terminal state for a failed 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: 'completed',
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'failed',
failureClass: 'tool_failed',
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 preserves aborted legacy terminal state when abortSource 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: 'aborted',
abortedAt: 103,
partialOutputRetained: true,
},
]);
await seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'cancelled',
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('cancelled');
expect(messages.at(-1)).toEqual({
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 103,
status: 'aborted',
abortedAt: 103,
abortSource: 'unknown',
partialOutputRetained: true,
});
expect(runtimeEvents.at(-1)?.status).toBe('aborted');
expect(runtimeEvents.at(-1)?.actions?.stateDelta?.abortSource).toBe('unknown');
});
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 repairs terminal run headers without terminal evidence as missing_terminal_event failures', 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: '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');
expect(runtimeEvents.at(-1)?.actions?.stateDelta?.failureClass).toBe('missing_terminal_event');
});
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 repairs missing abort source from an existing aborted 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: 'cancelled',
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-aborted',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 103,
role: 'system',
author: 'system',
status: 'aborted',
actions: { endInvocation: true },
}),
],
);
const messages = await manager.getMessages(session.id);
await manager.getMessages(session.id);
const repairedRun = (await runStore.readRun(session.id, 'run-1')) as AgentRunHeader & {
abortSource?: string;
};
const runtimeEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
expect(repairedRun.abortSource).toBe('unknown');
expect(messages.find((message) => message.type === 'turn_state')).toEqual({
type: 'turn_state',
id: 'rt-aborted',
turnId: 'turn-1',
ts: 103,
status: 'aborted',
abortedAt: 103,
abortSource: 'unknown',
partialOutputRetained: true,
});
expect(runtimeEvents.filter((event) => event.status === 'aborted')).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 repairs only the top-level run required by the read model', 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: '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 seedRuntimeRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
parentRunId: 'run-1',
parentTurnId: 'turn-1',
status: 'completed',
createdAt: 110,
updatedAt: 112,
completedAt: 112,
}),
[
runtimeEvent({
id: 'rt-child-text',
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
ts: 111,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'child answer' },
}),
],
);
await manager.getMessages(session.id);
const topLevelEvents = await runStore.readRuntimeEvents(session.id, 'run-1');
const childEvents = await runStore.readRuntimeEvents(session.id, 'child-run');
expect(topLevelEvents.some((event) => event.status === 'failed')).toBe(true);
expect(
childEvents.some((event) => event.status === 'failed' || event.status === 'completed'),
).toBe(false);
});
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', partialOutputRetained: true },
{ turnId: 'turn-2', status: 'running', 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('active RuntimeEvent ledger without a projection cache produces an explicit read-model error', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const session = await store.create(makeInput());
await runStore.createRun(
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
);
await expectRejects(
new RuntimeReadModel({ runStore, runtimeEventStore: runStore }).getSessionView(session.id),
/RuntimeEvent ledger is incomplete for an active run/,
);
});
test('getMessages rejects when runtime ledger read fails', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failRuntimeEventReads: true });
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 question',
assistantText: 'legacy answer',
legacyIdPrefix: 'legacy',
});
await expectRejects(manager.getMessages(session.id), /RuntimeEvent ledger read failed/);
});
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',
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 never leaves a terminal cloned run header without a terminal RuntimeEvent fact', 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).toBeDefined();
const childRuns = await runStore.listSessionRuns(child!.id);
for (const run of childRuns) {
const runtimeEvents = await runStore.readRuntimeEvents(child!.id, 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('getMessages fails fast when RuntimeReadModel stores are not provided', 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_760) });
const session = await manager.createSession(makeInput());
const legacyMessages: StoredMessage[] = [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'legacy only' },
];
await store.appendMessages(session.id, legacyMessages);
await expectRejects(
manager.getMessages(session.id),
/RuntimeReadModel requires AgentRunStore and RuntimeEventStore/,
);
});
test('next turn receives complete prior RuntimeEvent context and projection 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_800),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first' }));
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.context.some((message) => message.type === 'user' && message.turnId === 'turn-1'),
).toBe(true);
expect(
secondInput.context.some((message) => message.type === 'user' && message.turnId === 'turn-2'),
).toBe(false);
expect(secondInput.runtimeContext?.map((event) => event.turnId)).toEqual([
'turn-1',
'turn-1',
'turn-1',
]);
expect(secondInput.runtimeContext?.map((event) => event.role)).toEqual([
'user',
'model',
'system',
]);
expect(secondInput.runtimeContext?.[0]?.content).toEqual({ kind: 'text', text: 'first' });
});
test('next turn uses prior RuntimeEvents when terminal header commit was interrupted', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'completed' });
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_805),
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');
expect(
(await runStore.readRuntimeEvents(session.id, firstRun.runId)).some(isTerminalRuntimeEvent),
).toBe(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?.map((event) => event.role)).toEqual([
'user',
'model',
'system',
]);
expect(secondInput.runtimeContext?.[0]?.content).toEqual({ kind: 'text', text: 'first' });
});
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 resolves an expert member id to a tool-scoped, persona-injected child', 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,
// The full same-workspace child tool surface; the expert must be scoped
// down to just its archetype's tools (Read/Glob/Grep for local_read).
childTools: [
testTool('Read'),
testTool('Bash'),
testTool('Glob'),
testTool('WebSearch'),
testTool('Grep'),
...AGENT_TEAM_CHILD_TOOL_NAMES.map((name) => ({
...testTool(name),
categoryHint: 'read' as const,
})),
],
newId: nextId(),
now: nextNow(7_200),
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 team = getExpertTeam('code-review');
if (!team) throw new Error('code-review team missing');
const member = team.members[0]!;
const expertId = buildExpertAgentId(team.id, member.id);
const expected = materializeExpertAgentDefinition(team, member);
await drain(
manager.startChildTurn(session.id, {
turnId: 'child-turn',
parentRunId: parentRun.runId,
spec: { id: expertId, name: 'ignored', systemPrompt: 'ignored' },
prompt: 'review src/foo.ts',
}),
);
// The resolver flowed the materialized expert definition through the child
// turn: read-only archetype scope + composed persona + explore permission.
expect(contexts.map((ctx) => ctx.header.permissionMode)).toEqual(['ask', 'explore']);
expect(contexts[1]?.tools?.map((tool) => tool.name)).toEqual([
'Read',
'Glob',
'Grep',
...AGENT_TEAM_CHILD_TOOL_NAMES,
]);
expect(contexts[1]?.systemPrompt).toBe(expected.systemPrompt);
expect(contexts[1]?.agentTeam).toEqual({
role: 'member',
teamId: team.id,
agentId: expertId,
parentRunId: parentRun.runId,
});
const childRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.turnId === 'child-turn',
);
expect(childRun?.agentId).toBe(expertId);
expect(childRun?.agentName).toBe(member.name);
expect(childRun?.permissionMode).toBe('explore');
});
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 respondToPermission(): Promise<void> {},
async dispose(): Promise<void> {},
}));
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 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(retryRun.continuationSource).toBe(undefined);
expect(
(await store.readMessages(session.id)).filter(
(message) => 'turnId' in message && message.turnId === retried.turnId,
),
).toEqual([]);
});
test('omits the durable-reader capability when no RuntimeEventStore is configured', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
let context: BackendFactoryContext | undefined;
backends.register('fake', (ctx) => {
context = ctx;
return new TestBackend(ctx);
});
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(6_849),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect(context?.loadTurnRuntimeEvents).toBe(undefined);
});
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('public child resume is stop-visible during its 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 builtBackends: TestBackend[] = [];
backends.register('fake', (ctx) => {
const backend = new TestBackend(ctx);
builtBackends.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' }));
const source = await manager.spawnChildAgent(session.id, {
turnId: 'child-resume-source',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'complete source',
});
expect(source.status).toBe('completed');
expect(source.runId).toBeDefined();
gateLedgerRead = true;
const resumed = manager.resumeChildAgent(session.id, {
parentRunId: 'parent-run',
sourceRunId: source.runId!,
prompt: 'must stop before provider replay',
});
await ledgerReadStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { source: 'stop_button' }).finally(() => {
stopSettled = true;
});
await Promise.resolve();
expect(stopSettled).toBe(false);
releaseLedgerRead.release();
const [result] = await Promise.all([resumed, stopping]);
expect(result.status).toBe('cancelled');
expect(
builtBackends.flatMap((backend) => backend.sendInputs.map((input) => input.turnId)),
).toEqual(['child-resume-source']);
const resumedRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.resumedFromRunId === source.runId,
);
expect(resumedRun?.status).toBe('cancelled');
expect((await store.readHeader(session.id)).status === 'blocked').toBe(false);
});
test('public child retry is stop-visible during its 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 sendInputs: BackendSendInput[] = [];
const backends = new BackendRegistry();
backends.register('fake', (ctx) => ({
kind: 'fake' as const,
sessionId: ctx.sessionId,
async *send(input: BackendSendInput): AsyncIterable<SessionEvent> {
sendInputs.push(input);
yield {
type: 'error',
id: `${input.turnId}-rate-limit`,
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 respondToPermission(): Promise<void> {},
async dispose(): Promise<void> {},
}));
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 source = await manager.spawnChildAgent(session.id, {
turnId: 'child-retry-source',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'fail with a provider rate limit',
});
expect(source.status).toBe('failed');
expect(source.failureClass).toBe('RateLimit');
if (!source.runId) throw new Error('rate-limited child run id was not recorded');
gateLedgerRead = true;
const retried = manager.retryChildAgent(session.id, {
parentRunId: 'parent-run',
sourceRunId: source.runId,
});
await ledgerReadStarted.promise;
let stopSettled = false;
const stopping = manager.stopSession(session.id, { 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(source.runId);
expect(sendInputs.map((input) => input.turnId)).toEqual(['child-retry-source']);
const retryRun = (await runStore.listSessionRuns(session.id)).find(
(run) => run.retriedFromRunId === source.runId,
);
expect(retryRun?.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 respondToPermission(): 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 respondToPermission(): 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 and one control broadcast target', 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 manager.respondToPermission(session.id, {
requestId: 'control-broadcast',
decision: 'deny',
});
expect(backend?.permissionResponses).toBe(1);
expect(builds).toBe(1);
releaseSend.release();
while (!(await first.next()).done) {}
while (!(await second.next()).done) {}
expect(backend?.sendInputs).toHaveLength(2);
});
test('concurrent cold child starts share their route generation without an orphan', 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,
childTools: [testTool('Read'), testTool('Glob'), testTool('Grep')],
newId: nextId(),
now: nextNow(6_847),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
const childInput = {
turnId: 'cold-child-turn',
parentRunId: 'parent-run',
spec: { id: LOCAL_READ_AGENT_ID, name: 'Researcher', systemPrompt: 'read only' },
prompt: 'share one child route',
};
const first = manager.startChildTurn(session.id, childInput)[Symbol.asyncIterator]();
const second = manager.startChildTurn(session.id, childInput)[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();
const outcomes = await Promise.allSettled([firstEvent, secondEvent]);
expect(outcomes.filter((outcome) => outcome.status === 'fulfilled')).toHaveLength(1);
expect(outcomes.filter((outcome) => outcome.status === 'rejected')).toHaveLength(1);
await manager.respondToPermission(session.id, {
requestId: 'child-control-broadcast',
decision: 'deny',
});
expect(backend?.permissionResponses).toBe(1);
expect(builds).toBe(1);
releaseSend.release();
const activeChild = outcomes[0]?.status === 'fulfilled' ? first : second;
while (!(await activeChild.next()).done) {}
expect(backend?.sendInputs).toHaveLength(1);
});
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,
});
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('modelReplay' in output).toBe(false);
expect('projection' in output).toBe(false);
});
test('agent output rejects ambiguous child run locators', 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_850),
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: 130,
completedAt: 130,
parentRunId: 'parent-run',
agentId: LOCAL_READ_AGENT_ID,
agentName: 'Researcher',
permissionMode: 'explore',
}),
);
await expectRejects(
manager.readChildAgentOutput(session.id, { runId: 'child-run', turnId: 'child-turn' }),
/exactly one execution, runId, or turnId/,
);
});
test('next turn still receives RuntimeEvent context when projection cache has extra rows', 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_850),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first' }));
await store.appendMessage(session.id, {
type: 'assistant',
id: 'legacy-extra-assistant',
turnId: 'legacy-extra',
ts: 6_899,
text: 'cache-only context',
modelId: 'fake-model',
});
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.context.some(
(message) => message.type === 'assistant' && message.id === 'legacy-extra-assistant',
),
).toBe(false);
});
test('next turn repairs a prior RuntimeEvent ledger before building 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_900),
runtimeSource: 'test',
});
const session = await manager.createSession(makeInput());
await store.appendMessages(session.id, [
{ type: 'user', id: 'legacy-user', turnId: 'turn-1', ts: 101, text: 'first' },
{
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: 'first' },
}),
],
);
await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' }));
const firstInput = backendInstances[0]?.sendInputs[0];
if (!firstInput) throw new Error('backend input was not recorded');
expect(
firstInput.runtimeContext?.map((event) => ({
role: event.role,
text: event.content?.kind === 'text' ? event.content.text : undefined,
status: event.status,
})),
).toEqual([
{ role: 'user', text: 'first', status: undefined },
{ role: 'model', text: 'answer', status: undefined },
{ role: 'system', text: undefined, status: 'completed' },
]);
expect(backendInstances[0]?.sendInputs.length ?? 0).toBe(1);
});
test('RuntimeKernel turn runner uses AiSdkFlow instead of an inline mapper flow', async () => {
const source = await readFile(new URL('../../src/runtime-kernel.ts', import.meta.url), 'utf8');
const turnRunnerSource = source.slice(
source.indexOf('private async *runAgentTurn'),
source.indexOf('async stopSession'),
);
expect(turnRunnerSource.includes('new AiSdkFlow')).toBe(true);
expect(turnRunnerSource.includes('mapSessionEventToRuntimeEvent')).toBe(false);
expect(turnRunnerSource.includes('createSessionEventMapMemory')).toBe(false);
});
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 session running while a turn is in flight and active after completion', 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(8_000) });
const session = await manager.createSession(makeInput());
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })
[Symbol.asyncIterator]();
await iterator.next();
expect((await store.readHeader(session.id)).status).toBe('running');
gate.release();
await iterator.next();
await iterator.next();
const header = await store.readHeader(session.id);
expect(header.status).toBe('active');
expect(header.blockedReason).toBe(undefined);
const turns = await store.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'turn-1')?.status).toBe('completed');
});
test('marks permission handoff as waiting_for_user', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new EventBackend(ctx, [
{
type: 'permission_request',
kind: 'tool_permission',
requestId: 'pr-1',
toolUseId: 'tool-1',
toolName: 'Bash',
category: 'shell_safe',
reason: 'custom',
args: {},
rememberForTurnAllowed: true,
},
{ type: 'complete', stopReason: 'permission_handoff' },
]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_000),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const header = await store.readHeader(session.id);
expect(header.status).toBe('waiting_for_user');
expect(header.blockedReason).toBe(undefined);
const [run] = await runStore.listSessionRuns(session.id);
const events = await runStore.readEvents(session.id, run!.runId);
expect(
events.some(
(event) =>
event.type === 'run_status_changed' && event.data?.sessionStatus === 'waiting_for_user',
),
).toBe(true);
});
test('startup recovery does not treat permission handoff as a completed run fact', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new EventBackend(ctx, [
{
type: 'permission_request',
kind: 'tool_permission',
requestId: 'pr-1',
toolUseId: 'tool-1',
toolName: 'Bash',
category: 'shell_safe',
reason: 'custom',
args: {},
rememberForTurnAllowed: true,
},
{ type: 'complete', stopReason: 'permission_handoff' },
]),
);
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(9_010),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [runBeforeRecovery] = await runStore.listSessionRuns(session.id);
expect(runBeforeRecovery?.status).toBe('waiting_for_user');
await manager.recoverInterruptedSessions();
const [runAfterRecovery] = await runStore.listSessionRuns(session.id);
expect(runAfterRecovery?.status).toBe('failed');
expect(runAfterRecovery?.failureClass).toBe('app_restarted');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('app_restarted');
const terminalEvents = (
await runStore.readRuntimeEvents(session.id, runAfterRecovery!.runId)
).filter(isTerminalRuntimeEvent);
expect(terminalEvents).toHaveLength(1);
expect(terminalEvents[0]?.status).toBe('failed');
});
test('rejects mode changes while a tool permission request is waiting', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) =>
new EventBackend(ctx, [
{
type: 'permission_request',
kind: 'tool_permission',
requestId: 'pr-1',
toolUseId: 'tool-1',
toolName: 'Bash',
category: 'shell_safe',
reason: 'custom',
args: {},
rememberForTurnAllowed: true,
},
{ type: 'complete', stopReason: 'permission_handoff' },
]),
);
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(9_500) });
const session = await manager.createSession(makeInput({ permissionMode: 'ask' }));
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
await expectRejects(
manager.setPermissionMode(session.id, 'execute'),
/当前有工具调用正在等待确认/,
);
expect((await store.readHeader(session.id)).permissionMode).toBe('ask');
const messages = await store.readMessages(session.id);
expect(
messages.some((message) => message.type === 'system_note' && message.kind === 'mode_change'),
).toBe(false);
});
test('marks backend errors as blocked with a generalized reason', 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' },
]),
);
const manager = new SessionManager({ store, 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 header = await store.readHeader(session.id);
expect(header.status).toBe('blocked');
expect(header.blockedReason).toBe('tool_failed');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('tool_failed');
});
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('marks aborts as aborted', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register(
'fake',
(ctx) => new EventBackend(ctx, [{ type: 'abort', reason: 'user_stop' }]),
);
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(11_000) });
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const header = await store.readHeader(session.id);
expect(header.status).toBe('aborted');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('aborted');
expect(turn?.partialOutputRetained).toBe(false);
});
test('cancel keeps partial assistant output and marks the turn aborted', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new PartialAbortBackend(ctx));
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_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('aborted');
expect(turn?.partialOutputRetained).toBe(true);
expect(
(await store.readMessages(session.id)).some(
(message) =>
message.type === 'assistant' &&
message.turnId === 'turn-1' &&
message.text === 'partial answer',
),
).toBe(true);
});
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 keeps aborted state even if the backend emits a late completion', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
const gate = makeGate();
backends.register('fake', (ctx) => new TestBackend(ctx, gate));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_700),
});
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();
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');
const events = await runStore.readEvents(session.id, run!.runId);
expect(events.map((event) => event.type)).toContain('run_cancelled');
});
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('usage_recorded');
expect(events.map((event) => event.type)).toContain('run_completed');
expect(JSON.stringify(events).includes('sk-live-secret-token-value')).toBe(false);
});
test('durable run ledger records provider request capture metadata and complete attempt segments', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
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_760),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [run] = await runStore.listSessionRuns(session.id);
const events = await runStore.readEvents(session.id, run!.runId);
const captured = events.find((event) => event.type === 'provider_request_captured');
const attempt = events.find((event) => event.type === 'provider_request_attempt_recorded');
expect(captured?.data?.artifactId).toBe('artifact-capture');
expect(captured?.data?.requestHash).toBe('sha256:request');
expect(attempt?.data?.captureId).toBe('capture-1');
expect((attempt?.data?.segments as unknown[])?.length).toBe(75);
});
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('omits provider request telemetry hooks when no run store is configured', async () => {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
let captureHook: BackendFactoryContext['recordProviderRequestCapture'];
let attemptHook: BackendFactoryContext['recordProviderRequestAttempt'];
backends.register('fake', (ctx) => {
captureHook = ctx.recordProviderRequestCapture;
attemptHook = ctx.recordProviderRequestAttempt;
return new FakeBackend(ctx);
});
const manager = new SessionManager({
store,
backends,
newId: nextId(),
now: nextNow(12_765),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
expect(captureHook).toBeUndefined();
expect(attemptHook).toBeUndefined();
});
test('durable run ledger records full active compact blocks asynchronously', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new ActiveCompactBlockBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_775),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [run] = await runStore.listSessionRuns(session.id);
const events = await runStore.readEvents(session.id, run!.runId);
const compactEvent = events.find(
(event) => event.type === 'active_full_compact_block_recorded',
);
expect(compactEvent?.data?.blockId).toBe('afcompact-test');
expect(compactEvent?.data?.boundaryKind).toBe('activeFullCompact');
const block = compactEvent?.data?.block as ActiveFullCompactBlock | undefined;
expect(block?.kind).toBe('maka.active_full_compact_block');
expect(block?.summary.text).toBe('persist the full active compact block');
expect(block?.sourceRefs[0]?.sourceId).toBe('provider-message:0');
});
test('durable run ledger records bounded history compact checkpoints asynchronously', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new HistoryCompactCheckpointBackend(ctx));
const manager = new SessionManager({
store,
runStore,
runtimeEventStore: runStore,
backends,
newId: nextId(),
now: nextNow(12_790),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' }));
const [run] = await runStore.listSessionRuns(session.id);
const events = await runStore.readEvents(session.id, run!.runId);
const checkpointEvent = events.find(
(event) => event.type === 'history_compact_checkpoint_recorded',
);
expect(checkpointEvent?.data?.checkpointId).toBe('hcheckpoint-test');
expect(checkpointEvent?.data?.boundaryKind).toBe('historyCompact');
const checkpoint = checkpointEvent?.data?.checkpoint as HistoryCompactCheckpoint | undefined;
expect(checkpoint?.summary).toBe('persist the bounded checkpoint');
});
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('does not expose a durable checkpoint recorder without an AgentRun store', async () => {
const store = new MemorySessionStore();
let recorderExposed: boolean | undefined;
const backends = new BackendRegistry();
backends.register('fake', (ctx) => {
recorderExposed = ctx.recordHistoryCompactCheckpoint !== undefined;
return new TestBackend(ctx);
});
const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_799) });
const session = await manager.createSession(makeInput());
await drain(
manager.sendMessage(session.id, { turnId: 'turn-without-run-store', text: 'hello' }),
);
expect(recorderExposed).toBe(false);
});
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 uses AgentRun ledger to fail stale running model-started runs', 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_810),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_started',
ts: 12,
}),
],
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([session.id]);
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('app_restarted');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('app_restarted');
const events = await runStore.readEvents(session.id, 'run-1');
expect(events.map((event) => event.type)).toContain('run_failed');
});
test('startup recovery fails stale child runs without writing child turn_state into the parent transcript', 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_812),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'parent-turn');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'parent-run',
turnId: 'parent-turn',
type: 'tool_started',
ts: 12,
}),
],
);
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
status: 'running',
parentRunId: 'parent-run',
agentName: 'Researcher',
permissionMode: 'explore',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-turn',
type: 'run_started',
ts: 13,
}),
makeRunEvent({
sessionId: session.id,
runId: 'child-run',
turnId: 'child-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.some((message) => message.type === 'turn_state' && message.turnId === 'child-turn'),
).toBe(false);
const childRun = await runStore.readRun(session.id, 'child-run');
expect(childRun.status).toBe('failed');
expect(childRun.failureClass).toBe('app_restarted');
const childEvents = await runStore.readEvents(session.id, 'child-run');
expect(childEvents.map((event) => event.type)).toContain('run_failed');
});
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 uses a completed RuntimeEvent terminal fact before incomplete AgentRun 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_815),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_started',
ts: 12,
}),
],
);
await runStore.appendRuntimeEvent(
session.id,
'run-1',
runtimeEvent({
id: 'rt-completed',
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
ts: 13,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([session.id]);
expect((await store.readHeader(session.id)).status).toBe('active');
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('completed');
const storedTurnStates = (await store.readMessages(session.id)).filter(
(message) => message.type === 'turn_state' && message.turnId === 'turn-1',
);
expect(
storedTurnStates.map((message) => (message.type === 'turn_state' ? message.status : '')),
).toEqual(['running', 'completed']);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('completed');
const events = await runStore.readEvents(session.id, 'run-1');
expect(events.map((event) => event.type)).toEqual([
'run_started',
'model_stream_started',
'run_completed',
]);
const recoveredEvent = events.find((event) => event.type === 'run_completed');
expect(recoveredEvent?.data?.recoveryReason).toBe('runtime_event_terminal_fact');
});
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 fails stale tool tails while preserving partial output retention', 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_820),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'turn-1');
await store.appendMessage(session.id, {
type: 'assistant',
id: 'partial-assistant',
turnId: 'turn-1',
ts: 13,
text: 'partial output',
modelId: 'fake-model',
});
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'tool_started',
ts: 12,
}),
],
);
await manager.recoverInterruptedSessions();
const [turn] = await store.listTurns(session.id);
expect(turn?.status).toBe('failed');
expect(turn?.errorClass).toBe('app_restarted');
expect(turn?.partialOutputRetained).toBe(true);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('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 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');
expect(turn?.errorClass).toBe('app_restarted');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('app_restarted');
});
test('startup recovery repairs stale completed model tails without leaving running runs', 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_840),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'model_stream_completed',
ts: 12,
}),
],
);
await manager.recoverInterruptedSessions();
expect((await store.readHeader(session.id)).status).toBe('active');
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status === 'running' || run?.status === 'waiting_for_user').toBe(false);
const [turn] = await store.listTurns(session.id);
expect(turn?.status === 'running').toBe(false);
});
test('startup recovery tolerates corrupt AgentRun events and records a conservative failed state', 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_850),
});
const session = await manager.createSession(makeInput({ status: 'running' }));
await seedRunningTurn(store, session.id, 'turn-1');
await seedRun(
runStore,
makeRunHeader({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
status: 'running',
}),
[
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'run_started',
ts: 11,
}),
makeRunEvent({
sessionId: session.id,
runId: 'run-1',
turnId: 'turn-1',
type: 'event_corrupt',
ts: 12,
message: 'Invalid AgentRun event JSONL line',
}),
],
);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([session.id]);
const [run] = await runStore.listSessionRuns(session.id);
expect(run?.status).toBe('failed');
expect(run?.failureClass).toBe('app_restarted');
const events = await runStore.readEvents(session.id, 'run-1');
expect(events.map((event) => event.type)).toContain('event_corrupt');
expect(events.map((event) => event.type)).toContain('run_failed');
});
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('startup recovery does not leave persisted running sessions stuck when message read fails', 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_900) });
const running = await manager.createSession(makeInput({ status: 'running' }));
const active = await manager.createSession(makeInput({ status: 'active' }));
store.failReadMessagesFor.add(running.id);
store.failReadMessagesFor.add(active.id);
const recovered = await manager.recoverInterruptedSessions();
expect(recovered).toEqual([running.id]);
expect((await store.readHeader(running.id)).status).toBe('active');
expect((await store.readHeader(active.id)).status).toBe('active');
});
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('regenerate creates a new sibling turn from a completed source 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(14_000),
});
const session = await manager.createSession(makeInput());
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'answer this' }));
await drain(manager.regenerateTurn(session.id, { sourceTurnId: 'source', turnId: 'regen-1' }));
const turns = await store.listTurns(session.id);
expect(turns.find((turn) => turn.turnId === 'source')?.status).toBe('completed');
const regen = turns.find((turn) => turn.turnId === 'regen-1');
expect(regen?.status).toBe('completed');
expect(regen?.regeneratedFromTurnId).toBe('source');
});
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 preserves an explicit no-project association', 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_100),
});
const session = await manager.createSession(makeInput({ projectId: null }));
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
const child = await manager.branchFromTurn(session.id, { sourceTurnId: 'source' });
expect(child.projectId).toBe(null);
expect((await store.readHeader(child.id)).projectId).toBe(null);
});
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 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);
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);
});
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('reviseBeforeTurn preserves an explicit no-project association', 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({ projectId: null }));
await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
const revision = await manager.reviseBeforeTurn(session.id, { sourceTurnId: 'source' });
expect(revision.projectId).toBe(null);
expect((await store.readHeader(revision.id)).projectId).toBe(null);
});
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,
acceptPermissionRequest: async () => ({ state: 'pending' }),
commitPermissionAnswer: async ({ answer }) => ({
kind: 'permission_answer',
answer,
}),
commitPermissionTimeout: async () => ({ kind: 'closure', reason: 'timed_out' }),
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 permission answer waits for its durable ack before resuming and entering the tool', {
timeout: 2_000,
}, async (t) => {
const ackAppendStarted = makeGate();
const releaseAckAppend = makeGate();
t.after(() => releaseAckAppend.release());
let toolExecutions = 0;
let continuation: RuntimePermissionContinuation | undefined;
let answerPermission: (() => Promise<void>) | undefined;
let ackDurable: boolean | undefined;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventAppend: async (_sessionId, _runId, event, options) => {
if (!event.actions?.permissionAnswerAccepted) return;
ackDurable = options?.durable;
ackAppendStarted.release();
await releaseAckAppend.promise;
},
});
const interactionAuthority: RuntimeInteractionAuthority = {
bindRun: (identity) => ({
...identity,
acceptPermissionRequest: async ({ continuation: accepted }) => {
continuation = accepted;
answerPermission = () => accepted.applyAnswer({ decision: 'allow' });
return { state: 'pending' };
},
commitPermissionAnswer: async ({ continuation: accepted, answer }) => {
await accepted.applyAnswer(answer);
return { kind: 'permission_answer', answer };
},
commitPermissionTimeout: async ({ continuation: accepted }) => {
await accepted.applyClosure('timed_out');
return { kind: 'closure', reason: 'timed_out' };
},
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {},
}),
};
const model = steeringToolThenDoneModel(true);
const { manager, session, store } = await steeringDeliverySession(
runStore,
model,
async () => {
const [run] = await runStore.listSessionRuns(session.id);
const runtimeEvents = await runStore.readRuntimeEvents(session.id, run!.runId);
expect(runtimeEvents.some((event) => event.actions?.permissionAnswerAccepted)).toBe(true);
expect(run?.status).toBe('running');
expect((await store.readHeader(session.id)).status).toBe('running');
toolExecutions += 1;
},
{
permissionRequired: true,
permissionMode: 'ask',
interactionAuthority,
},
);
const turn = drainAll(
manager.sendMessage(
session.id,
{ turnId: 'turn-hosted-answer-durable', text: 'go' },
{ durability: 'required' },
),
);
await waitUntil(() => continuation !== undefined);
await waitUntil(async () => {
const [run] = await runStore.listSessionRuns(session.id);
return (
run?.status === 'waiting_for_user' &&
(await store.readHeader(session.id)).status === 'waiting_for_user'
);
});
await answerPermission!();
await ackAppendStarted.promise;
expect(ackDurable).toBe(true);
const [waitingRun] = await runStore.listSessionRuns(session.id);
expect(toolExecutions).toBe(0);
expect(waitingRun?.status).toBe('waiting_for_user');
expect((await store.readHeader(session.id)).status).toBe('waiting_for_user');
expect(
(await runStore.readRuntimeEvents(session.id, waitingRun!.runId)).some(
(event) => event.actions?.permissionAnswerAccepted,
),
).toBe(false);
releaseAckAppend.release();
await turn;
expect(toolExecutions).toBe(1);
expect(model.doStreamCalls.length).toBe(2);
});
test('hosted permission answer fails stop when its durable ack append is rejected', {
timeout: 2_000,
}, async () => {
let toolExecutions = 0;
let continuation: RuntimePermissionContinuation | undefined;
let answerPermission: (() => Promise<void>) | undefined;
let ackDurable: boolean | undefined;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventAppend: (_sessionId, _runId, event, options) => {
if (!event.actions?.permissionAnswerAccepted) return;
ackDurable = options?.durable;
throw new Error('permission answer ack append rejected');
},
});
const interactionAuthority: RuntimeInteractionAuthority = {
bindRun: (identity) => ({
...identity,
acceptPermissionRequest: async ({ continuation: accepted }) => {
continuation = accepted;
answerPermission = () => accepted.applyAnswer({ decision: 'allow' });
return { state: 'pending' };
},
commitPermissionAnswer: async ({ continuation: accepted, answer }) => {
await accepted.applyAnswer(answer);
return { kind: 'permission_answer', answer };
},
commitPermissionTimeout: async ({ continuation: accepted }) => {
await accepted.applyClosure('timed_out');
return { kind: 'closure', reason: 'timed_out' };
},
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {},
}),
};
const { manager, session } = await steeringDeliverySession(
runStore,
steeringToolThenDoneModel(true),
() => {
toolExecutions += 1;
},
{
permissionRequired: true,
permissionMode: 'ask',
interactionAuthority,
},
);
const turn = drainAll(
manager.sendMessage(
session.id,
{ turnId: 'turn-hosted-answer-rejected', text: 'go' },
{ durability: 'required' },
),
);
await waitUntil(() => continuation !== undefined);
await answerPermission!();
let failure: unknown;
try {
await turn;
} catch (error) {
failure = error;
}
expect((failure as Error).message).toBe('permission answer ack append rejected');
expect(ackDurable).toBe(true);
expect(toolExecutions).toBe(0);
const [run] = await runStore.listSessionRuns(session.id);
expect(
(await runStore.readRuntimeEvents(session.id, run!.runId)).some(
(event) => event.actions?.permissionAnswerAccepted,
),
).toBe(false);
});
test('hosted permission timeout waits for its durable closure ack before continuing with a synthetic error', {
timeout: 2_000,
}, async (t) => {
const ackAppendStarted = makeGate();
const releaseAckAppend = makeGate();
t.after(() => releaseAckAppend.release());
let toolExecutions = 0;
let closureApplications = 0;
let ackDurable: boolean | undefined;
const runStore = new MemoryAgentRunStore({
beforeRuntimeEventAppend: async (_sessionId, _runId, event, options) => {
if (!event.actions?.permissionClosureAccepted) return;
ackDurable = options?.durable;
ackAppendStarted.release();
await releaseAckAppend.promise;
},
});
const interactionAuthority: RuntimeInteractionAuthority = {
bindRun: (identity) => ({
...identity,
acceptPermissionRequest: async () => ({ state: 'pending' }),
commitPermissionAnswer: async ({ continuation, answer }) => {
await continuation.applyAnswer(answer);
return { kind: 'permission_answer', answer };
},
commitPermissionTimeout: async ({ continuation }) => {
closureApplications += 1;
await continuation.applyClosure('timed_out');
return { kind: 'closure', reason: 'timed_out' };
},
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {},
}),
};
const model = steeringToolThenDoneModel(true);
const { manager, session, store } = await steeringDeliverySession(
runStore,
model,
() => {
toolExecutions += 1;
},
{
permissionRequired: true,
permissionMode: 'ask',
permissionTimeoutMs: 5,
interactionAuthority,
},
);
const turn = drainAll(
manager.sendMessage(
session.id,
{ turnId: 'turn-hosted-timeout-durable', text: 'go' },
{ durability: 'required' },
),
);
await ackAppendStarted.promise;
expect(ackDurable).toBe(true);
const [waitingRun] = await runStore.listSessionRuns(session.id);
expect(closureApplications).toBe(1);
expect(toolExecutions).toBe(0);
expect(waitingRun?.status).toBe('waiting_for_user');
expect((await store.readHeader(session.id)).status).toBe('waiting_for_user');
expect(
(await runStore.readRuntimeEvents(session.id, waitingRun!.runId)).some(
(event) => event.actions?.permissionClosureAccepted,
),
).toBe(false);
expect(model.doStreamCalls.length).toBe(1);
releaseAckAppend.release();
const events = await turn;
const runtimeEvents = await runStore.readRuntimeEvents(session.id, waitingRun!.runId);
expect(
runtimeEvents.some(
(event) => event.actions?.permissionClosureAccepted?.reason === 'timed_out',
),
).toBe(true);
expect(
runtimeEvents.some(
(event) => event.content?.kind === 'function_response' && event.content.isError === true,
),
).toBe(true);
expect(events.some((event) => event.type === 'permission_closure_ack')).toBe(true);
expect(toolExecutions).toBe(0);
expect(model.doStreamCalls.length).toBe(2);
});
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,
acceptPermissionRequest: async () => ({ state: 'pending' }),
commitPermissionAnswer: async ({ answer }) => ({
kind: 'permission_answer',
answer,
}),
commitPermissionTimeout: async () => ({ kind: 'closure', reason: 'timed_out' }),
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,
acceptPermissionRequest: async () => ({ state: 'pending' }),
commitPermissionAnswer: async ({ answer }) => ({
kind: 'permission_answer',
answer,
}),
commitPermissionTimeout: async () => ({ kind: 'closure', reason: 'timed_out' }),
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 the event consumer abandons the run', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
const backends = new BackendRegistry();
backends.register('fake', (ctx) => new FakeBackend(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' }),
);
const iterator = manager
.sendMessage(session.id, { turnId: 'turn-host-abandoned', text: 'start' })
[Symbol.asyncIterator]();
await iterator.next();
await iterator.return?.(undefined);
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('steer injects a user message mid-turn and emits queue snapshots', async () => {
const { manager } = steeringManager();
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
let steerOutcome: unknown;
const events = await runTurnWith(manager, session.id, 'turn-1', () => {
steerOutcome = manager.steer(session.id, 'also do X');
});
expect(steerOutcome).toEqual({ kind: 'queued' });
// The interjection is echoed as a first-class user event…
expect(
events.some(
(event) => event.type === 'steering_message' && event.content.text === 'also do X',
),
).toBe(true);
// …the enqueue and the step-boundary consumption both push a queue snapshot…
const queueUpdates = events.filter(
(event): event is Extract<SessionEvent, { type: 'queue_update' }> =>
event.type === 'queue_update',
);
expect(queueUpdates.some((event) => event.steering.length === 1)).toBe(true);
expect(queueUpdates.at(-1)?.steering).toEqual([]);
// …and it lands in the durable ledger as a user message, in this turn.
const messages = await manager.getMessages(session.id);
expect(
messages.some((message) => message.type === 'user' && message.text === 'also do X'),
).toBe(true);
// The queues are drained by turn end.
expect(manager.drainFollowup(session.id)).toBe(null);
});
test('queued followups drain at turn end joined with blank lines', async () => {
const { manager } = steeringManager();
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await runTurnWith(manager, session.id, 'turn-1', () => {
expect(manager.queueMessage(session.id, 'first').kind).toBe('queued');
const second = manager.queueMessage(session.id, 'second');
expect(second).toEqual({ kind: 'queued' });
});
// Followups are never injected mid-turn; they wait for the drain.
expect(manager.drainFollowup(session.id)).toBe('first\n\nsecond');
expect(manager.drainFollowup(session.id)).toBe(null);
});
test('retract returns and clears both queues', async () => {
const { manager } = steeringManager();
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
let retracted: string | undefined;
await runTurnWith(manager, session.id, 'turn-1', () => {
manager.steer(session.id, 'steer me');
manager.queueMessage(session.id, 'queue me');
retracted = manager.retractQueue(session.id);
});
expect(retracted).toBe('steer me\n\nqueue me');
expect(manager.drainFollowup(session.id)).toBe(null);
});
test('interrupt clears both queues', async () => {
const { manager } = steeringManager();
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
await runTurnWith(manager, session.id, 'turn-1', () => {
manager.steer(session.id, 'steer me');
manager.queueMessage(session.id, 'queue me');
void manager.stopSession(session.id, { source: 'stop_button' });
});
expect(manager.drainFollowup(session.id)).toBe(null);
expect(manager.retractQueue(session.id)).toBe('');
});
test('steer with no active run falls back so nothing is dropped', async () => {
const { manager } = steeringManager();
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: 'bypass' }),
);
// No turn is running — the caller is told to open a fresh turn instead.
expect(manager.steer(session.id, 'x')).toEqual({ kind: 'fallback' });
expect(manager.queueMessage(session.id, 'y')).toEqual({ kind: 'fallback' });
});
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(permissionRequired = false): 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: permissionRequired ? 'Bash' : 'Probe',
input: JSON.stringify(permissionRequired ? { command: 'echo x' } : { 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,
options: {
permissionRequired?: boolean;
permissionMode?: PermissionMode;
permissionTimeoutMs?: number;
interactionAuthority?: RuntimeInteractionAuthority;
} = {},
) {
const store = new MemorySessionStore();
const backends = new BackendRegistry();
let manager!: SessionManager;
let sessionId = '';
backends.register(
'fake',
(ctx) =>
new AiSdkBackend({
sessionId: ctx.sessionId,
header: ctx.header,
appendMessage: async () => {},
connection: {
slug: 'mock-main',
name: 'Mock',
providerType: 'anthropic',
defaultModel: 'mock-model-id',
enabled: true,
createdAt: 1,
updatedAt: 1,
} satisfies LlmConnection,
apiKey: 'sk-test',
modelId: 'mock-model-id',
permissionEngine: new PermissionEngine({ newId: () => 'perm-1', now: () => 1 }),
modelFactory: () => model,
...(options.permissionTimeoutMs !== undefined
? { permissionTimeoutMs: options.permissionTimeoutMs }
: {}),
tools: [
{
name: options.permissionRequired ? 'Bash' : 'Probe',
description: options.permissionRequired ? 'Shell description' : 'Probe description',
parameters: options.permissionRequired
? z.object({ command: z.string() })
: z.object({ q: z.string() }),
permissionRequired: options.permissionRequired ?? false,
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 = options.interactionAuthority
? new SessionManager({
...managerDeps,
interactionAuthority: options.interactionAuthority,
canonicalPermissionOutcomes: noCanonicalPermissionOutcomes,
})
: new SessionManager(managerDeps);
const session = await manager.createSession(
makeInput({ backend: 'fake', permissionMode: options.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 respondToPermission(_decision: PermissionDecision): 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;
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 *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 stopSession(sessionId: string): Promise<void> {
this.stopped.push(sessionId);
}
async respondToPermission(
sessionId: string,
_response: Parameters<RuntimeKernelLike['respondToPermission']>[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> {
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 respondToPermission(): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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: 'usage',
type: 'usage_recorded',
message: 'Token usage recorded',
data: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
});
yield {
type: 'complete',
id: `${input.turnId}-complete`,
turnId: input.turnId,
ts: 4,
stopReason: 'end_turn',
};
}
async stop(): Promise<void> {}
async respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): 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[]>();
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;
generatedTitleAttempted: Gate | undefined;
async createSubagent(
input: CreateSessionInput,
): 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), created: true };
}
async createAgentGraphOperator(
input: CreateSessionInput,
request: AgentGraphOperatorProvisionRequest,
_expectedRevision: number,
): Promise<{ header: SessionHeader } & AgentGraphOperatorProvisionResult> {
const header = await this.create(input);
return {
header,
created: true,
provision: {
...request,
edges: request.edges.map((edge) => ({ ...edge })),
targetSessionId: header.id,
provisionedAt: 1,
},
};
}
async create(input: CreateSessionInput): 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, []);
return header;
}
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);
}
}
class MemoryAgentRunStore implements AgentRunStore, RuntimeEventStore {
listSessionRunsCalls = 0;
readEventsCalls = 0;
private headers = new Map<string, AgentRunHeader>();
private events = new Map<string, AgentRunEvent[]>();
private runtimeEvents = new Map<string, RuntimeEvent[]>();
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;
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');
return (this.runtimeEvents.get(key(sessionId, runId)) ?? [])
.filter((event) => event.partial !== true)
.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);
}
}
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 respondToPermission(_decision: PermissionDecision): 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 respondToPermission(_decision: PermissionDecision): Promise<void> {}
async dispose(): Promise<void> {}
}
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 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: { ...LOCAL_READ_AGENT_DEFINITION.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: {},
permissionRequired: false,
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<AgentRunEvent> = {}): AgentRunEvent {
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: AgentRunEvent[],
): 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,
acceptPermissionRequest: async () => ({ state: 'pending' }),
commitPermissionAnswer: async ({ answer }) => ({
kind: 'permission_answer',
answer,
}),
commitPermissionTimeout: async () => ({ kind: 'closure', reason: 'timed_out' }),
acceptUserQuestionRequest: async () => {},
close: async () => {},
release: () => {},
}),
};
}
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;
}