blob: 43860d4a2b6a3aac13fd1fa6c9b120b0734a7598 [file]
import { describe, test } from 'node:test';
import type {
AgentRunHeader,
CreateSessionInput,
RuntimeEvent,
SessionHeader,
SessionListFilter,
SessionSummary,
StoredMessage,
TurnRecord,
} from '@maka/core';
import { deriveTurnRecords } from '@maka/core';
import { expect } from '../test-helpers.js';
import {
compareRuntimeReadModelMessages,
projectRuntimeEventsToStoredMessages,
projectRuntimeEventsToStoredMessagesWithArchiveStatuses,
} from '../runtime-event-read-model.js';
import { buildRuntimeEventModelReplayPlan } from '../model-history.js';
import { materializeSession } from '../materializer.js';
import { BackendRegistry, SessionManager, type SessionStore } from '../session-manager.js';
const ts = 1_800_000_000_000;
const sessionId = 'sess-1';
const runId = 'run-1';
const turnId = 'turn-1';
const invocationId = 'inv-1';
let eventSeq = 0;
const header: AgentRunHeader = {
runId,
sessionId,
turnId,
status: 'completed',
backendKind: 'ai-sdk',
llmConnectionSlug: 'anthropic',
modelId: 'claude-sonnet-4-5',
cwd: '/tmp/work',
permissionMode: 'ask',
createdAt: ts,
updatedAt: ts + 20,
completedAt: ts + 20,
parentTurnId: 'parent-turn',
};
function ev(overrides: Partial<RuntimeEvent>): RuntimeEvent {
eventSeq += 1;
return {
id: `event-${eventSeq}`,
invocationId,
runId,
sessionId,
turnId,
ts,
partial: false,
role: 'system',
author: 'system',
...overrides,
};
}
function baseEvents(): RuntimeEvent[] {
return [
ev({
id: 'evt-user',
ts: ts + 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: 'read the file' },
refs: { storedMessageId: 'legacy-user' },
}),
ev({
id: 'evt-tool-call',
ts: ts + 2,
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'tool-1',
name: 'Read',
args: { path: '/tmp/a.txt' },
},
actions: { stateDelta: { displayName: 'Read file', intent: 'inspect' } },
refs: { toolCallId: 'tool-1' },
}),
ev({
id: 'evt-permission-request',
ts: ts + 3,
role: 'system',
author: 'system',
actions: {
permissionRequest: {
kind: 'tool_permission',
requestId: 'req-1',
toolUseId: 'tool-1',
toolName: 'Read',
category: 'read',
reason: 'custom',
args: { path: '/tmp/a.txt' },
rememberForTurnAllowed: true,
hint: 'needs read access',
},
},
refs: { toolCallId: 'tool-1' },
}),
ev({
id: 'evt-permission-decision',
ts: ts + 4,
role: 'system',
author: 'user',
actions: {
permissionDecision: {
requestId: 'req-1',
decision: 'allow',
rememberForTurn: true,
},
},
refs: { toolCallId: 'tool-1' },
}),
ev({
id: 'evt-tool-result',
ts: ts + 5,
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-1',
name: 'Read',
result: { kind: 'text', text: 'file contents' },
},
actions: { stateDelta: { durationMs: 42 } },
refs: { toolCallId: 'tool-1', storedMessageId: 'legacy-result' },
}),
ev({
id: 'evt-assistant',
ts: ts + 6,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'The file says: file contents' },
refs: { storedMessageId: 'legacy-assistant' },
}),
ev({
id: 'evt-token',
ts: ts + 7,
role: 'system',
author: 'system',
actions: {
tokenUsage: {
input: 100,
output: 25,
cacheRead: 10,
costUsd: 0.002,
systemPromptHash: 'sys-hash',
runtimeSteps: 3,
contextRemaining: 9000,
},
},
refs: { providerRequestTraceId: 'provider-trace-1' },
}),
ev({
id: 'evt-complete',
ts: ts + 8,
role: 'system',
author: 'system',
status: 'completed',
actions: { endInvocation: true },
}),
];
}
function equivalentLegacyMessages(): StoredMessage[] {
return [
{
type: 'user',
id: 'legacy-user',
turnId,
ts: ts + 1,
text: 'read the file',
},
{
type: 'tool_call',
id: 'tool-1',
turnId,
ts: ts + 2,
toolName: 'Read',
displayName: 'Read file',
intent: 'inspect',
args: { path: '/tmp/a.txt' },
},
{
type: 'permission_decision',
id: 'req-1',
turnId,
ts: ts + 4,
toolUseId: 'tool-1',
toolName: 'Read',
decision: 'allow',
rememberForTurn: true,
hint: 'needs read access',
},
{
type: 'tool_result',
id: 'legacy-result',
turnId,
ts: ts + 5,
toolUseId: 'tool-1',
isError: false,
content: { kind: 'text', text: 'file contents' },
durationMs: 42,
},
{
type: 'assistant',
id: 'legacy-assistant',
turnId,
ts: ts + 6,
text: 'The file says: file contents',
modelId: 'claude-sonnet-4-5',
},
{
type: 'token_usage',
id: 'evt-token',
turnId,
ts: ts + 7,
input: 100,
output: 25,
cacheRead: 10,
costUsd: 0.002,
systemPromptHash: 'sys-hash',
runtimeSteps: 3,
contextRemaining: 9000,
providerRequestTraceId: 'provider-trace-1',
},
{
type: 'turn_state',
id: 'evt-complete',
turnId,
ts: ts + 8,
status: 'completed',
parentTurnId: 'parent-turn',
partialOutputRetained: true,
},
];
}
describe('projectRuntimeEventsToStoredMessages', () => {
test('projects user displayText from RuntimeEvent text content', () => {
const typed = '/skill:alpha 帮我整理';
const envelope = 'The user explicitly invoked…\n\n<user-message>\n帮我整理\n</user-message>';
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-user-skill',
ts: ts + 1,
role: 'user',
author: 'user',
content: { kind: 'text', text: envelope, displayText: typed },
refs: { storedMessageId: 'user-skill' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([
{
type: 'user',
id: 'user-skill',
turnId,
ts: ts + 1,
text: envelope,
displayText: typed,
},
]);
const compare = compareRuntimeReadModelMessages(out.messages, [
{
type: 'user',
id: 'user-skill',
turnId,
ts: ts + 1,
text: envelope,
displayText: typed,
},
]);
expect(compare.diagnostics).toEqual([]);
});
test('full RuntimeEvent turn projects legacy-compatible rows', () => {
const out = projectRuntimeEventsToStoredMessages(baseEvents(), { runHeaders: [header] });
expect(out.messages.map((message) => message.type)).toEqual([
'user',
'tool_call',
'permission_decision',
'tool_result',
'assistant',
'token_usage',
'turn_state',
]);
expect(out.messages[1]).toMatchObject({
type: 'tool_call',
id: 'tool-1',
toolName: 'Read',
displayName: 'Read file',
intent: 'inspect',
});
expect(out.messages[2]).toMatchObject({
type: 'permission_decision',
id: 'req-1',
toolUseId: 'tool-1',
toolName: 'Read',
decision: 'allow',
hint: 'needs read access',
});
expect(out.messages[3]).toMatchObject({
type: 'tool_result',
id: 'legacy-result',
toolUseId: 'tool-1',
durationMs: 42,
});
expect(out.messages[4]).toMatchObject({
type: 'assistant',
modelId: 'claude-sonnet-4-5',
text: 'The file says: file contents',
});
expect(out.messages[6]).toMatchObject({
type: 'turn_state',
status: 'completed',
parentTurnId: 'parent-turn',
partialOutputRetained: true,
});
expect(out.diagnostics).toEqual([]);
});
test('projects an AskUserQuestion round trip without a legacy row for the live request', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-question-call',
ts: ts + 1,
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'question-tool-1',
name: 'AskUserQuestion',
args: { questions: [{ question: 'Choose', options: [{ label: 'Extend' }] }] },
},
refs: { toolCallId: 'question-tool-1' },
}),
ev({
id: 'evt-question-request',
ts: ts + 2,
actions: {
userQuestionRequest: {
requestId: 'question-1',
toolUseId: 'question-tool-1',
questions: [{ question: 'Choose', options: [{ label: 'Extend' }] }],
},
},
refs: { toolCallId: 'question-tool-1' },
}),
ev({
id: 'evt-question-result',
ts: ts + 3,
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'question-tool-1',
name: 'AskUserQuestion',
result: { kind: 'json', value: { answers: ['Extend'] } },
},
refs: { toolCallId: 'question-tool-1' },
}),
ev({
id: 'evt-question-complete',
ts: ts + 4,
status: 'completed',
actions: { endInvocation: true },
}),
],
{ runHeaders: [header] },
);
expect(out.messages.map((message) => message.type)).toEqual([
'tool_call',
'tool_result',
'turn_state',
]);
expect(out.diagnostics).toEqual([]);
});
test('restores legacy Plan tool results and consumes proposal state events', () => {
const output = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-plan-call',
ts: ts + 1,
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'plan-tool-1',
name: 'SubmitPlan',
args: {
title: 'Plan',
steps: [{ id: 'one', title: 'First step', description: 'One' }],
},
},
refs: { toolCallId: 'plan-tool-1' },
}),
ev({
id: 'evt-plan-result',
ts: ts + 2,
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'plan-tool-1',
name: 'SubmitPlan',
result: {
kind: 'plan_submitted',
proposal: { proposalId: 'proposal-1' },
storeVersion: 1,
} as never,
},
refs: { toolCallId: 'plan-tool-1' },
}),
ev({
id: 'evt-plan-submitted',
ts: ts + 3,
role: 'system',
author: 'agent',
actions: {
stateDelta: {
planId: 'plan-1',
proposalId: 'proposal-1',
title: 'Plan',
steps: [
{
id: 'one',
title: 'First step',
description: 'One',
status: 'pending',
},
],
},
},
}),
ev({
id: 'evt-plan-complete',
ts: ts + 4,
status: 'completed',
actions: { endInvocation: true },
}),
],
{ runHeaders: [header] },
);
expect(output.diagnostics).toEqual([]);
expect(output.messages.map((message) => message.type)).toEqual([
'tool_call',
'tool_result',
'turn_state',
]);
const result = output.messages.find((message) => message.type === 'tool_result');
expect(result?.type === 'tool_result' ? result.content.kind : undefined).toBe('json');
});
test('normalizes an exact legacy terminal result at RuntimeEvent restore', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-legacy-terminal',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-legacy-terminal',
name: 'Bash',
result: {
kind: 'terminal',
cwd: '/tmp/work',
cmd: 'printf ok',
status: 'completed',
exitCode: 0,
stdout: 'ok',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
},
},
refs: { toolCallId: 'tool-legacy-terminal' },
}),
],
{ runHeaders: [header] },
);
const result = out.messages.find((message) => message.type === 'tool_result');
expect(result?.type === 'tool_result' ? result.content : undefined).toEqual({
kind: 'terminal',
cwd: '/tmp/work',
cmd: 'printf ok',
status: 'completed',
exitCode: 0,
output: {
mode: 'pipes',
stdout: 'ok',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
});
});
test('replays generic provider tool results without Maka result decoding', () => {
const events = [
ev({
id: 'evt-generic-primitive-call',
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'generic-primitive-1',
name: 'ProviderPrimitive',
args: {},
},
}),
ev({
id: 'evt-generic-primitive-result',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'generic-primitive-1',
name: 'ProviderPrimitive',
result: 42 as never,
},
}),
ev({
id: 'evt-generic-json-call',
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'generic-json-1',
name: 'ProviderJson',
args: {},
},
}),
ev({
id: 'evt-generic-json-result',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'generic-json-1',
name: 'ProviderJson',
result: { providerPayload: true, values: [1, 2, 3] } as never,
},
}),
ev({
id: 'evt-generic-subagent-collision-call',
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'generic-subagent-collision-1',
name: 'ProviderJson',
args: {},
},
}),
ev({
id: 'evt-generic-subagent-collision-result',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'generic-subagent-collision-1',
name: 'ProviderJson',
result: {
kind: 'subagent',
status: 'waiting_permission',
providerPayload: true,
} as never,
},
}),
];
const replay = buildRuntimeEventModelReplayPlan(events);
expect(replay.items.map((item) => item.kind)).toEqual([
'tool_call',
'tool_result',
'tool_call',
'tool_result',
'tool_call',
'tool_result',
]);
expect(
replay.items.filter((item) => item.kind === 'tool_result').map((item) => item.output),
).toEqual([
42,
{ providerPayload: true, values: [1, 2, 3] },
{
kind: 'subagent',
status: 'waiting_permission',
providerPayload: true,
},
]);
expect(replay.diagnostics).toEqual([]);
});
test('restores a settled Agent Swarm function response', () => {
const result = {
kind: 'agent_swarm' as const,
status: 'completed' as const,
items: [
{
itemId: 'contract',
index: 0,
profile: 'local_read',
started: true,
agentId: 'local-read',
agentName: 'Local Read',
turnId: 'child-turn',
runId: 'child-run',
status: 'completed' as const,
summary: 'Verified the contract.',
artifactIds: [],
startedAt: ts + 1,
completedAt: ts + 2,
durationMs: 1,
},
],
startedAt: ts,
completedAt: ts + 2,
durationMs: 2,
};
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-agent-swarm-result',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-agent-swarm',
name: 'agent_swarm',
result,
},
refs: { toolCallId: 'tool-agent-swarm' },
}),
],
{ runHeaders: [header] },
);
const projected = out.messages.find((message) => message.type === 'tool_result');
expect(projected?.type === 'tool_result' ? projected.content : undefined).toEqual(result);
expect(out.diagnostics).toEqual([]);
});
test('diagnoses a mixed legacy/current shell result instead of restoring it', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-mixed-terminal',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-mixed-terminal',
name: 'Bash',
result: {
kind: 'terminal',
cwd: '/tmp/work',
cmd: 'printf bad',
status: 'completed',
exitCode: 0,
stdout: 'bad',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
output: {
mode: 'pipes',
stdout: 'bad',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
},
},
refs: { toolCallId: 'tool-mixed-terminal' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages.some((message) => message.type === 'tool_result')).toBe(false);
expect(out.diagnostics.map((diagnostic) => diagnostic.code)).toContain('incomplete_event');
});
test('projects first-observed step content order for stable live handoff', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
ts: ts + 1,
role: 'model',
author: 'agent',
content: { kind: 'function_call', id: 'tool-1', name: 'Read', args: {} },
refs: { toolCallId: 'tool-1', stepId: 'message-1' },
}),
ev({
ts: ts + 2,
role: 'model',
author: 'agent',
content: { kind: 'thinking', text: 'late reasoning' },
refs: { providerEventId: 'message-1' },
}),
ev({
ts: ts + 3,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer' },
refs: { providerEventId: 'message-1' },
}),
],
{ runHeaders: [header] },
);
const assistant = out.messages.find((message) => message.type === 'assistant');
expect((assistant as unknown as { contentOrder?: string[] } | undefined)?.contentOrder).toEqual(
['tools', 'thinking', 'text'],
);
});
test('archived tool-result placeholders project to diagnostic tool-result rows', () => {
const events = baseEvents();
const toolResult = events.find((event) => event.id === 'evt-tool-result');
if (toolResult?.content?.kind !== 'function_response')
throw new Error('fixture missing tool result');
toolResult.content.result = {
kind: 'maka.archived_tool_result',
rewriteVersion: 1,
artifactId: 'artifact-tool-result',
runtimeEventId: 'evt-tool-result',
toolCallId: 'tool-1',
toolName: 'Read',
bodySha256: 'a'.repeat(64),
originalEstimatedTokens: 200,
originalBytes: 800,
reason: 'stale_tool_result_pruned_before_compact',
};
const out = projectRuntimeEventsToStoredMessages(events, { runHeaders: [header] });
const projected = out.messages.find((message) => message.type === 'tool_result');
expect(projected).toMatchObject({
type: 'tool_result',
toolUseId: 'tool-1',
content: {
kind: 'archived_tool_result',
status: 'not_loaded',
artifactId: 'artifact-tool-result',
bodySha256: 'a'.repeat(64),
runtimeEventId: 'evt-tool-result',
toolCallId: 'tool-1',
toolName: 'Read',
originalEstimatedTokens: 200,
originalBytes: 800,
rewriteVersion: 1,
reason: 'stale_tool_result_pruned_before_compact',
},
});
expect(out.diagnostics.map((diag) => diag.code)).toEqual(['archived_tool_result_placeholder']);
});
test('archive status wrapper can project missing and corrupt rows without changing sync defaults', () => {
const events = baseEvents();
const toolResult = events.find((event) => event.id === 'evt-tool-result');
if (toolResult?.content?.kind !== 'function_response')
throw new Error('fixture missing tool result');
toolResult.content.result = {
kind: 'maka.archived_tool_result',
rewriteVersion: 1,
artifactId: 'artifact-tool-result',
runtimeEventId: 'evt-tool-result',
toolCallId: 'tool-1',
toolName: 'Read',
bodySha256: 'a'.repeat(64),
originalEstimatedTokens: 200,
originalBytes: 800,
reason: 'stale_tool_result_pruned_before_compact',
};
const defaultOut = projectRuntimeEventsToStoredMessages(events, { runHeaders: [header] });
const defaultProjected = defaultOut.messages.find((message) => message.type === 'tool_result');
expect(defaultProjected).toMatchObject({ type: 'tool_result' });
expect(archivedStatus(defaultProjected)).toBe('not_loaded');
const missingOut = projectRuntimeEventsToStoredMessagesWithArchiveStatuses(events, {
runHeaders: [header],
archiveStatuses: { 'evt-tool-result': 'missing' },
});
const missingProjected = missingOut.messages.find((message) => message.type === 'tool_result');
expect(missingProjected).toMatchObject({ type: 'tool_result' });
expect(archivedStatus(missingProjected)).toBe('missing');
const corruptOut = projectRuntimeEventsToStoredMessagesWithArchiveStatuses(events, {
runHeaders: [header],
archiveStatuses: [{ runtimeEventId: 'evt-tool-result', status: 'corrupt' }],
});
const corruptProjected = corruptOut.messages.find((message) => message.type === 'tool_result');
expect(corruptProjected).toMatchObject({ type: 'tool_result' });
expect(archivedStatus(corruptProjected)).toBe('corrupt');
});
test('projected rows materialize to the same runtime view model as equivalent legacy rows', () => {
const out = projectRuntimeEventsToStoredMessages(baseEvents(), { runHeaders: [header] });
const projected = materializeSession(out.messages);
const legacy = materializeSession(equivalentLegacyMessages());
expect(projected).toEqual(legacy);
});
test('partial RuntimeEvents are excluded', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-partial',
partial: true,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'streaming' },
}),
ev({
id: 'evt-final',
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'final' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toHaveLength(1);
expect(out.messages[0]).toMatchObject({ type: 'assistant', text: 'final' });
expect(out.diagnostics.map((diag) => diag.code)).toEqual(['partial_skipped']);
});
test('tool dispatch recovery facts are accepted without creating legacy message rows', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'toolop-1-dispatch',
role: 'system',
author: 'system',
actions: {
toolDispatch: {
protocol: 't1_after_preflight_v1',
operationId: 'toolop-1',
providerToolCallId: 'tool-1',
toolName: 'Bash',
canonicalArgsHash: 'sha256:args',
recoveryMode: 'reconcile',
},
},
refs: { toolCallId: 'tool-1', operationId: 'toolop-1' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([]);
expect(out.diagnostics).toEqual([]);
});
test('question answer acknowledgements remain non-visible audit facts', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'question-1-answered',
role: 'system',
author: 'user',
actions: { userQuestionAnswerAccepted: { requestId: 'question-1' } },
refs: { toolCallId: 'tool-1' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([]);
expect(out.diagnostics).toEqual([]);
});
test('terminal recovery bundle facts are accepted without creating legacy message rows', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'toolop-1-reconcile',
role: 'system',
author: 'system',
actions: {
toolRecovery: {
kind: 'maka.tool.reconcile_result',
version: 1,
payload: {
protocol: 'tool_reconcile_v1',
operationId: 'toolop-1',
observation: 'unreadable',
observationSchema: 'state_identity_v1',
observationDigest:
'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
},
},
refs: { toolCallId: 'tool-1', operationId: 'toolop-1' },
}),
ev({
id: 'toolop-1-decision',
role: 'system',
author: 'system',
actions: {
toolRecovery: {
kind: 'maka.tool.recovery_decision',
version: 1,
payload: {
protocol: 'tool_recovery_v1',
operationId: 'toolop-1',
disposition: 'parked',
reasonCode: 'reconcile_unreadable',
evidenceEventIds: ['call-1', 'dispatch-1', 'toolop-1-reconcile'],
},
},
},
refs: { toolCallId: 'tool-1', operationId: 'toolop-1' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([]);
expect(out.diagnostics).toEqual([]);
});
test('continuation-start recovery facts are accepted without creating legacy message rows', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'continuation-start',
role: 'system',
author: 'system',
actions: {
stateDelta: { continuationStart: true },
runtimeProtocol: { toolBoundary: 't1_after_preflight_v1' },
},
refs: {
sourceInvocationId: 'source-invocation',
sourceRunId: 'source-run',
sourceTurnId: 'source-turn',
sourceRuntimeEventHighWater: 2,
},
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([]);
expect(out.diagnostics).toEqual([]);
});
test('model thinking attaches to the assistant text row that shares its step message id', () => {
// Real emission and backfill give a step's thinking and text the same message
// id (providerEventId / storedMessageId), so the projection pairs by id.
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-thinking',
ts: ts + 5,
role: 'model',
author: 'agent',
content: {
kind: 'thinking',
text: 'private reasoning',
signature: 'sig-1',
providerOptions: { maka: { kimiReasoningField: 'reasoning' } },
},
refs: { storedMessageId: 'legacy-assistant' },
}),
ev({
id: 'evt-assistant',
ts: ts + 6,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'visible answer' },
refs: { storedMessageId: 'legacy-assistant' },
}),
],
{ runHeaders: [header] },
);
const legacy: StoredMessage[] = [
{
type: 'assistant',
id: 'legacy-assistant',
turnId,
ts: ts + 6,
text: 'visible answer',
modelId: 'claude-sonnet-4-5',
thinking: {
text: 'private reasoning',
signature: 'sig-1',
providerOptions: { maka: { kimiReasoningField: 'reasoning' } },
},
},
];
expect(out.messages).toEqual(legacy);
expect(out.diagnostics).toEqual([]);
expect(compareRuntimeReadModelMessages(out.messages, legacy).compatible).toBe(true);
});
test('per-step thinking pairs each step assistant row by its own message id', () => {
// Two steps in one turn, each with its own signed thinking. The ledger order
// per step is thinking → text (finish-step flush), and each step's thinking
// carries its step message id, so it must attach to its own assistant row —
// not the last row of the turn.
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-think-1',
ts: ts + 1,
role: 'model',
author: 'agent',
content: { kind: 'thinking', text: 'reasoning one', signature: 'sig-1' },
refs: { providerEventId: 'step-1' },
}),
ev({
id: 'evt-text-1',
ts: ts + 2,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer one' },
refs: { providerEventId: 'step-1' },
}),
ev({
id: 'evt-think-2',
ts: ts + 3,
role: 'model',
author: 'agent',
content: { kind: 'thinking', text: 'reasoning two', signature: 'sig-2' },
refs: { providerEventId: 'step-2' },
}),
ev({
id: 'evt-text-2',
ts: ts + 4,
role: 'model',
author: 'agent',
content: { kind: 'text', text: 'answer two' },
refs: { providerEventId: 'step-2' },
}),
],
{ runHeaders: [header] },
);
const assistants = out.messages.filter((message) => message.type === 'assistant');
expect(assistants).toEqual([
{
type: 'assistant',
id: 'step-1',
turnId,
ts: ts + 2,
text: 'answer one',
modelId: 'claude-sonnet-4-5',
thinking: { text: 'reasoning one', signature: 'sig-1' },
},
{
type: 'assistant',
id: 'step-2',
turnId,
ts: ts + 4,
text: 'answer two',
modelId: 'claude-sonnet-4-5',
thinking: { text: 'reasoning two', signature: 'sig-2' },
},
]);
expect(out.diagnostics).toEqual([]);
});
test('unsupported and incomplete events are diagnostic-only', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-thinking',
role: 'model',
author: 'agent',
content: { kind: 'thinking', text: 'private reasoning' },
}),
ev({
id: 'evt-permission-orphan',
actions: {
permissionDecision: {
requestId: 'missing-request',
decision: 'deny',
},
},
}),
ev({
id: 'evt-invalid-result',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-x',
name: 'Read',
result: 'plain string is not ToolResultContent',
},
}),
],
{ runHeaders: [header] },
);
expect(out.messages).toEqual([]);
expect(out.diagnostics.map((diag) => diag.code)).toEqual([
'incomplete_event',
'unsupported_event',
'incomplete_event',
'unsupported_event',
'unsupported_event',
]);
});
test('failed terminal RuntimeEvent maps to failed turn state when run header carries failure class', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-failed',
ts: ts + 9,
status: 'failed',
actions: { endInvocation: true },
}),
],
{
runHeaders: [{ ...header, status: 'failed', failureClass: 'tool_failed' }],
},
);
expect(out.messages).toEqual([
{
type: 'turn_state',
id: 'evt-failed',
turnId,
ts: ts + 9,
status: 'failed',
parentTurnId: 'parent-turn',
errorClass: 'tool_failed',
partialOutputRetained: false,
},
]);
expect(out.diagnostics).toEqual([]);
});
test('tool step cap terminal fact projects a persistent system notice', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-step-limit',
ts: ts + 9,
status: 'failed',
actions: {
endInvocation: true,
stateDelta: { stopReason: 'step_limit', failureClass: 'tool_step_cap_reached' },
},
}),
],
{
runHeaders: [{ ...header, status: 'failed', failureClass: 'tool_step_cap_reached' }],
},
);
expect(out.messages.find((message) => message.type === 'system_note')).toEqual({
type: 'system_note',
id: 'evt-step-limit:step-limit-notice',
turnId,
ts: ts + 9,
kind: 'step_limit',
});
});
test('aborted terminal RuntimeEvent preserves abort source from runtime state', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-aborted',
ts: ts + 9,
status: 'aborted',
actions: { endInvocation: true, stateDelta: { abortSource: 'renderer.stop_button' } },
}),
],
{
runHeaders: [{ ...header, status: 'cancelled' }],
},
);
expect(out.messages).toEqual([
{
type: 'turn_state',
id: 'evt-aborted',
turnId,
ts: ts + 9,
status: 'aborted',
parentTurnId: 'parent-turn',
abortedAt: ts + 9,
abortSource: 'renderer.stop_button',
partialOutputRetained: false,
},
]);
expect(out.diagnostics).toEqual([]);
});
test('aborted terminal RuntimeEvent keeps an explicit diagnostic when abort source is unavailable', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-aborted',
ts: ts + 9,
status: 'aborted',
actions: { endInvocation: true },
}),
],
{
runHeaders: [{ ...header, status: 'cancelled' }],
},
);
expect(out.messages[0]).toMatchObject({
type: 'turn_state',
status: 'aborted',
abortedAt: ts + 9,
});
expect(out.diagnostics.map((diag) => diag.code)).toEqual(['incomplete_event']);
});
test('projects tool_call stepId from refs so the UI timeline keeps step pairing', () => {
const stepCall = (id: string, stepId?: string) =>
ev({
id: `evt-${id}`,
role: 'model' as const,
author: 'agent' as const,
content: {
kind: 'function_call' as const,
id,
name: 'Read',
args: { path: '/tmp/a.txt' },
},
refs: { toolCallId: id, ...(stepId ? { stepId } : {}) },
});
const withStep = projectRuntimeEventsToStoredMessages([stepCall('tool-step', 'step-1')], {
runHeaders: [header],
});
expect(withStep.messages[0]).toMatchObject({
type: 'tool_call',
id: 'tool-step',
stepId: 'step-1',
});
// Legacy events without refs.stepId must not grow a stepId key: the UI
// uses its absence to pick the backward-compatible tools-first ordering.
const withoutStep = projectRuntimeEventsToStoredMessages([stepCall('tool-legacy')], {
runHeaders: [header],
});
const legacyCall = withoutStep.messages[0];
expect(legacyCall).toMatchObject({ type: 'tool_call', id: 'tool-legacy' });
expect(legacyCall && 'stepId' in legacyCall).toBe(false);
});
test('projects tool_call activityKind from runtime state for replay', () => {
const out = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-tool-kind',
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'tool-kind',
name: 'CustomCommand',
args: {},
},
actions: { stateDelta: { activityKind: 'command' } },
refs: { toolCallId: 'tool-kind' },
}),
],
{ runHeaders: [header] },
);
expect(out.messages[0]).toMatchObject({
type: 'tool_call',
id: 'tool-kind',
activityKind: 'command',
});
});
});
describe('compareRuntimeReadModelMessages', () => {
test('accepts semantically equivalent projected and legacy messages despite id differences', () => {
const projected = projectRuntimeEventsToStoredMessages(baseEvents(), { runHeaders: [header] });
const legacyWithDifferentIds = equivalentLegacyMessages().map((message) => {
if (message.type === 'tool_call' || message.type === 'permission_decision') return message;
return { ...message, id: `different-${message.id}` } as StoredMessage;
});
const result = compareRuntimeReadModelMessages(projected.messages, legacyWithDifferentIds);
expect(result.compatible).toBe(true);
expect(result.diagnostics).toEqual([]);
});
test('treats nested JSON with different property order as compatible', () => {
const projected = projectRuntimeEventsToStoredMessages(
[
ev({
id: 'evt-tool-call-json',
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: 'tool-json',
name: 'JsonTool',
args: { beta: 2, alpha: { z: 3, a: 1 } },
},
}),
ev({
id: 'evt-tool-result-json',
role: 'tool',
author: 'tool',
content: {
kind: 'function_response',
id: 'tool-json',
name: 'JsonTool',
result: { kind: 'json', value: { outer: { y: 2, x: 1 }, list: [{ b: 2, a: 1 }] } },
},
}),
],
{ runHeaders: [header] },
);
const legacy: StoredMessage[] = [
{
type: 'tool_call',
id: 'tool-json',
turnId,
ts,
toolName: 'JsonTool',
args: { alpha: { a: 1, z: 3 }, beta: 2 },
},
{
type: 'tool_result',
id: 'different-result-id',
turnId,
ts,
toolUseId: 'tool-json',
isError: false,
content: { kind: 'json', value: { list: [{ a: 1, b: 2 }], outer: { x: 1, y: 2 } } },
},
];
const result = compareRuntimeReadModelMessages(projected.messages, legacy);
expect(result.compatible).toBe(true);
expect(result.diagnostics).toEqual([]);
});
test('rejects a mismatched tool activity kind', () => {
const projected: StoredMessage[] = [
{
type: 'tool_call',
id: 'tool-kind',
turnId,
ts,
toolName: 'CustomTool',
activityKind: 'read',
args: {},
},
];
const legacy: StoredMessage[] = [
{
...(projected[0] as Extract<StoredMessage, { type: 'tool_call' }>),
activityKind: 'command',
},
];
expect(compareRuntimeReadModelMessages(projected, legacy).compatible).toBe(false);
});
test('rejects mismatched replay-critical token usage fields', () => {
const usage: Extract<StoredMessage, { type: 'token_usage' }> = {
type: 'token_usage',
id: 'usage-1',
turnId,
ts,
input: 100,
output: 25,
runtimeSteps: 3,
contextRemaining: 9000,
providerRequestTraceId: 'provider-trace-1',
};
expect(
compareRuntimeReadModelMessages([usage], [{ ...usage, runtimeSteps: 4 }]).compatible,
).toBe(false);
expect(
compareRuntimeReadModelMessages([usage], [{ ...usage, contextRemaining: 8000 }]).compatible,
).toBe(false);
expect(
compareRuntimeReadModelMessages(
[usage],
[{ ...usage, providerRequestTraceId: 'provider-trace-2' }],
).compatible,
).toBe(false);
});
test('rejects missing tool result and assistant text cases', () => {
const projected = projectRuntimeEventsToStoredMessages(baseEvents(), { runHeaders: [header] });
const missing = projected.messages.filter(
(message) => message.type !== 'tool_result' && message.type !== 'assistant',
);
const result = compareRuntimeReadModelMessages(missing, equivalentLegacyMessages());
expect(result.compatible).toBe(false);
expect(result.diagnostics.map((diag) => diag.code)).toEqual([
'missing_legacy_message',
'missing_legacy_message',
]);
});
});
describe('SessionManager read behavior', () => {
test('getMessages requires RuntimeReadModel stores instead of reading SessionStore messages directly', async () => {
const messages: StoredMessage[] = equivalentLegacyMessages();
const store = new ReadOnlyStore(messages);
const manager = new SessionManager({
store,
backends: new BackendRegistry(),
newId: () => 'id',
now: () => ts,
});
await expectRejects(
manager.getMessages(sessionId),
/RuntimeReadModel requires AgentRunStore and RuntimeEventStore/,
);
expect(store.readMessagesCalls).toBe(0);
});
});
class ReadOnlyStore implements SessionStore {
readMessagesCalls = 0;
constructor(private readonly messages: StoredMessage[]) {}
async createSubagent(
_input: CreateSessionInput,
): Promise<{ header: SessionHeader; created: boolean }> {
throw new Error('not implemented');
}
async create(_input: CreateSessionInput): Promise<SessionHeader> {
throw new Error('not implemented');
}
async list(_filter?: SessionListFilter): Promise<SessionSummary[]> {
return [];
}
async readHeader(id: string): Promise<SessionHeader> {
return makeHeader(id);
}
async readMessages(_sessionId: string): Promise<StoredMessage[]> {
this.readMessagesCalls += 1;
return [...this.messages];
}
async listTurns(_sessionId: string): Promise<TurnRecord[]> {
return deriveTurnRecords(this.messages);
}
async appendMessage(_sessionId: string, _m: StoredMessage): Promise<void> {
throw new Error('not implemented');
}
async appendMessages(_sessionId: string, _ms: StoredMessage[]): Promise<void> {
throw new Error('not implemented');
}
async updateHeader(id: string, patch: Partial<SessionHeader>): Promise<SessionHeader> {
return { ...makeHeader(id), ...patch };
}
async markSessionReadThrough(id: string, readThroughTs: number): Promise<SessionHeader> {
const header = makeHeader(id);
if (
!Number.isFinite(readThroughTs) ||
!header.hasUnread ||
(header.lastMessageAt !== undefined && header.lastMessageAt > readThroughTs)
)
return header;
return { ...header, hasUnread: false };
}
async archive(_sessionId: string): Promise<void> {}
async unarchive(_sessionId: string): Promise<void> {}
async setFlagged(_sessionId: string, _isFlagged: boolean): Promise<void> {}
async rename(_sessionId: string, _name: string): Promise<void> {}
async remove(_sessionId: string): Promise<void> {}
}
async function expectRejects(promise: Promise<unknown>, pattern: RegExp): Promise<void> {
try {
await promise;
} catch (error) {
expect(error instanceof Error ? error.message : String(error)).toMatch(pattern);
return;
}
throw new Error(`Expected promise to reject with ${pattern}`);
}
function archivedStatus(message: StoredMessage | undefined): string | undefined {
if (message?.type !== 'tool_result') return undefined;
return message.content.kind === 'archived_tool_result' ? message.content.status : undefined;
}
function makeHeader(id: string): SessionHeader {
return {
id,
workspaceRoot: '/tmp/work',
cwd: '/tmp/work',
createdAt: ts,
lastUsedAt: ts,
name: 'Session',
titleIsManual: true,
isFlagged: false,
labels: [],
isArchived: false,
status: 'active',
hasUnread: false,
backend: 'fake',
llmConnectionSlug: 'fake',
connectionLocked: false,
model: 'fake-model',
permissionMode: 'ask',
schemaVersion: 1,
};
}