Download packages/transcript/test/layers.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 97.5 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/transcript/test/layers.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/transcript/test/layers.test.ts
-
curl -L -o layers.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/transcript/test/layers.test.ts
97.5 kB
| import { describe, expect, it } from 'vitest'; | |
| import { filterOpsForGrade, isAppendOnly, redactSnapshotForGrade } from '#/granularity/filterOps'; | |
| import { detachGrades, gradeFor, needsResetOnTransition } from '#/granularity/grade'; | |
| import { paginateTurns } from '#/pagination/paginate'; | |
| import { ViewRegistry } from '#/view/registry'; | |
| import { projectTranscriptUserOrigin } from '#/contract/origin'; | |
| import { groupMessagesIntoSnapshot, type HistoryContentPart } from '#/history/groupTurns'; | |
| import { foldWireRecordFacts, type HistoryWireRecord } from '#/history/foldFacts'; | |
| import { | |
| transcriptOperationSchema, | |
| transcriptQuerySchema, | |
| transcriptResponseSchema, | |
| transcriptGradeSpecSchema, | |
| transcriptUserOriginSchema, | |
| } from '#/contract/schema'; | |
| import type { TranscriptItem } from '#/model/item'; | |
| import type { AgentTranscriptSnapshot, TranscriptOperation } from '#/ops/operation'; | |
| const idLabel = (i: TranscriptItem): string => | |
| i.kind === 'turn' ? i.turnId : i.kind === 'marker' ? i.markerId : i.refId; | |
| describe('client metadata in transcript user origins', () => { | |
| it('retains user-invoked single skill frame metadata without exposing model-triggered activations as user input', () => { | |
| const origin = { kind: 'skill_activation', trigger: 'user-slash', skillName: 'example-skill', skillArgs: 'args', clientMetadata: [{ display_text: 'Save button' }] }; | |
| expect(transcriptUserOriginSchema.parse(projectTranscriptUserOrigin(origin))).toEqual(origin); | |
| expect(projectTranscriptUserOrigin({ ...origin, trigger: 'model-tool' })).toBeUndefined(); | |
| }); | |
| it('rebuilds a user turn payload without server-local paths', () => { | |
| const clientMetadata = [{ display_text: 'Visible prompt' }]; | |
| const origin = { | |
| kind: 'user', | |
| clientMetadata, | |
| skillActivations: [{ activationId: 'a1', skillName: 'deploy', skillArgs: 'now', skillPath: '/private/deploy/SKILL.md' }], | |
| attachments: [{ name: 'notes.pdf', mediaType: 'application/pdf', size: 42, path: '/private/notes.pdf' }], | |
| }; | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'rendered skill' }, { type: 'text', text: 'visible prompt' }], toolCalls: [], origin }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'reply' }], toolCalls: [] }, | |
| ]); | |
| const turn = snapshot.items.find((item) => item.kind === 'turn'); | |
| expect(turn?.origin).toEqual({ kind: 'user', payload: { kind: 'user', clientMetadata, skillActivations: [{ skillName: 'deploy', skillArgs: 'now' }] } }); | |
| expect(JSON.stringify(turn)).not.toContain('/private/'); | |
| expect(JSON.stringify(snapshot.attachments)).not.toContain('/private/'); | |
| }); | |
| it('keeps opening prompt metadata when rebuilding history turns', () => { | |
| const clientMetadata = [{ kimi_code_composer: { version: 1, doc: { type: 'doc' } } }]; | |
| const origin = { kind: 'user', clientMetadata }; | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'visible prompt' }], toolCalls: [], origin }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'reply' }], toolCalls: [] }, | |
| ]); | |
| const turn = snapshot.items.find((item) => item.kind === 'turn'); | |
| expect(turn?.origin).toEqual({ kind: 'user', payload: { kind: 'user', clientMetadata } }); | |
| expect(turn?.prompt).toBe('visible prompt'); | |
| }); | |
| it('projects and validates independent document snapshots without losing their nested fields', () => { | |
| const clientMetadata = [ | |
| { kimi_code_composer: { version: 1, doc: { type: 'doc', content: [{ type: 'paragraph' }] }, captureIds: ['capture-a'] } }, | |
| { kimi_code_composer: { version: 1, captureIds: ['capture-b'] } }, | |
| ]; | |
| const projected = projectTranscriptUserOrigin({ kind: 'user', clientMetadata }); | |
| expect(transcriptUserOriginSchema.parse(projected)).toEqual({ kind: 'user', clientMetadata }); | |
| expect(projectTranscriptUserOrigin({ kind: 'user' })).toStrictEqual({ kind: 'user' }); | |
| expect(projectTranscriptUserOrigin({ kind: 'injection', clientMetadata })).toBeUndefined(); | |
| }); | |
| }); | |
| describe('user slash skill activations as transcript origins', () => { | |
| it('projects a user-invoked activation and rejects a model-triggered one', () => { | |
| const origin = { kind: 'skill_activation', trigger: 'user-slash', skillName: 'example-skill', skillArgs: 'args' }; | |
| expect(transcriptUserOriginSchema.parse(projectTranscriptUserOrigin(origin))).toEqual(origin); | |
| expect(projectTranscriptUserOrigin({ ...origin, trigger: 'model-tool' })).toBeUndefined(); | |
| expect(projectTranscriptUserOrigin({ ...origin, skillName: '' })).toBeUndefined(); | |
| expect(projectTranscriptUserOrigin({ kind: 'user' })).toStrictEqual({ kind: 'user' }); | |
| }); | |
| }); | |
| const turnOp = (n: number): TranscriptOperation => ({ | |
| op: 'turn.upsert', | |
| turn: { kind: 'turn', turnId: `t${n}`, ordinal: n, state: 'running', origin: { kind: 'user' } }, | |
| }); | |
| const stepOp: TranscriptOperation = { | |
| op: 'step.upsert', | |
| turnId: 't1', | |
| step: { kind: 'step', stepId: 't1.1', turnId: 't1', ordinal: 1, state: 'running' }, | |
| }; | |
| const frameOp: TranscriptOperation = { | |
| op: 'frame.upsert', | |
| turnId: 't1', | |
| stepId: 't1.1', | |
| frame: { kind: 'text', frameId: 't1.1.f1', role: 'assistant', text: 'full' }, | |
| }; | |
| const appendOp: TranscriptOperation = { | |
| op: 'append', | |
| target: { type: 'frame', turnId: 't1', stepId: 't1.1', frameId: 't1.1.f1' }, | |
| offset: 0, | |
| text: 'chunk', | |
| }; | |
| const promptOp: TranscriptOperation = { | |
| op: 'prompt.upsert', | |
| prompt: { promptId: 'p1', status: 'queued', createdAt: '2026-07-22T00:00:00.000Z' }, | |
| }; | |
| describe('granularity', () => { | |
| const ops: TranscriptOperation[] = [ | |
| turnOp(1), | |
| stepOp, | |
| frameOp, | |
| appendOp, | |
| promptOp, | |
| { op: 'meta.merge', meta: { activity: 'turn' } }, | |
| ]; | |
| it('off admits nothing', () => { | |
| expect(filterOpsForGrade('off', ops)).toEqual([]); | |
| }); | |
| it('turn admits headers and global state only', () => { | |
| expect(filterOpsForGrade('turn', ops).map((op) => op.op)).toEqual([ | |
| 'turn.upsert', | |
| 'prompt.upsert', | |
| 'meta.merge', | |
| ]); | |
| }); | |
| it('block admits step/frame upserts but no appends', () => { | |
| expect(filterOpsForGrade('block', ops).map((op) => op.op)).toEqual([ | |
| 'turn.upsert', | |
| 'step.upsert', | |
| 'frame.upsert', | |
| 'prompt.upsert', | |
| 'meta.merge', | |
| ]); | |
| }); | |
| it('delta admits everything', () => { | |
| expect(filterOpsForGrade('delta', ops)).toHaveLength(ops.length); | |
| }); | |
| it('gradeFor resolves agent override over wildcard default', () => { | |
| const spec = { '*': 'turn', main: 'delta' } as const; | |
| expect(gradeFor(spec, 'main')).toBe('delta'); | |
| expect(gradeFor(spec, 'sub-1')).toBe('turn'); | |
| expect(gradeFor(undefined, 'main')).toBe('off'); | |
| }); | |
| it('upgrade needs reset, downgrade does not', () => { | |
| expect(needsResetOnTransition('turn', 'delta')).toBe(true); | |
| expect(needsResetOnTransition('delta', 'turn')).toBe(false); | |
| }); | |
| it('detachGrades writes explicit off so a wildcard default cannot resurrect the agent', () => { | |
| expect(detachGrades({ '*': 'delta' }, ['main'])).toEqual({ '*': 'delta', main: 'off' }); | |
| expect(detachGrades({ '*': 'delta', main: 'turn' }, ['main'])).toEqual({ | |
| '*': 'delta', | |
| main: 'off', | |
| }); | |
| }); | |
| it('detachGrades deletes a listed wildcard entry', () => { | |
| expect(detachGrades({ '*': 'delta', main: 'turn' }, ['*'])).toEqual({ main: 'turn' }); | |
| }); | |
| it('detachGrades collapses an all-off spec to undefined', () => { | |
| expect(detachGrades({ main: 'delta' }, ['main'])).toBeUndefined(); | |
| expect(detachGrades({ '*': 'delta', main: 'off' }, ['*'])).toBeUndefined(); | |
| expect(detachGrades(undefined, ['main'])).toBeUndefined(); | |
| }); | |
| it('append-only batches are volatile-safe', () => { | |
| expect(isAppendOnly([appendOp])).toBe(true); | |
| expect(isAppendOnly([appendOp, frameOp])).toBe(false); | |
| }); | |
| it('redactSnapshotForGrade strips step detail below block, keeps it at block+', () => { | |
| const snapshot: AgentTranscriptSnapshot = { | |
| items: [ | |
| { | |
| kind: 'turn', | |
| turnId: 't1', | |
| ordinal: 1, | |
| state: 'completed', | |
| origin: { kind: 'user' }, | |
| prompt: 'hi', | |
| steps: [ | |
| { | |
| kind: 'step', | |
| stepId: 't1.1', | |
| turnId: 't1', | |
| ordinal: 1, | |
| state: 'completed', | |
| frames: [{ kind: 'text', frameId: 't1.1.f1', role: 'assistant', text: 'body' }], | |
| }, | |
| ], | |
| }, | |
| { kind: 'marker', markerId: 'm1', marker: 'skill' }, | |
| ], | |
| tasks: [], | |
| interactions: [ | |
| { | |
| interactionId: 'appr-1', | |
| interactionKind: 'approval' as const, | |
| toolCallId: 'c1', | |
| state: 'pending' as const, | |
| }, | |
| ], | |
| attachments: [ | |
| { attachmentId: 'att_1', mediaType: 'image/png', source: { kind: 'url' as const, url: 'https://example.com/a.png' } }, | |
| ], | |
| todos: [{ todoId: 'todo', items: [{ title: 'write tests', status: 'in_progress' as const }] }], | |
| prompts: [{ promptId: 'p1', status: 'running' as const, createdAt: '2026-07-22T00:00:00.000Z' }], | |
| meta: {}, | |
| }; | |
| const turnGrade = redactSnapshotForGrade('turn', snapshot); | |
| expect(turnGrade.interactions).toHaveLength(1); | |
| expect(turnGrade.attachments).toHaveLength(1); | |
| expect(turnGrade.todos).toHaveLength(1); | |
| expect(turnGrade.prompts).toHaveLength(1); | |
| const turn = turnGrade.items[0]; | |
| expect(turn?.kind === 'turn' && turn.steps).toEqual([]); | |
| expect(turn?.kind === 'turn' && turn.prompt).toBe('hi'); | |
| expect(turnGrade.items[1]?.kind).toBe('marker'); | |
| expect(redactSnapshotForGrade('block', snapshot)).toBe(snapshot); | |
| expect(redactSnapshotForGrade('delta', snapshot)).toBe(snapshot); | |
| }); | |
| }); | |
| describe('paginateTurns', () => { | |
| const items: TranscriptItem[] = [ | |
| { kind: 'marker', markerId: 'm0', marker: 'goal' }, | |
| ...[1, 2, 3, 4, 5].flatMap((n): TranscriptItem[] => [ | |
| { | |
| kind: 'turn', | |
| turnId: `t${n}`, | |
| ordinal: n, | |
| state: 'completed', | |
| origin: { kind: 'user' }, | |
| steps: [], | |
| }, | |
| { kind: 'marker', markerId: `m${n}`, marker: 'skill' }, | |
| ]), | |
| ]; | |
| it('default page is the newest N turns with trailing segment items', () => { | |
| const page = paginateTurns(items, { pageSize: 2 }); | |
| expect(page.items.map(idLabel)).toEqual(['t4', 'm4', 't5', 'm5']); | |
| expect(page.hasMore).toBe(true); | |
| }); | |
| it('before_turn pages toward older turns; head marker rides the oldest segment', () => { | |
| const page = paginateTurns(items, { beforeTurn: 't4', pageSize: 2 }); | |
| expect(page.items.map(idLabel)).toEqual(['t2', 'm2', 't3', 'm3']); | |
| expect(page.hasMore).toBe(true); | |
| const oldest = paginateTurns(items, { beforeTurn: 't2', pageSize: 5 }); | |
| expect(oldest.items[0]).toEqual({ kind: 'marker', markerId: 'm0', marker: 'goal' }); | |
| expect(oldest.hasMore).toBe(false); | |
| }); | |
| it('after_turn pages toward newer turns without the head unit', () => { | |
| const page = paginateTurns(items, { afterTurn: 't3', pageSize: 2 }); | |
| expect(page.items.map(idLabel)).toEqual(['t4', 'm4', 't5', 'm5']); | |
| expect(page.hasMore).toBe(false); | |
| }); | |
| it('keeps head non-turn items with the newest page when turns exactly fill it', () => { | |
| const page = paginateTurns(items, { pageSize: 5 }); | |
| expect(page.items[0]).toEqual({ kind: 'marker', markerId: 'm0', marker: 'goal' }); | |
| expect(page.items.map(idLabel)).toEqual(['m0', 't1', 'm1', 't2', 'm2', 't3', 'm3', 't4', 'm4', 't5', 'm5']); | |
| expect(page.hasMore).toBe(false); | |
| }); | |
| it('returns a marker-only timeline as one page with nothing older', () => { | |
| const only = paginateTurns([{ kind: 'marker', markerId: 'm0', marker: 'goal' }], { pageSize: 3 }); | |
| expect(only.items.map(idLabel)).toEqual(['m0']); | |
| expect(only.hasMore).toBe(false); | |
| }); | |
| }); | |
| describe('ViewRegistry', () => { | |
| it('dispatches on view ?? name, origin.kind and marker keys', () => { | |
| const registry = new ViewRegistry<string>({ fallbackTool: 'generic' }); | |
| registry.registerTool('read', 'readRenderer'); | |
| registry.registerTool('swarm', 'swarmRenderer'); | |
| registry.registerInput('cron', 'cronInput'); | |
| registry.registerMarker('goal', 'goalMarker'); | |
| expect( | |
| registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c1', name: 'Read', state: 'done' }), | |
| ).toBe('readRenderer'); | |
| expect( | |
| registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c2', name: 'AgentSwarm', view: 'swarm', state: 'running' }), | |
| ).toBe('swarmRenderer'); | |
| expect( | |
| registry.resolveTool({ kind: 'tool', frameId: 'f', toolCallId: 'c3', name: 'Bash', state: 'running' }), | |
| ).toBe('generic'); | |
| expect(registry.resolveInput({ kind: 'cron' })).toBe('cronInput'); | |
| expect(registry.resolveInput({ kind: 'user' })).toBeUndefined(); | |
| expect(registry.resolveMarker('goal')).toBe('goalMarker'); | |
| }); | |
| }); | |
| describe('contract schemas', () => { | |
| it('roundtrips every op kind', () => { | |
| const ops: TranscriptOperation[] = [ | |
| { op: 'reset', agentId: 'main', snapshot: { items: [], tasks: [], interactions: [], attachments: [], todos: [], prompts: [], meta: {}, hasMoreOlder: true } }, | |
| turnOp(1), | |
| stepOp, | |
| frameOp, | |
| appendOp, | |
| { op: 'marker.upsert', item: { kind: 'marker', markerId: 'm1', marker: 'goal' } }, | |
| { op: 'taskref.upsert', item: { kind: 'taskref', refId: 'r1', taskId: 'task1' } }, | |
| { op: 'task.upsert', task: { taskId: 'task1', kind: 'shell', state: 'running', detached: false, outputTail: '' } }, | |
| { | |
| op: 'interaction.upsert', | |
| interaction: { interactionId: 'appr-1', interactionKind: 'approval', toolCallId: 'c1', state: 'pending' }, | |
| }, | |
| { | |
| op: 'attachment.upsert', | |
| attachment: { attachmentId: 'att_1', mediaType: 'image/png', source: { kind: 'file', fileId: 'f1' } }, | |
| }, | |
| { op: 'todo.upsert', todo: { todoId: 'todo', items: [{ title: 'x', status: 'done' }] } }, | |
| promptOp, | |
| { op: 'meta.merge', meta: { goal: { objective: 'x', status: 'active' } } }, | |
| { op: 'items.remove', ids: ['t1'] }, | |
| ]; | |
| for (const op of ops) { | |
| expect(transcriptOperationSchema.parse(op)).toBeDefined(); | |
| } | |
| }); | |
| it('roundtrips ops carrying the extended wire detail', () => { | |
| const usage = { inputOther: 10, output: 5, inputCacheRead: 3, inputCacheCreation: 2 }; | |
| const ops: TranscriptOperation[] = [ | |
| { | |
| op: 'reset', | |
| agentId: 'main', | |
| snapshot: { | |
| items: [], | |
| tasks: [], | |
| interactions: [], | |
| attachments: [], | |
| todos: [], | |
| prompts: [ | |
| { | |
| promptId: 'p1', | |
| status: 'completed', | |
| userMessageId: 'u1', | |
| content: [{ type: 'text', text: 'hi' }], | |
| createdAt: '2026-07-22T00:00:00.000Z', | |
| finishedAt: '2026-07-22T00:01:00.000Z', | |
| steeredAt: '2026-07-22T00:00:30.000Z', | |
| }, | |
| ], | |
| meta: { | |
| agent: { | |
| model: 'k2', | |
| thinkingEffort: 'high', | |
| usage: { byModel: { k2: usage }, currentTurn: usage, total: usage }, | |
| contextTokens: 1234, | |
| maxContextTokens: 128000, | |
| contextUsage: 0.01, | |
| permission: 'auto', | |
| phase: { kind: 'retrying', turnId: 1, step: 1, stepId: 't1.1', failedAttempt: 1, nextAttempt: 2, maxAttempts: 3, delayMs: 500, since: 1000 }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| op: 'turn.upsert', | |
| turn: { | |
| kind: 'turn', turnId: 't1', triggerPromptId: 'prompt-1', ordinal: 1, state: 'failed', origin: { kind: 'user' }, | |
| usage: { inputTokens: 12, outputTokens: 5, cachedTokens: 3 }, | |
| durationMs: 1500, | |
| error: 'boom', | |
| }, | |
| }, | |
| { | |
| op: 'step.upsert', | |
| turnId: 't1', | |
| step: { | |
| kind: 'step', stepId: 't1.1', turnId: 't1', ordinal: 1, state: 'interrupted', | |
| usage, | |
| finishReason: 'stop', | |
| timing: { | |
| llmFirstTokenLatencyMs: 120, | |
| llmStreamDurationMs: 900, | |
| llmRequestBuildMs: 5, | |
| llmServerFirstTokenMs: 110, | |
| llmServerDecodeMs: 700, | |
| llmClientConsumeMs: 950, | |
| llmClientBlockedMs: 25, | |
| }, | |
| retry: { failedAttempt: 1, nextAttempt: 2, maxAttempts: 3, delayMs: 500, errorName: 'RateLimit', errorMessage: 'slow down', statusCode: 429 }, | |
| endReason: 'aborted', | |
| endMessage: 'user pressed escape', | |
| }, | |
| }, | |
| { | |
| op: 'frame.upsert', | |
| turnId: 't1', | |
| stepId: 't1.1', | |
| frame: { | |
| kind: 'tool', frameId: 't1.1.c1', toolCallId: 'c1', name: 'Bash', state: 'running', | |
| inputText: '{"command":"ls', | |
| progress: { kind: 'progress', text: 'half', percent: 50, customKind: 'bar', customData: { x: 1 } }, | |
| }, | |
| }, | |
| { | |
| op: 'task.upsert', | |
| task: { | |
| taskId: 'task1', kind: 'subagent', state: 'completed', detached: false, outputTail: '', | |
| resultSummary: 'scanned 12 files', | |
| error: 'partial failure', | |
| stateReason: 'waiting for input', | |
| usage, | |
| }, | |
| }, | |
| { | |
| op: 'meta.merge', | |
| meta: { agent: { model: 'k2', phase: { kind: 'ended', turnId: 1, reason: 'completed', durationMs: 1500, at: 2000 } } }, | |
| }, | |
| ]; | |
| for (const op of ops) { | |
| expect(transcriptOperationSchema.parse(op)).toEqual(op); | |
| } | |
| }); | |
| it('accepts provenance only on user text frames', () => { | |
| const base = { | |
| op: 'frame.upsert', | |
| turnId: 't1', | |
| stepId: 't1.1', | |
| } as const; | |
| expect(transcriptOperationSchema.safeParse({ | |
| ...base, | |
| frame: { | |
| kind: 'text', | |
| frameId: 't1.1.f1', | |
| role: 'user', | |
| text: 'steered in', | |
| origin: { kind: 'user', skillActivations: [{ skillName: 'review', skillArgs: 'strict' }] }, | |
| }, | |
| }).success).toBe(true); | |
| expect(transcriptOperationSchema.safeParse({ | |
| ...base, | |
| frame: { | |
| kind: 'text', | |
| frameId: 't1.1.f2', | |
| role: 'assistant', | |
| text: 'reply', | |
| origin: { kind: 'user' }, | |
| }, | |
| }).success).toBe(false); | |
| }); | |
| it('rejects mutually exclusive cursors and bad grades', () => { | |
| expect(() => transcriptGradeSpecSchema.parse({ '*': 'stream' })).toThrow(); | |
| const ok = transcriptResponseSchema.safeParse({ | |
| agent_id: 'main', | |
| items: [], | |
| has_more: false, | |
| tasks: [], | |
| interactions: [], | |
| attachments: [], | |
| todos: [], | |
| meta: {}, | |
| agents: [{ agentId: 'main', type: 'main' }], | |
| pending_interactions: [], | |
| }); | |
| expect(ok.success).toBe(true); | |
| }); | |
| it('rejects path-hostile agent ids in the transcript query', () => { | |
| const base = { agent_id: 'main', before_turn: undefined, after_turn: undefined, page_size: undefined }; | |
| expect(transcriptQuerySchema.safeParse({ ...base, agent_id: 'sub-1' }).success).toBe(true); | |
| expect(transcriptQuerySchema.safeParse({ ...base, agent_id: '01HF7YAT31J7SMRT1QXGJWKR8D' }).success).toBe(true); | |
| for (const hostile of ['../main', '..\\main', '..', 'a/b', 'a\\b', '.', 'a\0b', 'x'.repeat(200)]) { | |
| expect(transcriptQuerySchema.safeParse({ ...base, agent_id: hostile }).success).toBe(false); | |
| } | |
| }); | |
| }); | |
| describe('groupMessagesIntoSnapshot (cold path)', () => { | |
| const snapshotOf = (...parts: HistoryContentPart[]): AgentTranscriptSnapshot => | |
| groupMessagesIntoSnapshot([ | |
| { role: 'user', content: parts, toolCalls: [], origin: { kind: 'user' } }, | |
| ]); | |
| it('groups flat messages into turns with folded tool results', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'system', content: [{ type: 'text', text: 'sys' }] }, | |
| { role: 'user', content: [{ type: 'text', text: 'hello' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'assistant', | |
| content: [{ type: 'think', think: 'hmm' }, { type: 'text', text: 'checking' }], | |
| toolCalls: [{ id: 'c1', name: 'Read', arguments: '{"path":"/a"}' }], | |
| }, | |
| { role: 'tool', content: [{ type: 'text', text: 'file body' }], toolCallId: 'c1', toolCalls: [] }, | |
| { | |
| role: 'assistant', | |
| content: [{ type: 'text', text: 'done' }], | |
| toolCalls: [], | |
| }, | |
| { role: 'user', content: [{ type: 'text', text: 'next' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'summary of old' }], | |
| toolCalls: [], | |
| origin: { kind: 'compaction_summary' }, | |
| }, | |
| { role: 'user', content: [{ type: 'text', text: 'after' }], toolCalls: [], origin: { kind: 'user' } }, | |
| ]); | |
| const kinds = snapshot.items.map((i) => i.kind); | |
| expect(kinds).toEqual(['turn', 'turn', 'marker', 'turn']); | |
| const firstTurn = snapshot.items[0]; | |
| if (firstTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(firstTurn.prompt).toBe('hello'); | |
| expect(firstTurn.steps).toHaveLength(2); | |
| const tool = firstTurn.steps[0]?.frames.find((f) => f.kind === 'tool'); | |
| expect(tool?.kind === 'tool' && tool.output).toBe('file body'); | |
| expect(tool?.kind === 'tool' && tool.input).toEqual({ path: '/a' }); | |
| const marker = snapshot.items[2]; | |
| expect(marker?.kind === 'marker' && marker.marker).toBe('compaction'); | |
| }); | |
| it('folds task-notification user messages into the current turn instead of opening their own', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'assistant', | |
| content: [{ type: 'text', text: 'starting' }], | |
| toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{"command":"ls"}' }], | |
| }, | |
| { | |
| role: 'user', | |
| content: [{ | |
| type: 'text', | |
| text: '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">\nTitle: Background agent completed\nSeverity: info\ninspect done.\n</notification>', | |
| }], | |
| toolCalls: [], | |
| origin: { kind: 'task', taskId: 'task-9' } as { kind: string }, | |
| }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: '<notification id="task:task-8:completed"></notification>' }], | |
| toolCalls: [], | |
| origin: { kind: 'task', taskId: 'task-8' } as { kind: string }, | |
| }, | |
| { | |
| role: 'assistant', | |
| content: [{ type: 'text', text: 'continuing' }], | |
| toolCalls: [], | |
| }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turns = snapshot.items.filter((i) => i.kind === 'turn'); | |
| expect(turns).toHaveLength(1); | |
| const turn = turns[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const userFrames = turn.steps | |
| .flatMap((step) => step.frames) | |
| .filter((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(userFrames.map((f) => f.kind === 'text' && f.taskId)).toEqual(['task-9', 'task-8']); | |
| expect(userFrames[0]).toMatchObject({ text: 'Background agent completed\ninspect done.' }); | |
| const assistantTexts = turn.steps | |
| .flatMap((step) => step.frames) | |
| .filter((f) => f.kind === 'text' && f.role === 'assistant') | |
| .map((f) => f.kind === 'text' && f.text); | |
| expect(assistantTexts).toEqual(['starting', 'continuing']); | |
| expect(turn.steps).toHaveLength(2); | |
| expect(turn.steps[1]?.frames.map((f) => f.kind === 'text' && f.role)).toEqual([ | |
| 'user', | |
| 'user', | |
| 'assistant', | |
| ]); | |
| }); | |
| it('folds a user message whose content matches a turn.steer record into the current turn as a user frame', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: 'steered in' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((i) => i.kind)).toEqual(['turn']); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(2); | |
| expect(turn.steps[1]?.frames[0]).toMatchObject({ | |
| kind: 'text', | |
| role: 'user', | |
| text: 'steered in', | |
| }); | |
| }); | |
| it('keeps a trailing steered message visible by appending it to the last step', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: 'steered in' }], toolCalls: [], origin: { kind: 'user' } }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((i) => i.kind)).toEqual(['turn']); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const lastStep = turn.steps.at(-1); | |
| expect(lastStep?.frames.at(-1)).toMatchObject({ | |
| kind: 'text', | |
| role: 'user', | |
| text: 'steered in', | |
| }); | |
| }); | |
| it('flushes a pending steer into the closing turn when a new turn opens before any reply', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: 'steered in' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'user', content: [{ type: 'text', text: 'next question' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) }, | |
| ); | |
| const turns = snapshot.items.filter((i) => i.kind === 'turn'); | |
| expect(turns).toHaveLength(2); | |
| const first = turns[0]; | |
| if (first?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(first.steps.at(-1)?.frames.at(-1)).toMatchObject({ | |
| kind: 'text', | |
| role: 'user', | |
| text: 'steered in', | |
| }); | |
| const second = turns[1]; | |
| if (second?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(second.prompt).toBe('next question'); | |
| }); | |
| it('still opens its own turn for a mid-conversation user message unknown to the steer map', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: 'plain follow-up' }], toolCalls: [], origin: { kind: 'user' } }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify([{ type: 'text', text: 'steered in' }]), new Map([['user', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((i) => i.kind)).toEqual(['turn', 'turn']); | |
| }); | |
| it('stops folded notification text before child output blocks', () => { | |
| const xml = [ | |
| '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">', | |
| 'Title: Background agent completed', | |
| 'Severity: info', | |
| 'inspect done.', | |
| '<output-file>/tmp/out.log</output-file>', | |
| '/tmp/out.log', | |
| 'full output here', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(frame).toMatchObject({ text: 'Background agent completed\ninspect done.' }); | |
| }); | |
| it('stops folded notification text before an inline answer block', () => { | |
| const xml = [ | |
| '<notification id="task:question-1:completed" category="task" type="task.completed" source_kind="background_task" source_id="question-1">', | |
| 'Title: Background question answered', | |
| 'Severity: info', | |
| 'The user answered "Which database?".', | |
| '<answer>', | |
| '{"answers":{"Which database?":"Postgres"}}', | |
| '</answer>', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'question-1' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(frame).toMatchObject({ text: 'Background question answered\nThe user answered "Which database?".' }); | |
| }); | |
| it('buffers a folded notification that arrives before the first step into that step', () => { | |
| const xml = [ | |
| '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">', | |
| 'Title: Background agent completed', | |
| 'Severity: info', | |
| 'early done.', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(1); | |
| expect(turn.steps[0]?.frames.map((f) => f.kind === 'text' && f.role)).toEqual([ | |
| 'user', | |
| 'assistant', | |
| ]); | |
| expect(turn.steps[0]?.frames[0]).toMatchObject({ | |
| text: 'Background agent completed\nearly done.', | |
| taskId: 'task-9', | |
| }); | |
| }); | |
| it('drops a folded notification when no turn is open yet', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: '<notification id="n1"></notification>' }], | |
| toolCalls: [], | |
| origin: { kind: 'task', taskId: 'task-9' } as { kind: string }, | |
| }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| expect(snapshot.items).toHaveLength(0); | |
| }); | |
| it('keeps body lines that start with an angle bracket but are not output blocks', () => { | |
| const xml = [ | |
| '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">', | |
| 'Title: Background agent completed', | |
| 'Severity: info', | |
| 'first line.', | |
| '<div>pasted markup</div>', | |
| 'last line.', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(frame).toMatchObject({ | |
| text: 'Background agent completed\nfirst line.\n<div>pasted markup</div>\nlast line.', | |
| }); | |
| }); | |
| it('parses only the leading header lines, keeping later Title-like lines as body', () => { | |
| const xml = [ | |
| '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">', | |
| 'Title: Background agent completed', | |
| 'Severity: info', | |
| 'first line.', | |
| 'Title: details', | |
| 'last line.', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(frame).toMatchObject({ | |
| text: 'Background agent completed\nfirst line.\nTitle: details\nlast line.', | |
| }); | |
| }); | |
| it('keeps a body whose first line starts with Severity:', () => { | |
| const xml = [ | |
| '<notification id="task:task-9:completed" category="task" type="task.completed" source_kind="background_task" source_id="task-9">', | |
| 'Title: Background agent completed', | |
| 'Severity: info', | |
| 'Severity: this line is the task description, not a header', | |
| 'last line.', | |
| '</notification>', | |
| ].join('\n'); | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'go' }], toolCalls: [] }, | |
| { role: 'user', content: [{ type: 'text', text: xml }], toolCalls: [], origin: { kind: 'task', taskId: 'task-9' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set() }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const frame = turn.steps.flatMap((step) => step.frames).find((f) => f.kind === 'text' && f.role === 'user'); | |
| expect(frame).toMatchObject({ | |
| text: 'Background agent completed\nSeverity: this line is the task description, not a header\nlast line.', | |
| }); | |
| }); | |
| it('expands a bundled prompt into per-skill markers and a caller-text turn', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: 'rendered review block' }, | |
| { type: 'text', text: 'rendered security block' }, | |
| { type: 'text', text: 'please /skill:review and /skill:security' }, | |
| ], | |
| toolCalls: [], | |
| origin: { | |
| kind: 'user', | |
| skillActivations: [ | |
| { activationId: 'act-1', skillName: 'review' }, | |
| { activationId: 'act-2', skillName: 'security', skillArgs: 'src/app.ts' }, | |
| ], | |
| } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['marker', 'marker', 'turn']); | |
| const first = snapshot.items[0]; | |
| expect(first?.kind === 'marker' && first.marker).toBe('skill'); | |
| expect(first?.kind === 'marker' && first.payload).toMatchObject({ | |
| text: 'rendered review block', | |
| origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'review' }, | |
| }); | |
| const second = snapshot.items[1]; | |
| expect(second?.kind === 'marker' && second.payload).toMatchObject({ | |
| text: 'rendered security block', | |
| origin: { skillName: 'security', skillArgs: 'src/app.ts' }, | |
| }); | |
| const turn = snapshot.items[2]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.prompt).toBe('please /skill:review and /skill:security'); | |
| expect(turn.steps).toHaveLength(1); | |
| }); | |
| it('maps media parts on the opening user message to attachment entities, dropping base64 bytes', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: 'what is this? [Image #1]' }, | |
| { type: 'image', source: { kind: 'base64', media_type: 'image/png', data: 'aGVsbG8=' }, name: 'inline.png' }, | |
| { type: 'image', source: { kind: 'url', url: 'https://example.com/pic.png' }, name: 'remote.png' }, | |
| { type: 'image', source: { kind: 'file', file_id: 'file_8' }, name: 'stored.png' }, | |
| { type: 'file', file_id: 'file_9', name: 'notes.txt', media_type: 'text/plain', size: 128 }, | |
| ], | |
| toolCalls: [], | |
| origin: { kind: 'user' }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a screenshot' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.attachments).toHaveLength(4); | |
| expect(snapshot.attachments[0]).toMatchObject({ | |
| attachmentId: 'att_1', | |
| mediaType: 'image/png', | |
| name: 'inline.png', | |
| source: undefined, | |
| }); | |
| expect(snapshot.attachments[1]).toMatchObject({ | |
| attachmentId: 'att_2', | |
| name: 'remote.png', | |
| source: { kind: 'url', url: 'https://example.com/pic.png' }, | |
| }); | |
| expect(snapshot.attachments[2]).toMatchObject({ | |
| attachmentId: 'att_3', | |
| name: 'stored.png', | |
| source: { kind: 'file', fileId: 'file_8' }, | |
| }); | |
| expect(snapshot.attachments[3]).toMatchObject({ | |
| attachmentId: 'att_4', | |
| mediaType: 'text/plain', | |
| name: 'notes.txt', | |
| source: { kind: 'file', fileId: 'file_9' }, | |
| }); | |
| const firstTurn = snapshot.items[0]; | |
| if (firstTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(firstTurn.attachmentIds).toEqual(['att_1', 'att_2', 'att_3', 'att_4']); | |
| }); | |
| it('folds origin file attachments on the opening user message into path-sourced entities', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { | |
| type: 'text', | |
| text: 'Attached file "report.pdf" (application/pdf, 128 bytes): /data/report.pdf — open it with the Read tool', | |
| }, | |
| ], | |
| toolCalls: [], | |
| origin: { | |
| kind: 'user', | |
| attachments: [ | |
| { name: 'report.pdf', mediaType: 'application/pdf', size: 128, path: '/data/report.pdf' }, | |
| ], | |
| } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'application/pdf', | |
| name: 'report.pdf', | |
| size: 128, | |
| }, | |
| ]); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.attachmentIds).toEqual(['att_1']); | |
| }); | |
| it('folds origin file attachments on a skill activation message into path-sourced entities', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { | |
| type: 'text', | |
| text: 'User activated the skill "update-config".', | |
| }, | |
| ], | |
| toolCalls: [], | |
| origin: { | |
| kind: 'skill_activation', | |
| activationId: 'act_1', | |
| skillName: 'update-config', | |
| trigger: 'user-slash', | |
| attachments: [ | |
| { name: 'note.txt', mediaType: 'text/plain', size: 21, path: '/data/note.txt' }, | |
| ], | |
| } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'done' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'text/plain', | |
| name: 'note.txt', | |
| size: 21, | |
| }, | |
| ]); | |
| const turn = snapshot.items[1]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.attachmentIds).toEqual(['att_1']); | |
| }); | |
| it('maps persisted kimi-file media refs to attachments', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { | |
| type: 'video_url', | |
| videoUrl: { url: 'kimi-file://file_1', name: 'clip.mp4' }, | |
| } as HistoryContentPart, | |
| ], | |
| toolCalls: [], | |
| origin: { kind: 'user' }, | |
| }, | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: 'what is this?' }, | |
| { | |
| type: 'image_url', | |
| imageUrl: { url: 'kimi-file://file_2?path=%2Fcache%2Fshot.png', name: 'shot.png' }, | |
| } as HistoryContentPart, | |
| ], | |
| toolCalls: [], | |
| origin: { kind: 'user' }, | |
| }, | |
| ]); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'video/*', | |
| name: 'clip.mp4', | |
| source: { kind: 'session_media', fileId: 'file_1' }, | |
| }, | |
| { | |
| attachmentId: 'att_2', | |
| mediaType: 'image/*', | |
| name: 'shot.png', | |
| source: { kind: 'session_media', fileId: 'file_2' }, | |
| }, | |
| ]); | |
| const videoTurn = snapshot.items[0]; | |
| const imageTurn = snapshot.items[1]; | |
| if (videoTurn?.kind !== 'turn' || imageTurn?.kind !== 'turn') { | |
| throw new Error('expected turns'); | |
| } | |
| expect(videoTurn.attachmentIds).toEqual(['att_1']); | |
| expect(imageTurn.attachmentIds).toEqual(['att_2']); | |
| expect(videoTurn.prompt).toBe(''); | |
| expect(imageTurn.prompt).toBe('what is this?'); | |
| }); | |
| it('projects a daemon ref in a user-slash turn-opening input like a plain user turn', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [ | |
| { type: 'text', text: '/gen-docs ' }, | |
| { | |
| type: 'image_url', | |
| imageUrl: { url: 'kimi-file://file_4?path=%2Fcache%2Fshot.png' }, | |
| } as HistoryContentPart, | |
| ], | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as { | |
| kind: string; | |
| }, | |
| }, | |
| ]); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'image/*', | |
| source: { kind: 'session_media', fileId: 'file_4' }, | |
| }, | |
| ]); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['marker', 'turn']); | |
| const marker = snapshot.items[0]; | |
| if (marker?.kind !== 'marker') throw new Error('expected marker'); | |
| expect((marker.payload as { text: string }).text).toBe('/gen-docs '); | |
| const turn = snapshot.items[1]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.prompt).toBe('/gen-docs '); | |
| expect(turn.attachmentIds).toEqual(['att_1']); | |
| }); | |
| it('keeps a kimi-file ref as a nameless attachment and inline tags as user text', () => { | |
| const snapshot = snapshotOf( | |
| { type: 'text', text: 'open <image path="/tmp/other.png"></image> please' }, | |
| { | |
| type: 'image_url', | |
| imageUrl: { url: 'kimi-file://file_3' }, | |
| } as HistoryContentPart, | |
| ); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'image/*', | |
| source: { kind: 'session_media', fileId: 'file_3' }, | |
| }, | |
| ]); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.attachmentIds).toEqual(['att_1']); | |
| expect(turn.prompt).toBe('open <image path="/tmp/other.png"></image> please'); | |
| }); | |
| it('keeps a legacy tag+ref pair as prompt text plus the ref-derived attachment', () => { | |
| const snapshot = snapshotOf( | |
| { type: 'text', text: '<image path="/cache/shot.png"></image>' }, | |
| { | |
| type: 'image_url', | |
| imageUrl: { url: 'kimi-file://file_5?path=%2Fcache%2Fshot.png' }, | |
| } as HistoryContentPart, | |
| ); | |
| expect(snapshot.attachments).toEqual([ | |
| { | |
| attachmentId: 'att_1', | |
| mediaType: 'image/*', | |
| source: { kind: 'session_media', fileId: 'file_5' }, | |
| }, | |
| ]); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.prompt).toBe('<image path="/cache/shot.png"></image>'); | |
| expect(turn.attachmentIds).toEqual(['att_1']); | |
| }); | |
| it('keeps standalone <media path> tags as prompt text', () => { | |
| const snapshot = snapshotOf( | |
| { type: 'text', text: '<image path="/cache/shot.png">' }, | |
| { type: 'text', text: '<image path="/cache/shot.png" content_type="image/png"></image>' }, | |
| { type: 'text', text: '<video path="/cache/clip.mp4">' }, | |
| ); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.prompt).toBe( | |
| '<image path="/cache/shot.png"><image path="/cache/shot.png" content_type="image/png"></image><video path="/cache/clip.mp4">', | |
| ); | |
| expect(turn.attachmentIds).toBeUndefined(); | |
| expect(snapshot.attachments).toEqual([]); | |
| }); | |
| it('keeps cold tool calls running until a result is persisted', () => { | |
| const pending = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [], toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{}' }] }, | |
| ]); | |
| const pendingTurn = pending.items[0]; | |
| if (pendingTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| const pendingTool = pendingTurn.steps[0]?.frames.find((f) => f.kind === 'tool'); | |
| expect(pendingTool?.kind === 'tool' && pendingTool.state).toBe('running'); | |
| const done = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'run it' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [], toolCalls: [{ id: 'c1', name: 'Bash', arguments: '{}' }] }, | |
| { role: 'tool', content: [{ type: 'text', text: 'done.txt' }], toolCallId: 'c1', toolCalls: [] }, | |
| ]); | |
| const doneTurn = done.items[0]; | |
| if (doneTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| const doneTool = doneTurn.steps[0]?.frames.find((f) => f.kind === 'tool'); | |
| expect(doneTool?.kind === 'tool' && doneTool.state).toBe('done'); | |
| expect(doneTool?.kind === 'tool' && doneTool.output).toBe('done.txt'); | |
| }); | |
| it('opens a turn for subagent run prompts recorded as system triggers', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'scan the repo' }], | |
| toolCalls: [], | |
| origin: { kind: 'system_trigger', name: 'subagent' } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'scanning' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'turn']); | |
| const subTurn = snapshot.items[1]; | |
| if (subTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(subTurn.ordinal).toBe(1); | |
| expect(subTurn.origin.kind).toBe('other'); | |
| expect(subTurn.prompt).toBe('scan the repo'); | |
| expect(subTurn.steps).toHaveLength(1); | |
| }); | |
| it('starts a new turn for user-slash skill activations, keeps other triggers as markers only', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'skill body' }], | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as { | |
| kind: string; | |
| }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'docs done' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'nested skill body' }], | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'x' } as { | |
| kind: string; | |
| }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'still same turn' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker', 'turn', 'marker']); | |
| const slashTurn = snapshot.items[2]; | |
| if (slashTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(slashTurn.ordinal).toBe(1); | |
| expect(slashTurn.origin.kind).toBe('other'); | |
| expect(slashTurn.prompt).toBe('skill body'); | |
| expect(slashTurn.steps).toHaveLength(2); | |
| }); | |
| it('keeps model-tool skill activations as markers even when their content matches a steer record', () => { | |
| const skillContent = [{ type: 'text', text: 'skill body' }]; | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: skillContent, | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'write-tui' } as { | |
| kind: string; | |
| }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'used the skill' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify(skillContent), new Map([['skill_activation', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker']); | |
| const marker = snapshot.items[1]; | |
| if (marker?.kind !== 'marker') throw new Error('expected marker'); | |
| expect(marker.marker).toBe('skill'); | |
| }); | |
| it('still folds cron-origin steers into the running turn by content match', () => { | |
| const cronContent = [{ type: 'text', text: 'cron tick' }]; | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: cronContent, | |
| toolCalls: [], | |
| origin: { kind: 'cron_job', jobId: 'job1' } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify(cronContent), new Map([['cron_job', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn']); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(2); | |
| expect(turn.steps[1]?.frames[0]).toMatchObject({ | |
| kind: 'text', | |
| role: 'user', | |
| text: 'cron tick', | |
| }); | |
| }); | |
| it('still folds user-slash skill activations into the running turn by content match', () => { | |
| const slashContent = [{ type: 'text', text: 'slash skill body' }]; | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: slashContent, | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'user-slash', skillName: 'gen-docs' } as { | |
| kind: string; | |
| }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify(slashContent), new Map([['skill_activation', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn']); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(2); | |
| expect(turn.steps[1]?.frames[0]).toMatchObject({ | |
| kind: 'text', | |
| role: 'user', | |
| text: 'slash skill body', | |
| }); | |
| }); | |
| it('consumes the steer count for marker-only activations so a later identical prompt opens its own turn', () => { | |
| const shared = [{ type: 'text', text: 'same text' }]; | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: shared, | |
| toolCalls: [], | |
| origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'x' } as { | |
| kind: string; | |
| }, | |
| }, | |
| { role: 'user', content: shared, toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify(shared), new Map([['skill_activation', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker', 'turn']); | |
| }); | |
| it('does not consume a user steer count for a compaction summary with identical content', () => { | |
| const shared = [{ type: 'text', text: 'same text' }]; | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'active' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'working' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: shared, | |
| toolCalls: [], | |
| origin: { kind: 'compaction_summary' } as { kind: string }, | |
| }, | |
| { role: 'user', content: shared, toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'noted' }], toolCalls: [] }, | |
| ], | |
| { steeredContents: new Map([[JSON.stringify(shared), new Map([['user', 1]])]]) }, | |
| ); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'marker']); | |
| const turn = snapshot.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| const steered = turn.steps | |
| .flatMap((step) => step.frames) | |
| .filter((frame) => frame.kind === 'text' && frame.role === 'user'); | |
| expect(steered).toHaveLength(1); | |
| }); | |
| it('starts a promptless turn for turn-opening system triggers (goal continuation)', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'continue the goal' }], | |
| toolCalls: [], | |
| origin: { kind: 'system_trigger', name: 'goal_continuation' } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'continued' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'mode reminder' }], | |
| toolCalls: [], | |
| origin: { kind: 'injection', variant: 'permission_mode' } as { kind: string }, | |
| }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'still same turn' }], toolCalls: [] }, | |
| ]); | |
| expect(snapshot.items.map((item) => item.kind)).toEqual(['turn', 'turn']); | |
| const [first, second] = snapshot.items; | |
| if (first?.kind !== 'turn' || second?.kind !== 'turn') throw new Error('expected turns'); | |
| expect(first.ordinal).toBe(0); | |
| expect(first.steps.map((step) => step.frames.map((frame) => frame.kind))).toEqual([['text']]); | |
| expect(second.ordinal).toBe(1); | |
| expect(second.origin.kind).toBe('other'); | |
| expect(second.prompt).toBeUndefined(); | |
| expect(second.steps).toHaveLength(2); | |
| }); | |
| it('hides injected user messages and maps cron origins', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'secret context' }], | |
| toolCalls: [], | |
| origin: { kind: 'injection' }, | |
| }, | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'run report' }], | |
| toolCalls: [], | |
| origin: { kind: 'cron_job', jobId: 'job1' } as { kind: string }, | |
| }, | |
| ]); | |
| expect(snapshot.items).toHaveLength(2); | |
| const cronTurn = snapshot.items[1]; | |
| expect(cronTurn?.kind === 'turn' && cronTurn.origin.kind).toBe('cron'); | |
| }); | |
| it('opens a task turn for a notification with a task-origin turn.started in the wire', () => { | |
| const snapshot = groupMessagesIntoSnapshot( | |
| [ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'assistant', | |
| content: [{ type: 'text', text: 'answer' }], | |
| toolCalls: [], | |
| }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'task done' }], | |
| toolCalls: [], | |
| origin: { kind: 'task', taskId: 'task-9' } as { kind: string }, | |
| }, | |
| ], | |
| { taskOriginTurnTaskIds: new Set(['task-9']) }, | |
| ); | |
| const taskTurn = snapshot.items[1]; | |
| if (taskTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(taskTurn.origin).toMatchObject({ kind: 'task', taskId: 'task-9' }); | |
| }); | |
| it('maps legacy background_task origins to task turns, preserving the taskId', () => { | |
| const snapshot = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'task done' }], | |
| toolCalls: [], | |
| origin: { kind: 'background_task', taskId: 'b83rhswvs' } as { kind: string }, | |
| }, | |
| ]); | |
| const taskTurn = snapshot.items[1]; | |
| if (taskTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(taskTurn.origin).toMatchObject({ kind: 'task', taskId: 'b83rhswvs' }); | |
| }); | |
| }); | |
| describe('foldWireRecordFacts (cold facts)', () => { | |
| const baseWithMarker = (): AgentTranscriptSnapshot => | |
| groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'answer' }], toolCalls: [] }, | |
| { | |
| role: 'user', | |
| content: [{ type: 'text', text: 'summary of old' }], | |
| toolCalls: [], | |
| origin: { kind: 'compaction_summary' }, | |
| }, | |
| ]); | |
| it('returns the base snapshot unchanged when no fact records exist (old sessions)', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'metadata', protocol_version: '1.5', created_at: 1 }, | |
| { type: 'context.append_message', message: { role: 'user' }, time: 1 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded).toEqual(base); | |
| expect(folded.items).toBe(base.items); | |
| }); | |
| it('folds todo records into the global todo document, last write wins', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'tools.update_store', key: 'todo', value: [{ title: 'old', status: 'pending' }], time: 1000 }, | |
| { type: 'tools.update_store', key: 'other', value: [{ title: 'ignored', status: 'done' }], time: 2000 }, | |
| { | |
| type: 'tools.update_store', | |
| key: 'todo', | |
| value: [ | |
| { title: 'write tests', status: 'in_progress' }, | |
| { title: 'ship', status: 'pending' }, | |
| { title: 'malformed' }, | |
| ], | |
| time: 3000, | |
| }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.todos).toEqual([ | |
| { | |
| todoId: 'todo', | |
| items: [ | |
| { title: 'write tests', status: 'in_progress' }, | |
| { title: 'ship', status: 'pending' }, | |
| ], | |
| updatedAt: new Date(3000).toISOString(), | |
| }, | |
| ]); | |
| expect(folded.items).toBe(base.items); | |
| }); | |
| it('folds goal create/update/clear into meta.goal with markers, last write wins', () => { | |
| const base = baseWithMarker(); | |
| expect(base.items.some((item) => item.kind === 'marker' && item.markerId === 'm1')).toBe(true); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'goal.create', | |
| goalId: 'g1', | |
| objective: 'fix the bug', | |
| completionCriterion: 'tests pass', | |
| time: 1000, | |
| }, | |
| { type: 'goal.update', status: 'blocked', reason: 'stuck', tokensUsed: 1200, time: 2000 }, | |
| { type: 'goal.update', budgetLimits: { tokenBudget: 50000 }, time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.meta.goal).toEqual({ | |
| objective: 'fix the bug', | |
| status: 'blocked', | |
| completionCriterion: 'tests pass', | |
| budgetUsed: 1200, | |
| budgetLimit: 50000, | |
| }); | |
| const goalMarkers = folded.items.filter( | |
| (item) => item.kind === 'marker' && item.marker === 'goal', | |
| ); | |
| expect(goalMarkers.map((item) => item.kind === 'marker' && item.markerId)).toEqual([ | |
| 'm2', | |
| 'm3', | |
| 'm4', | |
| ]); | |
| expect(goalMarkers[0]).toMatchObject({ at: new Date(1000).toISOString() }); | |
| expect(folded.items.slice(0, base.items.length)).toEqual(base.items); | |
| const cleared = foldWireRecordFacts( | |
| [ | |
| { type: 'goal.create', goalId: 'g1', objective: 'fix the bug', time: 1000 }, | |
| { type: 'goal.clear', time: 2000 }, | |
| ], | |
| base, | |
| ); | |
| expect(cleared.meta.goal).toBeUndefined(); | |
| }); | |
| it('marks user-cancelled turns with interruption markers, skipping unattributable cancels', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.cancel', turnId: 0, target: 'active', reason: 'user_cancelled', time: 1000 }, | |
| { type: 'turn.cancel', turnId: 0, target: 'active', reason: 'user_cancelled', time: 1500 }, | |
| { type: 'turn.cancel', turnId: 1, target: 'queued', reason: 'user_cancelled', time: 2000 }, | |
| { type: 'turn.cancel', turnId: 2, target: 'active', reason: 'aborted', time: 3000 }, | |
| { type: 'turn.cancel', turnId: 4, target: 'active', reason: 'aborted', time: 3500 }, | |
| { type: 'turn.cancel', turnId: 4, target: 'active', reason: 'user_cancelled', time: 3600 }, | |
| { type: 'turn.cancel', turnId: 3, target: 'active', time: 4000 }, | |
| ], | |
| base, | |
| ); | |
| expect( | |
| folded.items.filter((item) => item.kind === 'marker' && item.marker === 'interruption'), | |
| ).toEqual([ | |
| { | |
| kind: 'marker', | |
| markerId: 'm2', | |
| marker: 'interruption', | |
| payload: { turnId: 0, target: 'active', reason: 'user_cancelled' }, | |
| at: new Date(1000).toISOString(), | |
| }, | |
| ]); | |
| }); | |
| it('folds plan/swarm mode records into meta.modes with enter/exit markers', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, | |
| { type: 'swarm_mode.enter', trigger: { kind: 'task' }, time: 2000 }, | |
| { type: 'plan_mode.exit', id: 'plan-1', time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.meta.modes).toEqual({ swarm: {} }); | |
| const markers = folded.items | |
| .filter((item) => item.kind === 'marker') | |
| .map((item) => item.kind === 'marker' && item.marker); | |
| expect(markers).toEqual(['compaction', 'plan.enter', 'swarm.enter', 'plan.exit']); | |
| const cancelled = foldWireRecordFacts( | |
| [ | |
| { type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, | |
| { type: 'plan_mode.cancel', time: 2000 }, | |
| { type: 'swarm_mode.enter', trigger: { kind: 'tool' }, time: 3000 }, | |
| { type: 'swarm_mode.exit', time: 4000 }, | |
| ], | |
| base, | |
| ); | |
| expect(cancelled.meta.modes).toEqual({}); | |
| const stillPlanning = foldWireRecordFacts( | |
| [{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 }], | |
| base, | |
| ); | |
| expect(stillPlanning.meta.modes).toEqual({ plan: {} }); | |
| }); | |
| it('folds tower mode records into meta.modes without markers', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'tower_mode.enter', time: 1000 }, | |
| { type: 'tower_mode.exit', time: 2000 }, | |
| { type: 'tower_mode.enter', time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.meta.modes).toEqual({ tower: {} }); | |
| const exited = foldWireRecordFacts( | |
| [ | |
| { type: 'tower_mode.enter', time: 1000 }, | |
| { type: 'tower_mode.exit', time: 2000 }, | |
| ], | |
| base, | |
| ); | |
| expect(exited.meta.modes).toEqual({}); | |
| }); | |
| it('folds plan.revision records into the plan badge and a timeline marker', () => { | |
| const base = baseWithMarker(); | |
| const revision = { | |
| type: 'plan.revision', | |
| id: 'plan-1', | |
| version: 2, | |
| key: 'plan/plan-1/v2.md', | |
| sha256: 'deadbeef', | |
| bytes: 512, | |
| time: 2000, | |
| }; | |
| const folded = foldWireRecordFacts( | |
| [{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, revision], | |
| base, | |
| { resolvePlanRevisionKey: (key) => `sessions/w/s/agents/main/${key}` }, | |
| ); | |
| expect(folded.meta.modes).toEqual({ | |
| plan: { reviewPath: 'sessions/w/s/agents/main/plan/plan-1/v2.md', version: 2 }, | |
| }); | |
| const revisionMarkers = folded.items.filter( | |
| (item) => item.kind === 'marker' && item.marker === 'plan.revision', | |
| ); | |
| expect(revisionMarkers).toEqual([ | |
| { | |
| kind: 'marker', | |
| markerId: 'm3', | |
| marker: 'plan.revision', | |
| payload: { | |
| id: 'plan-1', | |
| version: 2, | |
| path: 'sessions/w/s/agents/main/plan/plan-1/v2.md', | |
| sha256: 'deadbeef', | |
| bytes: 512, | |
| }, | |
| at: new Date(2000).toISOString(), | |
| }, | |
| ]); | |
| const exited = foldWireRecordFacts( | |
| [ | |
| { type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, | |
| revision, | |
| { type: 'plan_mode.exit', id: 'plan-1', time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| expect(exited.meta.modes?.plan).toBeUndefined(); | |
| expect( | |
| exited.items.filter((item) => item.kind === 'marker' && item.marker === 'plan.revision'), | |
| ).toHaveLength(1); | |
| const reentered = foldWireRecordFacts( | |
| [ | |
| { type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, | |
| revision, | |
| { type: 'plan_mode.exit', id: 'plan-1', time: 3000 }, | |
| { type: 'plan_mode.enter', id: 'plan-2', time: 4000 }, | |
| ], | |
| base, | |
| ); | |
| expect(reentered.meta.modes).toEqual({ plan: {} }); | |
| }); | |
| it('folds legacy plan.revision path records without a key', () => { | |
| const base = baseWithMarker(); | |
| const legacy = { | |
| type: 'plan.revision', | |
| id: 'plan-1', | |
| version: 1, | |
| path: 'sessions/w/s/agents/main/plan/plan-1/v1.md', | |
| sha256: 'deadbeef', | |
| bytes: 256, | |
| time: 2000, | |
| }; | |
| const folded = foldWireRecordFacts( | |
| [{ type: 'plan_mode.enter', id: 'plan-1', time: 1000 }, legacy], | |
| base, | |
| ); | |
| expect(folded.meta.modes).toEqual({ | |
| plan: { reviewPath: 'sessions/w/s/agents/main/plan/plan-1/v1.md', version: 1 }, | |
| }); | |
| const revisionMarkers = folded.items.filter( | |
| (item) => item.kind === 'marker' && item.marker === 'plan.revision', | |
| ); | |
| expect(revisionMarkers).toEqual([ | |
| { | |
| kind: 'marker', | |
| markerId: 'm3', | |
| marker: 'plan.revision', | |
| payload: { | |
| id: 'plan-1', | |
| version: 1, | |
| path: 'sessions/w/s/agents/main/plan/plan-1/v1.md', | |
| sha256: 'deadbeef', | |
| bytes: 256, | |
| }, | |
| at: new Date(2000).toISOString(), | |
| }, | |
| ]); | |
| }); | |
| it('folds task records into task entities and timeline taskrefs', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'task.started', | |
| info: { | |
| taskId: 'task_1', | |
| kind: 'process', | |
| description: 'pnpm test', | |
| status: 'running', | |
| startedAt: 1000, | |
| endedAt: null, | |
| }, | |
| time: 1000, | |
| }, | |
| { | |
| type: 'task.started', | |
| info: { | |
| taskId: 'task_2', | |
| kind: 'agent', | |
| description: 'scan the repo', | |
| status: 'running', | |
| detached: false, | |
| agentId: 'sub-1', | |
| startedAt: 2000, | |
| endedAt: null, | |
| }, | |
| time: 2000, | |
| }, | |
| { | |
| type: 'task.terminated', | |
| info: { | |
| taskId: 'task_1', | |
| kind: 'process', | |
| description: 'pnpm test', | |
| status: 'completed', | |
| startedAt: 1000, | |
| endedAt: 5000, | |
| }, | |
| outputTail: '42 passed', | |
| time: 5000, | |
| }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.tasks).toEqual([ | |
| { | |
| taskId: 'task_1', | |
| kind: 'shell', | |
| state: 'completed', | |
| detached: true, | |
| description: 'pnpm test', | |
| agentId: undefined, | |
| outputTail: '42 passed', | |
| startedAt: new Date(1000).toISOString(), | |
| endedAt: new Date(5000).toISOString(), | |
| }, | |
| { | |
| taskId: 'task_2', | |
| kind: 'subagent', | |
| state: 'running', | |
| detached: false, | |
| description: 'scan the repo', | |
| agentId: 'sub-1', | |
| outputTail: '', | |
| startedAt: new Date(2000).toISOString(), | |
| endedAt: undefined, | |
| }, | |
| ]); | |
| const refs = folded.items.filter((item) => item.kind === 'taskref'); | |
| expect(refs).toEqual([ | |
| { kind: 'taskref', refId: 'ref-task_1', taskId: 'task_1', at: new Date(1000).toISOString() }, | |
| { kind: 'taskref', refId: 'ref-task_2', taskId: 'task_2', at: new Date(2000).toISOString() }, | |
| ]); | |
| }); | |
| it('maps unknown task kinds to other and survives malformed task records', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'task.started', | |
| info: { taskId: 'task_q', kind: 'question', status: 'running', startedAt: 1000, endedAt: null }, | |
| time: 1000, | |
| }, | |
| { type: 'task.started', time: 2000 }, | |
| { type: 'task.terminated', info: { kind: 'process' }, time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.tasks).toHaveLength(1); | |
| expect(folded.tasks[0]).toMatchObject({ taskId: 'task_q', kind: 'other', state: 'running' }); | |
| }); | |
| it('folds interaction request/resolved into entities with terminal states', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'interaction.request', | |
| id: 'apr-1', | |
| kind: 'approval', | |
| toolCallId: 'call_1', | |
| request: { toolName: 'Bash' }, | |
| time: 1000, | |
| }, | |
| { | |
| type: 'interaction.request', | |
| id: 'q-1', | |
| kind: 'question', | |
| request: { questions: [] }, | |
| time: 2000, | |
| }, | |
| { | |
| type: 'interaction.resolved', | |
| id: 'apr-1', | |
| response: { decision: 'approved', scope: 'session' }, | |
| time: 3000, | |
| }, | |
| { type: 'interaction.resolved', id: 'q-1', response: null, time: 4000 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.interactions).toEqual([ | |
| { | |
| interactionId: 'apr-1', | |
| interactionKind: 'approval', | |
| toolCallId: 'call_1', | |
| state: 'approved', | |
| request: { toolName: 'Bash' }, | |
| response: { decision: 'approved', scope: 'session' }, | |
| }, | |
| { | |
| interactionId: 'q-1', | |
| interactionKind: 'question', | |
| toolCallId: undefined, | |
| state: 'dismissed', | |
| request: { questions: [] }, | |
| response: null, | |
| }, | |
| ]); | |
| }); | |
| it('cancels interactions still pending at the end of the scan (crash == cancelled)', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'interaction.request', | |
| id: 'apr-9', | |
| kind: 'approval', | |
| request: { toolCallId: 'call_9', toolName: 'Write' }, | |
| time: 1000, | |
| }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.interactions).toHaveLength(1); | |
| expect(folded.interactions[0]).toMatchObject({ | |
| interactionId: 'apr-9', | |
| state: 'cancelled', | |
| toolCallId: 'call_9', | |
| }); | |
| expect(folded.interactions.every((entity) => entity.state !== 'pending')).toBe(true); | |
| }); | |
| it('skips user_tool interactions like the live path', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'interaction.request', id: 'ut-1', kind: 'user_tool', request: {}, time: 1000 }, | |
| { type: 'interaction.resolved', id: 'ut-1', response: {}, time: 2000 }, | |
| ] satisfies HistoryWireRecord[], | |
| base, | |
| ); | |
| expect(folded.interactions).toEqual([]); | |
| }); | |
| it('folds turn.ended records into the matching turn items', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'turn.ended', | |
| turnId: 0, | |
| reason: 'failed', | |
| error: { code: 'provider.overloaded', message: 'Overloaded', name: 'APIStatusError', retryable: true }, | |
| durationMs: 1234, | |
| time: 5000, | |
| }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.state).toBe('failed'); | |
| expect(turn.error).toBe('Overloaded'); | |
| expect(turn.durationMs).toBe(1234); | |
| expect(turn.endedAt).toBe(new Date(5000).toISOString()); | |
| expect(folded.items).toHaveLength(base.items.length); | |
| }); | |
| it('applies turn.ended records last-wins per turn and folds blocked into failed', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'boom' }, time: 1000 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'cancelled', durationMs: 10, time: 2000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.state).toBe('cancelled'); | |
| expect(turn.error).toBeUndefined(); | |
| expect(turn.durationMs).toBe(10); | |
| expect(turn.endedAt).toBe(new Date(2000).toISOString()); | |
| const blocked = foldWireRecordFacts( | |
| [{ type: 'turn.ended', turnId: 0, reason: 'blocked', time: 3000 }], | |
| base, | |
| ); | |
| const blockedTurn = blocked.items[0]; | |
| if (blockedTurn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(blockedTurn.state).toBe('failed'); | |
| }); | |
| it('ignores turn.ended records with unknown turn ids or malformed payloads', () => { | |
| const base = baseWithMarker(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.ended', turnId: 9, reason: 'cancelled', time: 1000 }, | |
| { type: 'turn.ended', reason: 'completed', time: 2000 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'not-a-reason', time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.state).toBe('completed'); | |
| expect(turn.endedAt).toBe(new Date(3000).toISOString()); | |
| expect(folded.items).toHaveLength(base.items.length); | |
| }); | |
| const baseWithSteps = (): AgentTranscriptSnapshot => | |
| groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'first' }], toolCalls: [] }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'second' }], toolCalls: [] }, | |
| ]); | |
| it('folds turn.step.interrupted records into the matching step of the matching turn', () => { | |
| const base = baseWithSteps(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'turn.step.interrupted', | |
| turnId: 0, | |
| step: 2, | |
| reason: 'user_cancelled', | |
| message: 'stopped by user', | |
| time: 4000, | |
| }, | |
| { type: 'turn.ended', turnId: 0, reason: 'cancelled', time: 5000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(2); | |
| expect(turn.steps[0]).toMatchObject({ ordinal: 1, state: 'completed' }); | |
| expect(turn.steps[0]?.endReason).toBeUndefined(); | |
| expect(turn.steps[1]).toMatchObject({ | |
| ordinal: 2, | |
| state: 'interrupted', | |
| endReason: 'user_cancelled', | |
| endMessage: 'stopped by user', | |
| endedAt: new Date(4000).toISOString(), | |
| }); | |
| expect(turn.state).toBe('cancelled'); | |
| expect(folded.items).toHaveLength(base.items.length); | |
| }); | |
| it('applies turn.step.interrupted last-wins per step and tolerates a missing message', () => { | |
| const base = baseWithSteps(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.step.interrupted', turnId: 0, step: 1, reason: 'aborted', message: 'first', time: 1000 }, | |
| { type: 'turn.step.interrupted', turnId: 0, step: 1, reason: 'max_steps', time: 2000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps[0]).toMatchObject({ | |
| ordinal: 1, | |
| state: 'interrupted', | |
| endReason: 'max_steps', | |
| endedAt: new Date(2000).toISOString(), | |
| }); | |
| expect(turn.steps[0]?.endMessage).toBeUndefined(); | |
| expect(turn.steps[1]?.state).toBe('completed'); | |
| }); | |
| it('skips turn.step.interrupted records with unknown turn ids or missing reason', () => { | |
| const base = baseWithSteps(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.step.interrupted', turnId: 9, step: 1, reason: 'error', time: 1000 }, | |
| { type: 'turn.step.interrupted', turnId: 0, step: 1, time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps.map((step) => step.state)).toEqual(['completed', 'completed']); | |
| expect(turn.steps.every((step) => step.endReason === undefined)).toBe(true); | |
| }); | |
| it('creates the interrupted step when the cold tree has no step with that ordinal', () => { | |
| const base = baseWithSteps(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'turn.step.interrupted', | |
| turnId: 0, | |
| step: 3, | |
| reason: 'error', | |
| message: 'all retries exhausted', | |
| time: 2000, | |
| }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', time: 3000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps.map((step) => step.ordinal)).toEqual([1, 2, 3]); | |
| expect(turn.steps[2]).toEqual({ | |
| kind: 'step', | |
| stepId: 't0.3', | |
| turnId: 't0', | |
| ordinal: 3, | |
| state: 'interrupted', | |
| frames: [], | |
| endedAt: new Date(2000).toISOString(), | |
| endReason: 'error', | |
| endMessage: 'all retries exhausted', | |
| }); | |
| }); | |
| it('creates the interrupted step for a turn that produced no steps at all', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { role: 'user', content: [{ type: 'text', text: 'hi' }], toolCalls: [], origin: { kind: 'user' } }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.step.interrupted', turnId: 0, step: 1, reason: 'user_cancelled', time: 1000 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'cancelled', time: 2000 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.steps).toHaveLength(1); | |
| expect(turn.steps[0]).toMatchObject({ | |
| stepId: 't0.1', | |
| ordinal: 1, | |
| state: 'interrupted', | |
| endReason: 'user_cancelled', | |
| endedAt: new Date(1000).toISOString(), | |
| }); | |
| expect(turn.steps[0]?.endMessage).toBeUndefined(); | |
| }); | |
| it('ignores turn.step.retrying records in the cold fold', () => { | |
| const base = baseWithSteps(); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { | |
| type: 'turn.step.retrying', | |
| turnId: 0, | |
| step: 1, | |
| failedAttempt: 1, | |
| nextAttempt: 2, | |
| maxAttempts: 10, | |
| delayMs: 500, | |
| errorName: 'APIStatusError', | |
| errorMessage: 'Overloaded', | |
| statusCode: 429, | |
| time: 1000, | |
| }, | |
| ], | |
| base, | |
| ); | |
| expect(folded).toEqual(base); | |
| expect(folded.items).toBe(base.items); | |
| }); | |
| it('matches durable turns by prompt identity after a context-only blocked prompt', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-blocked', role: 'user', content: [{ type: 'text', text: 'blocked' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { id: 'prompt-live', role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'failed later' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', turnId: 2, input: [{ type: 'text', text: 'run' }], origin: { kind: 'user' }, promptId: 'prompt-live', time: 1 }, | |
| { type: 'turn.ended', turnId: 2, reason: 'failed', error: { message: 'later failure' }, time: 2 }, | |
| ], | |
| base, | |
| ); | |
| expect(folded.items).toHaveLength(2); | |
| const blocked = folded.items[0]; | |
| const live = folded.items[1]; | |
| if (blocked?.kind !== 'turn' || live?.kind !== 'turn') throw new Error('expected turns'); | |
| expect(blocked.triggerPromptId).toBe('prompt-blocked'); | |
| expect(blocked.error).toBeUndefined(); | |
| expect(live.triggerPromptId).toBe('prompt-live'); | |
| expect(live.state).toBe('failed'); | |
| expect(live.error).toBe('later failure'); | |
| }); | |
| it('recovers legacy boundary identity past a context-only blocked prompt', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-blocked', role: 'user', content: [{ type: 'text', text: 'blocked' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { id: 'prompt-real', role: 'user', content: [{ type: 'text', text: 'run' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'failed later' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'context.append_message', message: { id: 'prompt-blocked', role: 'user', origin: { kind: 'user' } }, time: 1 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'run' }], origin: { kind: 'user' }, time: 2 }, | |
| { type: 'context.append_message', message: { id: 'prompt-real', role: 'user', origin: { kind: 'user' } }, time: 3 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'legacy failure' }, time: 4 }, | |
| ], | |
| base, | |
| ); | |
| const blocked = folded.items[0]; | |
| const real = folded.items[1]; | |
| if (blocked?.kind !== 'turn' || real?.kind !== 'turn') throw new Error('expected turns'); | |
| expect(blocked.error).toBeUndefined(); | |
| expect(real.triggerPromptId).toBe('prompt-real'); | |
| expect(real.state).toBe('failed'); | |
| expect(real.error).toBe('legacy failure'); | |
| }); | |
| it('retires an unmatched legacy empty boundary before a later blocked prompt', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { role: 'assistant', content: [{ type: 'text', text: 'partial' }], toolCalls: [] }, | |
| { id: 'prompt-blocked', role: 'user', content: [{ type: 'text', text: 'blocked' }], toolCalls: [], origin: { kind: 'user' } }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [], origin: { kind: 'user' }, time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'empty failure' }, time: 2 }, | |
| { type: 'context.append_message', message: { id: 'prompt-blocked', role: 'user', origin: { kind: 'user' } }, time: 3 }, | |
| ], | |
| base, | |
| ); | |
| const empty = folded.items[0]; | |
| const blocked = folded.items[1]; | |
| if (empty?.kind !== 'turn' || blocked?.kind !== 'turn') throw new Error('expected turns'); | |
| expect(empty.state).toBe('failed'); | |
| expect(empty.error).toBe('empty failure'); | |
| expect(blocked.triggerPromptId).toBe('prompt-blocked'); | |
| expect(blocked.error).toBeUndefined(); | |
| }); | |
| it('folds terminal facts for an empty prompt turn without a context identity', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { role: 'assistant', content: [{ type: 'text', text: 'partial' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [], origin: { kind: 'user' }, promptId: 'prompt-empty', time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'empty failure' }, time: 2 }, | |
| ], | |
| base, | |
| ); | |
| const turn = folded.items[0]; | |
| if (turn?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(turn.triggerPromptId).toBeUndefined(); | |
| expect(turn.state).toBe('failed'); | |
| expect(turn.error).toBe('empty failure'); | |
| }); | |
| it('does not let an empty prompt claim a later system continuation turn', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'internal-prompt', role: 'user', content: [{ type: 'text', text: 'continue' }], toolCalls: [], origin: { kind: 'system_trigger', name: 'goal_continuation' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'stopped' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [], origin: { kind: 'user' }, promptId: 'prompt-empty', time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'empty failure' }, time: 2 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'continue' }], origin: { kind: 'system_trigger', name: 'goal_continuation' }, promptId: 'internal-prompt', time: 3 }, | |
| { type: 'turn.ended', turnId: 1, reason: 'cancelled', durationMs: 10, time: 4 }, | |
| ], | |
| base, | |
| ); | |
| const system = folded.items[0]; | |
| if (system?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(system.triggerPromptId).toBeUndefined(); | |
| expect(system.state).toBe('cancelled'); | |
| expect(system.durationMs).toBe(10); | |
| expect(system.error).toBeUndefined(); | |
| }); | |
| it('matches a system turn with internal prompt identity past a context-only blocked prompt', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-blocked', role: 'user', content: [{ type: 'text', text: 'blocked' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { id: 'internal-prompt', role: 'user', content: [{ type: 'text', text: 'continue' }], toolCalls: [], origin: { kind: 'system_trigger', name: 'goal_continuation' } as { kind: string } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'continuation failed' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'continue' }], origin: { kind: 'system_trigger', name: 'goal_continuation' }, promptId: 'internal-prompt', time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'system failure' }, time: 2 }, | |
| ], | |
| base, | |
| ); | |
| const blocked = folded.items[0]; | |
| const system = folded.items[1]; | |
| if (blocked?.kind !== 'turn' || system?.kind !== 'turn') throw new Error('expected turns'); | |
| expect(blocked.triggerPromptId).toBe('prompt-blocked'); | |
| expect(blocked.error).toBeUndefined(); | |
| expect(system.triggerPromptId).toBeUndefined(); | |
| expect(system.state).toBe('failed'); | |
| expect(system.error).toBe('system failure'); | |
| }); | |
| it('maps turn.ended around hidden retry turns replayed from the turn-clock records', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-1', role: 'user', content: [{ type: 'text', text: 'one' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a1' }], toolCalls: [] }, | |
| { id: 'prompt-2', role: 'user', content: [{ type: 'text', text: 'two' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a2' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'one' }], origin: { kind: 'user' }, promptId: 'prompt-1', time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'completed', time: 2 }, | |
| { type: 'turn.prompt', input: [], origin: { kind: 'retry' }, time: 3 }, | |
| { type: 'turn.ended', turnId: 1, reason: 'failed', error: { message: 'retry boom' }, time: 4 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'two' }], origin: { kind: 'user' }, promptId: 'prompt-2', time: 5 }, | |
| { type: 'turn.ended', turnId: 2, reason: 'cancelled', durationMs: 20, time: 6 }, | |
| ], | |
| base, | |
| ); | |
| const first = folded.items[0]; | |
| if (first?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(first.state).toBe('completed'); | |
| expect(first.triggerPromptId).toBe('prompt-1'); | |
| const second = folded.items[1]; | |
| if (second?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(second.state).toBe('cancelled'); | |
| expect(second.triggerPromptId).toBe('prompt-2'); | |
| expect(second.error).toBeUndefined(); | |
| expect(second.durationMs).toBe(20); | |
| }); | |
| it('maps turn.ended across queued-then-cancelled turn reservations', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-1', role: 'user', content: [{ type: 'text', text: 'one' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a1' }], toolCalls: [] }, | |
| { id: 'prompt-2', role: 'user', content: [{ type: 'text', text: 'two' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a2' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'one' }], origin: { kind: 'user' }, time: 1 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'completed', time: 2 }, | |
| { type: 'turn.cancel', turnId: 1, target: 'queued', time: 3 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'two' }], origin: { kind: 'user' }, promptId: 'prompt-2', time: 4 }, | |
| { type: 'turn.ended', turnId: 2, reason: 'failed', error: { message: 'boom' }, time: 5 }, | |
| ], | |
| base, | |
| ); | |
| const second = folded.items[1]; | |
| if (second?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(second.state).toBe('failed'); | |
| expect(second.error).toBe('boom'); | |
| expect(second.triggerPromptId).toBe('prompt-2'); | |
| }); | |
| it('skips undone turn boundaries when mapping prompt ids and turn endings', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-1', role: 'user', content: [{ type: 'text', text: 'one' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a1' }], toolCalls: [] }, | |
| { id: 'prompt-3', role: 'user', content: [{ type: 'text', text: 'replacement' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'a3' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'one' }], origin: { kind: 'user' }, promptId: 'prompt-1', time: 1 }, | |
| { type: 'context.append_message', message: { id: 'prompt-1', role: 'user', origin: { kind: 'user' } }, time: 1.5 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'completed', time: 2 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'undone' }], origin: { kind: 'user' }, promptId: 'prompt-2', time: 3 }, | |
| { type: 'context.append_message', message: { id: 'prompt-2', role: 'user', origin: { kind: 'user' } }, time: 3.5 }, | |
| { type: 'turn.ended', turnId: 1, reason: 'completed', time: 4 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'continue' }], origin: { kind: 'system_trigger', name: 'goal_continuation' }, time: 5 }, | |
| { type: 'turn.ended', turnId: 2, reason: 'completed', time: 6 }, | |
| { type: 'context.undo', count: 1, time: 7 }, | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'replacement' }], origin: { kind: 'user' }, promptId: 'prompt-3', time: 8 }, | |
| { type: 'turn.ended', turnId: 3, reason: 'failed', error: { message: 'replacement failed' }, time: 9 }, | |
| ], | |
| base, | |
| ); | |
| const replacement = folded.items[1]; | |
| if (replacement?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(replacement.triggerPromptId).toBe('prompt-3'); | |
| expect(replacement.state).toBe('failed'); | |
| expect(replacement.error).toBe('replacement failed'); | |
| }); | |
| it('counts context-only blocked prompts as undo anchors', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-real', role: 'user', content: [{ type: 'text', text: 'real' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'failed' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'real' }], origin: { kind: 'user' }, promptId: 'prompt-real', time: 1 }, | |
| { type: 'context.append_message', message: { id: 'prompt-real', role: 'user', origin: { kind: 'user' } }, time: 2 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'real failure' }, time: 3 }, | |
| { type: 'context.append_message', message: { id: 'prompt-blocked', role: 'user', origin: { kind: 'user' } }, time: 4 }, | |
| { type: 'context.undo', count: 1, time: 5 }, | |
| ], | |
| base, | |
| ); | |
| const real = folded.items[0]; | |
| if (real?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(real.triggerPromptId).toBe('prompt-real'); | |
| expect(real.state).toBe('failed'); | |
| expect(real.error).toBe('real failure'); | |
| }); | |
| it('does not consume a pending identity boundary for a different context-only anchor', () => { | |
| const base = groupMessagesIntoSnapshot([ | |
| { id: 'prompt-real', role: 'user', content: [{ type: 'text', text: 'real' }], toolCalls: [], origin: { kind: 'user' } }, | |
| { role: 'assistant', content: [{ type: 'text', text: 'failed' }], toolCalls: [] }, | |
| ]); | |
| const folded = foldWireRecordFacts( | |
| [ | |
| { type: 'turn.prompt', input: [{ type: 'text', text: 'real' }], origin: { kind: 'user' }, promptId: 'prompt-real', time: 1 }, | |
| { type: 'context.append_message', message: { id: 'prompt-blocked', role: 'user', origin: { kind: 'user' } }, time: 2 }, | |
| { type: 'context.undo', count: 1, time: 3 }, | |
| { type: 'context.append_message', message: { id: 'prompt-real', role: 'user', origin: { kind: 'user' } }, time: 4 }, | |
| { type: 'turn.ended', turnId: 0, reason: 'failed', error: { message: 'real failure' }, time: 5 }, | |
| ], | |
| base, | |
| ); | |
| const real = folded.items[0]; | |
| if (real?.kind !== 'turn') throw new Error('expected turn'); | |
| expect(real.state).toBe('failed'); | |
| expect(real.error).toBe('real failure'); | |
| }); | |
| }); | |