blob: 7505248d919cfdad5066c001d568cfe3a920d382 [file]
import { describe, test } from 'node:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { AgentRunHeader, RuntimeEvent, StoredMessage } from '@maka/core';
import { createSqliteRuntimeStore } from '@maka/storage';
import { expect } from '../test-helpers.js';
import {
RUNTIME_EVENT_BACKFILL_STATE_KEY,
backfillRuntimeEventsFromStoredMessages,
} from '../runtime-event-backfill.js';
const run: AgentRunHeader = {
runId: 'run-1',
sessionId: 'session-1',
turnId: 'turn-1',
status: 'completed',
backendKind: 'fake',
llmConnectionSlug: 'fake',
modelId: 'fake-model',
cwd: '/tmp/cwd',
permissionMode: 'ask',
createdAt: 100,
updatedAt: 180,
completedAt: 180,
};
function nextIds(): () => string {
let index = 0;
return () => {
index += 1;
return `rt-backfill-${index}`;
};
}
function recoveryMarker(event: RuntimeEvent): Record<string, unknown> | undefined {
return event.actions?.stateDelta?.[RUNTIME_EVENT_BACKFILL_STATE_KEY] as
| Record<string, unknown>
| undefined;
}
describe('runtime event backfill', () => {
test('persists legacy tool history through the canonical generic RuntimeEvent writer', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-runtime-backfill-sqlite-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
try {
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages: [
{
type: 'tool_call',
id: 'legacy-tool-call',
turnId: 'turn-1',
ts: 120,
toolName: 'Read',
activityKind: 'read',
displayName: 'Read file',
intent: 'inspect',
args: { path: 'README.md' },
},
{
type: 'tool_result',
id: 'legacy-tool-result',
turnId: 'turn-1',
ts: 130,
toolUseId: 'legacy-tool-call',
isError: false,
content: { kind: 'text', text: 'file body' },
durationMs: 42,
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 180,
status: 'completed',
partialOutputRetained: true,
},
],
newId: nextIds(),
now: () => 999,
});
for (const event of result.events) {
await store.appendRuntimeEvent(event.sessionId, event.runId, event);
}
const persisted = await store.readImmutableRuntimeEvents(run.sessionId, run.runId);
expect(persisted.map((event) => event.content?.kind ?? event.status)).toEqual([
'function_call',
'function_response',
'completed',
]);
expect(persisted[0]?.refs).toEqual({
storedMessageId: 'legacy-tool-call',
toolCallId: 'legacy-tool-call',
});
expect(persisted[1]?.refs).toEqual({
storedMessageId: 'legacy-tool-result',
toolCallId: 'legacy-tool-call',
});
expect(persisted.some((event) => event.actions?.toolDispatch !== undefined)).toBe(false);
expect(persisted.some((event) => event.refs?.operationId !== undefined)).toBe(false);
} finally {
store.close();
await rm(root, { recursive: true, force: true });
}
});
test('prefers the persisted Run invocation identity over a caller fallback', () => {
const result = backfillRuntimeEventsFromStoredMessages({
run: { ...run, invocationId: 'persisted-invocation' },
invocationId: 'caller-fallback',
messages: [
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 180,
status: 'completed',
partialOutputRetained: false,
},
],
newId: nextIds(),
now: () => 999,
});
expect(result.events.map((event) => event.invocationId)).toEqual(['persisted-invocation']);
});
test('preserves provider-native identity in StoredMessage fallback backfill', () => {
const providerOutput = [
{
type: 'web_search_result',
url: 'https://maka.example/',
title: 'Maka',
pageAge: null,
encryptedContent: 'encrypted-result',
},
];
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages: [
{
type: 'tool_call',
id: 'search-1',
turnId: 'turn-1',
ts: 120,
toolName: 'WebSearch',
args: { query: 'latest Maka' },
providerExecuted: true,
},
{
type: 'tool_result',
id: 'search-result-1',
turnId: 'turn-1',
ts: 130,
toolUseId: 'search-1',
isError: false,
content: { kind: 'web_search', provider: 'model', query: 'latest Maka', rows: [] },
providerExecuted: true,
providerOutput,
},
],
newId: nextIds(),
now: () => 999,
});
expect(result.events[0]?.content).toMatchObject({
kind: 'function_call',
providerExecuted: true,
});
expect(result.events[1]?.content).toMatchObject({
kind: 'function_response',
providerExecuted: true,
providerOutput,
});
});
test('drops provider-native fallback history when opaque replay output is unavailable', () => {
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages: [
{
type: 'tool_call',
id: 'search-legacy',
turnId: 'turn-1',
ts: 120,
toolName: 'WebSearch',
args: { query: 'latest Maka' },
providerExecuted: true,
},
{
type: 'tool_result',
id: 'search-result-legacy',
turnId: 'turn-1',
ts: 130,
toolUseId: 'search-legacy',
isError: false,
content: { kind: 'web_search', provider: 'model', query: 'latest Maka', rows: [] },
providerExecuted: true,
},
],
newId: nextIds(),
now: () => 999,
});
expect(result.events.map((event) => event.content?.kind ?? event.status)).toEqual([
'completed',
]);
expect(result.diagnostics).toEqual([
{
code: 'skipped_provider_native_replay_gap',
message:
'provider-native tool history requires the opaque provider output for lossless recovery',
detail: { messageId: 'search-legacy', toolUseId: 'search-legacy' },
},
]);
});
test('backfills a host-authored graph wake without attributing it to the user', () => {
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages: [
{
type: 'user',
id: 'legacy-graph-wake',
turnId: 'turn-1',
ts: 101,
text: 'graph checkpoint',
origin: {
kind: 'agent_graph',
graphId: 'graph-1',
wakeId: 'wake-1',
attemptId: 'attempt-1',
},
},
],
newId: nextIds(),
now: () => 999,
});
expect(result.events[0]?.role).toBe('user');
expect(result.events[0]?.author).toBe('host');
expect(result.events[0]?.content).toMatchObject({
kind: 'text',
origin: {
kind: 'agent_graph',
graphId: 'graph-1',
wakeId: 'wake-1',
attemptId: 'attempt-1',
},
});
});
test('backfills nested CodeMode tool rows without making them model-visible', () => {
const identity = {
origin: 'code_mode' as const,
modelVisibility: 'hidden' as const,
parentToolCallId: 'exec-1',
parentOperationId: 'exec-op-1',
};
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages: [
{
type: 'tool_call',
id: 'nested-1',
turnId: 'turn-1',
ts: 120,
toolName: 'Read',
args: {},
...identity,
},
{
type: 'tool_result',
id: 'nested-result-1',
turnId: 'turn-1',
ts: 130,
toolUseId: 'nested-1',
isError: false,
content: { kind: 'text', text: 'ok' },
...identity,
},
],
newId: nextIds(),
now: () => 999,
});
const toolEvents = result.events.filter(
(event) =>
event.content?.kind === 'function_call' || event.content?.kind === 'function_response',
);
expect(toolEvents).toHaveLength(2);
for (const event of toolEvents) {
expect(event).toMatchObject({ origin: 'code_mode', modelVisibility: 'hidden' });
expect(event.refs).toMatchObject({
parentToolCallId: 'exec-1',
parentOperationId: 'exec-op-1',
});
}
});
test('backfills only low-risk RuntimeEvents from legacy StoredMessage rows', () => {
const messages: StoredMessage[] = [
{
type: 'user',
id: 'legacy-user',
turnId: 'turn-1',
ts: 101,
text: 'hello',
attachments: [
{
kind: 'other',
name: 'note.txt',
mimeType: 'text/plain',
bytes: 12,
ref: {
kind: 'session_file',
sessionId: 'session-1',
relativePath: 'attachments/note.txt',
},
},
],
},
{
type: 'assistant',
id: 'legacy-assistant',
turnId: 'turn-1',
ts: 110,
text: 'answer',
modelId: 'fake-model',
thinking: { text: 'reasoning', signature: 'sig-1' },
},
{
type: 'tool_call',
id: 'tool-1',
turnId: 'turn-1',
ts: 120,
toolName: 'Read',
activityKind: 'read',
displayName: 'Read file',
intent: 'inspect',
args: { path: 'README.md' },
stepId: 'step-1',
},
{
type: 'tool_result',
id: 'legacy-tool-result',
turnId: 'turn-1',
ts: 130,
toolUseId: 'tool-1',
isError: false,
content: { kind: 'text', text: 'file body' },
durationMs: 42,
},
{
type: 'permission_decision',
id: 'perm-1',
turnId: 'turn-1',
ts: 140,
toolUseId: 'tool-1',
toolName: 'Read',
decision: 'allow',
rememberForTurn: true,
},
{
type: 'token_usage',
id: 'usage-1',
turnId: 'turn-1',
ts: 150,
input: 10,
output: 5,
total: 15,
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 180,
status: 'completed',
partialOutputRetained: true,
},
];
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages,
newId: nextIds(),
now: () => 999,
});
expect(result.diagnostics).toEqual([]);
expect(result.events.map((event) => event.id)).toEqual([
'rt-backfill-1',
'rt-backfill-2',
'rt-backfill-3',
'rt-backfill-4',
'rt-backfill-5',
'rt-backfill-6',
'rt-backfill-7',
'rt-backfill-8',
]);
expect(result.events.map((event) => event.invocationId)).toEqual(
Array(8).fill('backfill-run-1'),
);
expect(result.events.map((event) => event.partial)).toEqual(Array(8).fill(false));
expect(result.events[0]?.content).toEqual({
kind: 'text',
text: 'hello',
attachments: [
{
kind: 'other',
name: 'note.txt',
mimeType: 'text/plain',
bytes: 12,
ref: {
kind: 'session_file',
sessionId: 'session-1',
relativePath: 'attachments/note.txt',
},
},
],
});
expect(result.events[1]?.content).toEqual({ kind: 'text', text: 'answer' });
expect(result.events[2]?.content).toEqual({
kind: 'thinking',
text: 'reasoning',
signature: 'sig-1',
});
expect(result.events[3]?.content).toEqual({
kind: 'function_call',
id: 'tool-1',
name: 'Read',
args: { path: 'README.md' },
});
expect(result.events[3]?.actions?.stateDelta?.displayName).toBe('Read file');
expect(result.events[3]?.actions?.stateDelta?.activityKind).toBe('read');
expect(result.events[3]?.actions?.stateDelta?.intent).toBe('inspect');
expect(result.events[3]?.refs).toEqual({
storedMessageId: 'tool-1',
toolCallId: 'tool-1',
stepId: 'step-1',
});
expect(result.events[4]?.content).toEqual({
kind: 'function_response',
id: 'tool-1',
name: 'Read',
result: { kind: 'text', text: 'file body' },
isError: false,
});
expect(result.events[4]?.actions?.stateDelta?.durationMs).toBe(42);
expect(result.events[5]?.actions?.permissionDecision).toEqual({
requestId: 'perm-1',
decision: 'allow',
rememberForTurn: true,
});
expect(result.events[5]?.refs).toEqual({ storedMessageId: 'perm-1', toolCallId: 'tool-1' });
expect(result.events[6]?.actions?.tokenUsage).toEqual({ input: 10, output: 5, total: 15 });
expect(result.events[7]?.status).toBe('completed');
expect(result.events[7]?.actions?.endInvocation).toBe(true);
expect(result.events[7]?.refs).toEqual({ storedMessageId: 'legacy-state' });
expect(recoveryMarker(result.events[3]!)).toBe(undefined);
expect(recoveryMarker(result.events[4]!)).toBe(undefined);
for (const index of [0, 1, 2, 5, 6, 7]) {
expect(recoveryMarker(result.events[index]!)).toMatchObject({
kind: 'runtime_event_backfill',
source: 'legacy_stored_message',
reason: 'missing_runtime_event_ledger',
confidence: 'lossless',
generatedAt: 999,
version: 1,
});
}
});
test('skips high-risk legacy rows that cannot be reconstructed safely', () => {
const messages: StoredMessage[] = [
{
type: 'tool_result',
id: 'orphan-result',
turnId: 'turn-1',
ts: 120,
toolUseId: 'missing-tool',
isError: false,
content: { kind: 'text', text: 'orphan' },
},
{
type: 'permission_decision',
id: 'orphan-permission',
turnId: 'turn-1',
ts: 130,
toolUseId: 'missing-tool',
toolName: 'Write',
decision: 'deny',
},
{
type: 'system_note',
id: 'session-note',
turnId: 'turn-1',
ts: 140,
kind: 'session_resume',
},
{
type: 'turn_state',
id: 'legacy-state',
turnId: 'turn-1',
ts: 180,
status: 'completed',
partialOutputRetained: false,
},
];
const result = backfillRuntimeEventsFromStoredMessages({
run,
messages,
newId: nextIds(),
now: () => 999,
});
expect(result.events.map((event) => event.status)).toEqual(['completed']);
expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
'skipped_unmatched_tool_result',
'skipped_unmatched_permission_decision',
'skipped_high_risk_message',
]);
});
});