| 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'); |
| }); |
| }); |
|
|