import { daemonFileRefFromPart, readTodoItems, type AgentTaskInfo, type ContentPart, type TokenUsage, } from '@moonshot-ai/agent-core-v2'; import type { AssistantMessage, ContentPart as WireContentPart, InteractionMessage, ServerMessage, StepMessage, StepRetry, StepTiming, StepUsage, SystemMessage, TaskMessage, TaskNotificationPayload, ThinkingMessage, TodoMessage, ToolCallAgentRef, ToolCallMessage, ToolProgressPayload, TurnMessage, TurnOrigin, UserMessage, UserMessageOrigin, } from '../../protocol/messages'; import { PROJECTION_IGNORED_EVENT_TYPES, type ProjectionBusEvent } from './events'; import type { WireTurnFold } from './heal'; import { SystemIdAllocator, TODO_ENTITY_ID, attachmentIdOf, isCompactionSystemId, isUndoAnchorOrigin, stepIdOf, textMessageIdOf, turnIdOf, turnOrdinalOf, turnUserMessageIdOf, } from './ids'; const TASK_OUTPUT_TAIL_MAX = 8192; const PENDING_CLEAR_SETTLE_MS = 100; export interface ProjectorInteraction { readonly id: string; readonly kind: 'approval' | 'question'; readonly payload: unknown; readonly createdAt: number; } export interface ProjectorLookups { readonly stepOrdinal?: (turnId: string) => number | undefined; readonly resolvePlanRevisionKey?: (key: string) => string; } export interface ProjectorHooks { readonly onUnknownEvent?: (type: string) => void; readonly onDeferred?: (messages: ServerMessage[]) => void; } interface TurnRecord { turnId: string; ordinal: number; status: 'running' | 'completed'; origin: TurnOrigin; anchor: boolean; promptId?: string; userMessageId?: string; attachmentIds?: string[]; openingKey?: { text: string; attachments: number }; openingSteerDeduped: boolean; startedAt?: string; endedAt?: string; durationMs?: number; usage?: StepUsage; } interface StepRecord { stepId: string; turnId: string; ordinal: number; status: 'running' | 'completed' | 'interrupted' | 'failed'; startedAt?: string; endedAt?: string; usage?: StepUsage; finishReason?: string; timing?: StepTiming; retry?: StepRetry; endReason?: string; endMessage?: string; } interface TextRecord { messageId: string; kind: 'assistant' | 'thinking'; turnId: string; stepId: string; status: 'streaming' | 'completed'; text: string; } interface ToolRecord { toolCallId: string; turnId: string; stepId: string; name: string; status: 'running' | 'done' | 'error'; input?: unknown; inputText?: string; output?: unknown; display?: unknown; error?: string; progress?: ToolProgressPayload; taskId?: string; approvalId?: string; todoId?: string; agentRefs: ToolCallAgentRef[]; startedAt?: string; } interface TaskRecord { taskId: string; kind: TaskMessage['kind']; status: TaskMessage['status']; detached: boolean; description?: string; childAgentId?: string; outputTail: string; startedAt?: string; endedAt?: string; resultSummary?: string; error?: string; stateReason?: string; usage?: StepUsage; model?: string; thinkingEffort?: string; } interface InteractionRecord { interactionId: string; kind: 'approval' | 'question'; status: InteractionMessage['status']; toolCallId?: string; request?: unknown; response?: unknown; } interface UserRecord { messageId: string; turnId?: string; text: WireContentPart[]; status: 'unread' | 'read'; timestamp?: number; origin?: UserMessageOrigin; attachmentIds?: string[]; skillActivations?: { skill_name: string; skill_args?: string }[]; } interface PromptRecord { promptId: string; userMessageId: string; content: readonly ContentPart[]; status: 'running' | 'queued' | 'steered' | 'completed' | 'aborted'; createdAt: string; } export class AgentMessageProjector { private currentTurn: TurnRecord | undefined; private currentStep: StepRecord | undefined; private openText: TextRecord | undefined; private openThinking: TextRecord | undefined; private serverUserSeq = 0; private attachmentSeq = 0; private phantomUserSeq = 0; private readonly turns = new Map(); private readonly steps = new Map(); private readonly texts = new Map(); private readonly stepTextIds = new Map(); private readonly stepTextSeqs = new Map(); private readonly tools = new Map(); private readonly tasks = new Map(); private readonly shellTasks = new Map(); private readonly interactions = new Map(); private readonly users = new Map(); private readonly prompts = new Map(); private readonly stepOrdinals = new Map(); private readonly stepUsageByTurn = new Map(); private mergedSteers: { text: string; promptIds: string[] }[] = []; private pendingFullCut = false; private pendingClearTimer: NodeJS.Timeout | undefined; private todoItems: { title: string; status: 'pending' | 'in_progress' | 'done' }[] | undefined; private todoUpdatedAt: string | undefined; private planMode = false; private swarmMode = false; private readonly timelineIds: string[] = []; private readonly sysIds = new SystemIdAllocator(); private readonly endedTurnOrdinals: number[] = []; private readonly anchorTurnOrdinals = new Set(); private timelineRewriteCount = 0; private nextTurnIdHint = 0; constructor( readonly agentId: string, private readonly sessionId: string, private readonly subagentTaskIds: Map, private readonly lookups?: ProjectorLookups, private readonly hooks?: ProjectorHooks, ) {} map(event: ProjectionBusEvent): ServerMessage[] { switch (event.type) { case 'plan.revision': return this.onPlanRevision(event); case 'turn.started': return this.onTurnStarted(event); case 'turn.ended': return this.onTurnEnded(event); case 'turn.step.started': return this.onStepStarted(event); case 'turn.step.completed': return this.onStepCompleted(event); case 'turn.step.interrupted': return this.onStepInterrupted(event); case 'turn.step.retrying': return this.onStepRetrying(event); case 'assistant.delta': return this.onTextDelta(event, 'assistant'); case 'thinking.delta': return this.onTextDelta(event, 'thinking'); case 'tool.call.delta': return this.onToolCallDelta(event); case 'tool.progress': return this.onToolProgress(event); case 'tool.call.started': return this.onToolCallStarted(event); case 'tool.result': return this.onToolResult(event); case 'task.started': case 'task.terminated': return this.onTaskLifecycle(event); case 'shell.started': return this.onShellStarted(event); case 'shell.output': return this.onShellOutput(event); case 'shell.completed': return this.onShellCompleted(event); case 'subagent.spawned': return this.onSubagentSpawned(event); case 'subagent.completed': case 'subagent.failed': case 'subagent.cancelled': case 'subagent.suspended': return this.onSubagentRun(event); case 'goal.updated': return this.onGoalUpdated(event); case 'agent.status.updated': return this.onAgentStatusUpdated(event); case 'prompt.submitted': return this.onPromptSubmitted(event); case 'prompt.queued': return this.onPromptQueued(event); case 'prompt.started': return this.onPromptStarted(event); case 'prompt.completed': return this.onPromptCompleted(event); case 'prompt.aborted': return this.onPromptAborted(event); case 'prompt.steered': return this.onPromptSteered(event); case 'turn.steer': return this.onTurnSteered(event); case 'hook.result': return [this.systemOp('hook', hookPayload(event), event.time)]; case 'skill.activated': case 'plugin_command.activated': return []; case 'compaction.started': case 'compaction.blocked': case 'compaction.cancelled': return []; case 'compaction.completed': { const result = event.result; const text = result.summary.length > 0 ? result.summary : result.contextSummary; return [ this.systemOp( 'compaction', { phase: 'completed', text: text !== undefined && text.length > 0 ? text : undefined }, event.time, ), ]; } case 'context.spliced': return this.onContextSpliced(event); case 'context.undone': return this.onContextUndone(event); case 'error': return [ this.systemOp( 'notice', { level: 'error', message: event.message, ...restOf(event) }, event.time, ), ]; case 'warning': return [ this.systemOp( 'notice', { level: 'warning', message: event.message, code: event.code }, event.time, ), ]; case 'cron.fired': case 'permission.approval.requested': case 'permission.approval.resolved': case 'subagent.started': return []; case 'task.notified': return this.onTaskNotified(event); default: { const type = (event as { type: string }).type; if (PROJECTION_IGNORED_EVENT_TYPES.has(type)) return []; this.hooks?.onUnknownEvent?.(type); return []; } } } seedActiveTurn(info: { turnId: number; promptId?: string; userMessageId?: string; origin?: TurnOrigin; anchor?: boolean; }): void { const turnId = turnIdOf(info.turnId); this.noteTurnId(info.turnId); if (info.anchor === true) this.anchorTurnOrdinals.add(info.turnId); this.currentTurn = { turnId, ordinal: info.turnId, status: 'running', origin: info.origin ?? { kind: 'other' }, anchor: info.anchor === true, promptId: info.promptId, userMessageId: info.promptId === undefined ? undefined : (info.userMessageId ?? turnUserMessageIdOf(turnId)), openingSteerDeduped: false, }; this.turns.set(turnId, this.currentTurn); this.timelineIds.push(turnId); } seedTask(info: AgentTaskInfo): ServerMessage[] { if (info.status !== 'running') return []; const agentInfo = agentInfoOf(info); const kind = mapTaskKind(info.kind); const task = this.upsertTask(info.taskId, (prev) => ({ taskId: info.taskId, kind, status: 'running', detached: info.detached ?? prev?.detached ?? kind !== 'shell', description: info.description, childAgentId: agentInfo?.agentId ?? prev?.childAgentId, outputTail: prev?.outputTail ?? '', startedAt: prev?.startedAt ?? epochMsToIso(info.startedAt), model: agentInfo?.model ?? prev?.model, thinkingEffort: agentInfo?.thinkingEffort ?? prev?.thinkingEffort, })); const childAgentId = agentInfo?.agentId; if (info.kind === 'agent' && typeof childAgentId === 'string' && childAgentId.length > 0) { this.subagentTaskIds.set(childAgentId, info.taskId); } return [this.taskOp(task)]; } seedTodo( items: readonly { title: string; status: 'pending' | 'in_progress' | 'done' }[], ): ServerMessage[] { if (items.length === 0) return []; this.todoItems = items.map((item) => ({ title: item.title, status: item.status })); this.todoUpdatedAt = undefined; return [this.todoOp()]; } taskOutputUpdated(taskId: string, outputTail: string): ServerMessage[] { const task = this.tasks.get(taskId); if (task === undefined || task.outputTail === outputTail) return []; task.outputTail = outputTail; return [this.taskOp(task)]; } seedModes(modes: { planMode?: boolean; swarmMode?: boolean }): void { if (modes.planMode !== undefined) this.planMode = modes.planMode; if (modes.swarmMode !== undefined) this.swarmMode = modes.swarmMode; } todoChanged( items: readonly { title: string; status: 'pending' | 'in_progress' | 'done' }[], ): ServerMessage[] { this.todoItems = items.map((item) => ({ title: item.title, status: item.status })); this.todoUpdatedAt = nowIso(); return [this.todoOp()]; } interactionRequested(interaction: ProjectorInteraction): ServerMessage[] { const payload = interaction.payload as Record | null; const toolCallId = typeof payload?.['toolCallId'] === 'string' ? payload['toolCallId'] : undefined; const record: InteractionRecord = { interactionId: interaction.id, kind: interaction.kind, status: 'pending', toolCallId, request: this.wireInteractionRequest(interaction), }; this.interactions.set(interaction.id, record); const ops: ServerMessage[] = [this.interactionOp(record)]; if (toolCallId !== undefined) { const tool = this.tools.get(toolCallId); if (tool !== undefined && tool.approvalId !== interaction.id) { tool.approvalId = interaction.id; ops.push(this.toolOp(tool)); } } return ops; } interactionResolved(id: string, response: unknown): ServerMessage[] { const record = this.interactions.get(id); if (record === undefined) return []; record.status = mapInteractionEndStatus(record.kind, response); record.response = this.wireInteractionResponse(record, response); return [this.interactionOp(record)]; } recoveryMessages(): ServerMessage[] { const ops: ServerMessage[] = []; const turn = this.currentTurn; if (turn !== undefined && turn.status === 'running') { ops.push(this.turnOp(turn)); const step = this.currentStep; const replayStepId = step !== undefined && step.turnId === turn.turnId ? step.stepId : undefined; if (step !== undefined && replayStepId !== undefined) { ops.push(this.stepOp(step)); for (const record of this.texts.values()) { if (record.stepId === replayStepId) ops.push(this.textOp(record)); } } for (const tool of this.tools.values()) { if (tool.turnId !== turn.turnId) continue; if (tool.status === 'running' || tool.stepId === replayStepId) ops.push(this.toolOp(tool)); } } for (const record of this.interactions.values()) { if (record.status === 'pending') ops.push(this.interactionOp(record)); } for (const task of this.tasks.values()) { if (task.status === 'running') ops.push(this.taskOp(task)); } for (const user of this.users.values()) { if (user.status === 'unread') ops.push(this.userOp(user)); } if (this.todoItems !== undefined) ops.push(this.todoOp()); return ops; } notifyContextCleared(): ServerMessage[] { this.cancelPendingClearTimer(); this.pendingFullCut = false; return this.applyClear(); } applyTimelineSeed(seed: { timelineIds: readonly string[]; systemCounts: ReadonlyMap; anchorTurnOrdinals: readonly number[]; nextTurnId: number; }): void { if (this.timelineRewriteCount > 0) return; const existing = new Set(this.timelineIds); this.timelineIds.unshift(...seed.timelineIds.filter((id) => !existing.has(id))); for (const [subtype, count] of seed.systemCounts) this.sysIds.seed(subtype, count); for (const ordinal of seed.anchorTurnOrdinals) this.anchorTurnOrdinals.add(ordinal); this.nextTurnIdHint = Math.max(this.nextTurnIdHint, seed.nextTurnId); } dispose(): void { this.cancelPendingClearTimer(); } private noteTurnId(turnId: number): void { this.nextTurnIdHint = Math.max(this.nextTurnIdHint, turnId + 1); } takeEndedTurnOrdinals(): number[] { return this.endedTurnOrdinals.splice(0); } healTurn(ordinal: number, fold: WireTurnFold): ServerMessage[] { const turnId = turnIdOf(ordinal); const held = this.turns.get(turnId); if (held?.status !== 'completed') return []; const ops: ServerMessage[] = []; const stepOrdinals = new Set([...fold.steps.keys(), ...fold.texts.keys()]); for (const wireTool of fold.tools.values()) stepOrdinals.add(wireTool.step); for (const stepOrdinal of [...stepOrdinals].toSorted((a, b) => a - b)) { const wireStep = fold.steps.get(stepOrdinal); const stepId = stepIdOf(turnId, stepOrdinal); const live = this.steps.get(stepId); if (live === undefined) { const step: StepRecord = { stepId, turnId, ordinal: stepOrdinal, status: wireStep?.status ?? 'interrupted', endedAt: wireStep?.endedAt, usage: wireStep?.usage, finishReason: wireStep?.finishReason, timing: wireStep?.timing, endReason: wireStep?.endReason, endMessage: wireStep?.endMessage, }; this.steps.set(stepId, step); this.stepOrdinals.set(turnId, Math.max(this.stepOrdinals.get(turnId) ?? 0, stepOrdinal)); ops.push(this.stepOp(step)); } else if (live.status === 'running' && wireStep !== undefined) { live.status = wireStep.status; live.endedAt = wireStep.endedAt; live.usage = live.usage ?? wireStep.usage; live.finishReason = live.finishReason ?? wireStep.finishReason; live.timing = live.timing ?? wireStep.timing; live.endReason = live.endReason ?? wireStep.endReason; live.endMessage = live.endMessage ?? wireStep.endMessage; ops.push(this.stepOp(live)); } const wireTexts = fold.texts.get(stepOrdinal); if (wireTexts !== undefined) { ops.push(...this.healStepTexts(stepId, turnId, wireTexts)); } } for (const [toolCallId, wireTool] of fold.tools) { const live = this.tools.get(toolCallId); const stepId = stepIdOf(turnId, wireTool.step); if (live === undefined) { const tool: ToolRecord = { toolCallId, turnId, stepId, name: wireTool.name, status: wireTool.isError === true ? 'error' : 'done', input: parseToolArgs(wireTool.args), inputText: typeof wireTool.args === 'string' ? wireTool.args : undefined, output: wireTool.output, error: wireTool.isError === true && typeof wireTool.output === 'string' ? wireTool.output : undefined, agentRefs: [], }; this.tools.set(toolCallId, tool); ops.push(this.toolOp(tool)); continue; } const liveHasOutcome = live.output !== undefined || live.error !== undefined || live.status !== 'running'; const wireHasOutcome = wireTool.output !== undefined || wireTool.isError === true; if (liveHasOutcome || !wireHasOutcome) continue; live.status = wireTool.isError === true ? 'error' : 'done'; live.output = wireTool.output; live.error = wireTool.isError === true && typeof wireTool.output === 'string' ? wireTool.output : undefined; ops.push(this.toolOp(live)); } this.dropTurnDetails(turnId); return ops; } inFlight(): { turn_id: string; step_id: string } | undefined { const turn = this.currentTurn; const step = this.currentStep; if (turn === undefined || step === undefined) return undefined; if (turn.status !== 'running' || step.turnId !== turn.turnId) return undefined; return { turn_id: turn.turnId, step_id: step.stepId }; } private healStepTexts( stepId: string, turnId: string, wireTexts: { assistant: string; thinking: string; first: 'assistant' | 'thinking' }, ): ServerMessage[] { const ops: ServerMessage[] = []; const kinds: readonly ('assistant' | 'thinking')[] = wireTexts.first === 'thinking' ? ['thinking', 'assistant'] : ['assistant', 'thinking']; for (const kind of kinds) { const wireText = kind === 'assistant' ? wireTexts.assistant : wireTexts.thinking; const liveId = this.stepTextIds.get(stepId)?.[kind]; const live = liveId === undefined ? undefined : this.texts.get(liveId); if (live === undefined) { if (wireText.length === 0) continue; const record = this.createTextRecord(stepId, turnId, kind); record.text = wireText; record.status = 'completed'; ops.push(this.textOp(record)); continue; } if (wireText.length > live.text.length) { live.text = wireText; live.status = 'completed'; ops.push(this.textOp(live)); } } return ops; } private onTurnStarted(event: { time: number; turnId: number; promptId?: string; origin: unknown; prompt?: string; promptAttachments?: readonly unknown[]; }): ServerMessage[] { const ops = this.settlePendingClear(); if (this.currentTurn !== undefined && this.currentTurn.status === 'running') { ops.push(...this.finalizeTurn(this.currentTurn, event.time)); } const turnId = turnIdOf(event.turnId); this.noteTurnId(event.turnId); this.serverUserSeq = this.phantomUserSeq; this.phantomUserSeq = 0; const origin = this.mapTurnOrigin(event.origin); const attachments = event.promptAttachments ?? []; const attachmentIds = attachments.map((_, index) => attachmentIdOf(turnId, index + 1)); this.attachmentSeq = attachmentIds.length; const promptRecord = event.promptId === undefined ? undefined : this.prompts.get(event.promptId); const promptText = event.prompt ?? (promptRecord === undefined ? undefined : promptTextOf(promptRecord.content)); const wantsUser = wantsUserMessage(event.origin, promptText); const anchor = isUndoAnchorOrigin(event.origin); if (anchor) this.anchorTurnOrdinals.add(event.turnId); const turn: TurnRecord = { turnId, ordinal: event.turnId, status: 'running', origin, anchor, promptId: event.promptId, userMessageId: wantsUser ? (promptRecord?.userMessageId ?? turnUserMessageIdOf(turnId)) : undefined, attachmentIds: attachmentIds.length > 0 ? attachmentIds : undefined, openingKey: { text: promptText ?? '', attachments: attachmentIds.length }, openingSteerDeduped: false, startedAt: epochMsToIso(event.time), }; this.currentTurn = turn; this.turns.set(turnId, turn); this.timelineIds.push(turnId); this.currentStep = undefined; this.openText = undefined; this.openThinking = undefined; ops.push(this.turnOp(turn)); if (wantsUser && turn.userMessageId !== undefined) { const user: UserRecord = { messageId: turn.userMessageId, turnId, text: promptRecord === undefined ? textPartsOf(promptText ?? '') : wireContentParts(promptRecord.content), status: 'read', timestamp: event.time, origin: userOriginOf(event.origin), attachmentIds: attachmentIds.length > 0 ? attachmentIds : undefined, skillActivations: skillActivationsOf(event.origin), }; this.users.set(user.messageId, user); ops.push(this.userOp(user)); } return ops; } private onTurnEnded(event: { time: number; turnId: number; reason: 'completed' | 'cancelled' | 'failed' | 'blocked'; durationMs?: number; interruptReason?: string; }): ServerMessage[] { const ops = this.settlePendingClear(); const turnId = turnIdOf(event.turnId); const turn = this.currentTurn?.turnId === turnId ? this.currentTurn : this.turns.get(turnId); if (turn === undefined) return ops; ops.push(...this.finalizeTurn(turn, event.time, event.reason, event.durationMs)); this.currentStep = undefined; if (this.currentTurn?.turnId === turnId) this.currentTurn = undefined; this.endedTurnOrdinals.push(event.turnId); if (event.reason === 'cancelled' && event.interruptReason === 'user_cancelled') { ops.push( this.systemOp( 'interruption', { turn_id: turnId, reason: event.interruptReason }, event.time, ), ); } return ops; } private finalizeTurn( turn: TurnRecord, time: number, reason?: 'completed' | 'cancelled' | 'failed' | 'blocked', durationMs?: number, ): ServerMessage[] { const ops = this.flushOpenTexts(); const turnId = turn.turnId; if (this.currentStep !== undefined && this.currentStep.turnId === turnId) { const step = this.currentStep; if (step.status === 'running') { step.status = reason === 'failed' || reason === 'blocked' ? 'failed' : 'interrupted'; step.endedAt = epochMsToIso(time); ops.push(this.stepOp(step)); } } turn.status = 'completed'; turn.endedAt = epochMsToIso(time); turn.durationMs = durationMs; turn.usage = this.takeTurnUsage(turnId); ops.push(this.turnOp(turn)); return ops; } private takeTurnUsage(turnId: string): StepUsage | undefined { const usages = this.stepUsageByTurn.get(turnId); this.stepUsageByTurn.delete(turnId); if (usages === undefined || usages.length === 0) return undefined; let inputOther = 0; let output = 0; let inputCacheRead = 0; let inputCacheCreation = 0; for (const usage of usages) { inputOther += usage.input_other; output += usage.output; inputCacheRead += usage.input_cache_read; inputCacheCreation += usage.input_cache_creation; } return { input_other: inputOther, output, input_cache_read: inputCacheRead, input_cache_creation: inputCacheCreation, }; } private onStepStarted(event: { time: number; turnId: number; step: number }): ServerMessage[] { const ops = this.settlePendingClear(); const turnId = turnIdOf(event.turnId); if (this.currentStep !== undefined && this.currentStep.status === 'running') { ops.push(...this.flushOpenTexts()); this.currentStep.status = 'completed'; this.currentStep.endedAt = epochMsToIso(event.time); ops.push(this.stepOp(this.currentStep)); } const stepId = stepIdOf(turnId, event.step); this.stepOrdinals.set(turnId, event.step); const step: StepRecord = { stepId, turnId, ordinal: event.step, status: 'running', startedAt: epochMsToIso(event.time), }; this.currentStep = step; this.steps.set(stepId, step); this.openText = undefined; this.openThinking = undefined; ops.push(this.stepOp(step)); return ops; } private onStepCompleted(event: { time: number; turnId: number; step: number; usage?: TokenUsage; finishReason?: string; rawFinishReason?: string; providerFinishReason?: string; llmFirstTokenLatencyMs?: number; llmStreamDurationMs?: number; }): ServerMessage[] { const ops = this.settlePendingClear(); ops.push(...this.flushOpenTexts()); const turnId = turnIdOf(event.turnId); const stepId = stepIdOf(turnId, event.step); const prev = this.currentStep?.stepId === stepId ? this.currentStep : this.steps.get(stepId); const usage = event.usage === undefined ? undefined : toSnakeUsage(event.usage); if (usage !== undefined) { const usages = this.stepUsageByTurn.get(turnId) ?? []; usages.push(usage); this.stepUsageByTurn.set(turnId, usages); } const step: StepRecord = { stepId, turnId, ordinal: event.step, status: 'completed', startedAt: prev?.startedAt, endedAt: epochMsToIso(event.time), usage, finishReason: event.finishReason ?? event.rawFinishReason ?? event.providerFinishReason, timing: timingOf(event), }; this.currentStep = step; this.steps.set(stepId, step); ops.push(this.stepOp(step)); return ops; } private onStepInterrupted(event: { time: number; turnId: number; step: number; reason: string; message?: string; }): ServerMessage[] { const ops = this.settlePendingClear(); ops.push(...this.flushOpenTexts()); const turnId = turnIdOf(event.turnId); const stepId = stepIdOf(turnId, event.step); const prev = this.currentStep?.stepId === stepId ? this.currentStep : this.steps.get(stepId); const step: StepRecord = { stepId, turnId, ordinal: event.step, status: 'interrupted', startedAt: prev?.startedAt, endedAt: epochMsToIso(event.time), endReason: event.reason, endMessage: event.message, }; this.currentStep = step; this.steps.set(stepId, step); ops.push(this.stepOp(step)); return ops; } private onStepRetrying(event: { turnId: number; step: number; failedAttempt: number; nextAttempt: number; maxAttempts: number; delayMs: number; errorName: string; errorMessage: string; statusCode?: number; }): ServerMessage[] { const turnId = turnIdOf(event.turnId); const stepId = stepIdOf(turnId, event.step); const prev = this.currentStep?.stepId === stepId ? this.currentStep : this.steps.get(stepId); const step: StepRecord = { stepId, turnId, ordinal: event.step, status: 'running', startedAt: prev?.startedAt, retry: { failed_attempt: event.failedAttempt, next_attempt: event.nextAttempt, max_attempts: event.maxAttempts, delay_ms: event.delayMs, error_name: event.errorName, error_message: event.errorMessage, status_code: event.statusCode, }, }; this.currentStep = step; this.steps.set(stepId, step); return [this.stepOp(step)]; } private onTextDelta( event: { time: number; turnId: number; delta: string }, kind: 'assistant' | 'thinking', ): ServerMessage[] { if (kind === 'thinking' && event.delta.length === 0) return []; const ops = this.settlePendingClear(); const turnId = turnIdOf(event.turnId); this.ensureTurn(turnId, event.time, ops); const step = this.ensureStep(turnId, event.time, ops); let open = kind === 'assistant' ? this.openText : this.openThinking; if (open === undefined || open.stepId !== step.stepId) { open = this.createTextRecord(step.stepId, turnId, kind); if (kind === 'assistant') this.openText = open; else this.openThinking = open; ops.push(this.textOp(open)); } open.text += event.delta; ops.push(this.textDeltaOp(open, event.delta)); return ops; } private flushOpenTexts(): ServerMessage[] { const ops: ServerMessage[] = []; for (const open of [this.openText, this.openThinking]) { if (open === undefined) continue; open.status = 'completed'; ops.push(this.textOp(open)); } this.openText = undefined; this.openThinking = undefined; return ops; } private ensureTurn(turnId: string, time: number, ops: ServerMessage[]): TurnRecord { if (this.currentTurn !== undefined && this.currentTurn.turnId === turnId) { return this.currentTurn; } const ordinal = turnOrdinalOf(turnId) ?? 0; this.noteTurnId(ordinal); const turn: TurnRecord = { turnId, ordinal, status: 'running', origin: { kind: 'other' }, anchor: false, openingSteerDeduped: false, startedAt: epochMsToIso(time), }; this.currentTurn = turn; this.turns.set(turnId, turn); this.timelineIds.push(turnId); ops.push(this.turnOp(turn)); return turn; } private ensureStep(turnId: string, time: number, ops: ServerMessage[]): StepRecord { if (this.currentStep !== undefined && this.currentStep.turnId === turnId) { return this.currentStep; } const ordinal = this.lookups?.stepOrdinal?.(turnId) ?? this.stepOrdinals.get(turnId) ?? 1; const step: StepRecord = { stepId: stepIdOf(turnId, ordinal), turnId, ordinal, status: 'running', startedAt: epochMsToIso(time), }; this.stepOrdinals.set(turnId, ordinal); this.currentStep = step; this.steps.set(step.stepId, step); ops.push(this.stepOp(step)); return step; } private onToolCallDelta(event: { time: number; turnId: number; toolCallId: string; name?: string; argumentsPart?: string; }): ServerMessage[] { const ops = this.settlePendingClear(); const prev = this.tools.get(event.toolCallId); if (prev !== undefined) { prev.inputText = (prev.inputText ?? '') + (event.argumentsPart ?? ''); ops.push(this.toolDeltaOp(event.toolCallId, event.argumentsPart ?? '')); return ops; } const turnId = turnIdOf(event.turnId); this.ensureTurn(turnId, event.time, ops); const step = this.ensureStep(turnId, event.time, ops); const tool: ToolRecord = { toolCallId: event.toolCallId, turnId, stepId: step.stepId, name: event.name ?? '', status: 'running', inputText: event.argumentsPart ?? '', agentRefs: [], startedAt: epochMsToIso(event.time), }; this.tools.set(event.toolCallId, tool); ops.push(this.toolOp(tool)); if ((event.argumentsPart ?? '').length > 0) { ops.push(this.toolDeltaOp(event.toolCallId, event.argumentsPart ?? '')); } return ops; } private onToolProgress(event: { toolCallId: string; update: { kind: 'stdout' | 'stderr' | 'progress' | 'status' | 'custom'; text?: string; percent?: number; customKind?: string; customData?: unknown; }; }): ServerMessage[] { const tool = this.tools.get(event.toolCallId); if (tool === undefined) return []; tool.progress = { kind: event.update.kind, text: event.update.text, percent: event.update.percent, custom_kind: event.update.customKind, custom_data: event.update.customData, }; return [ { type: 'tool.progress', ...this.base(), tool_call_id: event.toolCallId, progress: tool.progress, }, ]; } private onToolCallStarted(event: { time: number; turnId: number; toolCallId: string; name: string; args: unknown; display?: unknown; }): ServerMessage[] { const ops = this.settlePendingClear(); const turnId = turnIdOf(event.turnId); this.ensureTurn(turnId, event.time, ops); const step = this.ensureStep(turnId, event.time, ops); const prev = this.tools.get(event.toolCallId); const input = parseToolArgs(event.args); const todoItems = event.name === 'TodoList' ? todoWriteItems(input) : undefined; const tool: ToolRecord = { toolCallId: event.toolCallId, turnId, stepId: step.stepId, name: event.name, status: 'running', input, inputText: prev?.inputText ?? (typeof event.args === 'string' ? event.args : undefined), display: event.display, todoId: todoItems !== undefined ? TODO_ENTITY_ID : undefined, progress: prev?.progress, agentRefs: prev?.agentRefs ?? [], startedAt: prev?.startedAt ?? epochMsToIso(event.time), }; this.tools.set(event.toolCallId, tool); ops.push(this.toolOp(tool)); return ops; } private onToolResult(event: { time: number; turnId: number; toolCallId: string; output: unknown; isError?: boolean; }): ServerMessage[] { const ops = this.settlePendingClear(); let tool = this.tools.get(event.toolCallId); if (tool === undefined) { const turnId = turnIdOf(event.turnId); this.ensureTurn(turnId, event.time, ops); const step = this.ensureStep(turnId, event.time, ops); tool = { toolCallId: event.toolCallId, turnId, stepId: step.stepId, name: '', status: 'running', agentRefs: [], }; this.tools.set(event.toolCallId, tool); } const isError = event.isError === true; tool.status = isError ? 'error' : 'done'; tool.output = event.output; tool.error = isError && typeof event.output === 'string' ? event.output : undefined; ops.push(this.toolOp(tool)); return ops; } private onTaskLifecycle(event: { type: 'task.started' | 'task.terminated'; time: number; info: AgentTaskInfo; }): ServerMessage[] { const ops = this.settlePendingClear(); const { info } = event; const agentInfo = agentInfoOf(info); const parentTool = agentInfo?.parentToolCallId === undefined ? undefined : this.tools.get(agentInfo.parentToolCallId); const task = this.upsertTask(info.taskId, (prev) => ({ taskId: info.taskId, kind: mapTaskKind(info.kind), status: info.status, detached: info.detached ?? prev?.detached ?? true, description: info.description, childAgentId: agentInfo?.agentId ?? prev?.childAgentId, outputTail: prev?.outputTail ?? '', startedAt: prev?.startedAt ?? parentTool?.startedAt ?? epochMsToIso(info.startedAt), endedAt: info.endedAt === null ? prev?.endedAt : epochMsToIso(info.endedAt), resultSummary: prev?.resultSummary, usage: prev?.usage, error: prev?.error, stateReason: info.stopReason ?? prev?.stateReason, model: agentInfo?.model ?? prev?.model, thinkingEffort: agentInfo?.thinkingEffort ?? prev?.thinkingEffort, })); if (event.type === 'task.started') { const childAgentId = agentInfo?.agentId; if (info.kind === 'agent' && typeof childAgentId === 'string' && childAgentId.length > 0) { this.subagentTaskIds.set(childAgentId, info.taskId); if (parentTool !== undefined && parentTool.taskId !== info.taskId) { parentTool.taskId = info.taskId; ops.push(this.toolOp(parentTool)); } } } ops.push(this.taskOp(task)); return ops; } private onShellStarted(event: { time: number; commandId: string; taskId: string; }): ServerMessage[] { const ops = this.settlePendingClear(); this.shellTasks.set(event.commandId, event.taskId); const task = this.upsertTask(event.taskId, (prev) => ({ taskId: event.taskId, kind: 'shell', status: 'running', detached: prev?.detached ?? false, description: prev?.description, outputTail: prev?.outputTail ?? '', startedAt: prev?.startedAt ?? epochMsToIso(event.time), })); ops.push(this.taskOp(task)); return ops; } private shellTaskId(event: { commandId: string; taskId?: string }): string { const taskId = this.shellTasks.get(event.commandId) ?? event.taskId ?? `shell-${event.commandId}`; this.shellTasks.set(event.commandId, taskId); return taskId; } private onShellOutput(event: { time: number; commandId: string; taskId?: string; update: { kind: string; text?: string }; }): ServerMessage[] { const text = event.update.text; if (typeof text !== 'string' || text.length === 0) return []; const ops = this.settlePendingClear(); const taskId = this.shellTaskId(event); const task = this.upsertTask(taskId, (prev) => ({ taskId, kind: prev?.kind ?? 'shell', status: 'running', detached: prev?.detached ?? false, description: prev?.description, outputTail: tailWindow((prev?.outputTail ?? '') + text), startedAt: prev?.startedAt ?? epochMsToIso(event.time), })); ops.push(this.taskOp(task)); return ops; } private onShellCompleted(event: { time: number; commandId: string; taskId?: string; isError: boolean; }): ServerMessage[] { const ops = this.settlePendingClear(); const taskId = this.shellTaskId(event); const task = this.upsertTask(taskId, (prev) => ({ taskId, kind: prev?.kind ?? 'shell', status: event.isError ? 'failed' : 'completed', detached: prev?.detached ?? false, description: prev?.description, outputTail: prev?.outputTail ?? '', startedAt: prev?.startedAt ?? epochMsToIso(event.time), endedAt: epochMsToIso(event.time), })); ops.push(this.taskOp(task)); return ops; } private upsertTask( taskId: string, build: (prev: TaskRecord | undefined) => TaskRecord, ): TaskRecord { const task = build(this.tasks.get(taskId)); this.tasks.set(taskId, task); return task; } private onSubagentSpawned(event: { time: number; subagentId: string; parentToolCallId: string; description?: string; swarmIndex?: number; runInBackground: boolean; taskId?: string; model?: string; thinkingEffort?: string; }): ServerMessage[] { const ops = this.settlePendingClear(); const tool = this.tools.get(event.parentToolCallId); if (tool !== undefined) { const ref: ToolCallAgentRef = { agent_id: event.subagentId, role: event.swarmIndex !== undefined ? 'member' : 'child', }; tool.agentRefs = [...tool.agentRefs, ref]; ops.push(this.toolOp(tool)); } const taskId = event.taskId; if (taskId === undefined) return ops; this.subagentTaskIds.set(event.subagentId, taskId); if (tool !== undefined && tool.taskId !== taskId) { tool.taskId = taskId; ops.push(this.toolOp(tool)); } const task = this.upsertTask(taskId, (prev) => ({ taskId, kind: 'subagent', status: 'running', detached: event.runInBackground, description: event.description ?? prev?.description, childAgentId: event.subagentId, outputTail: prev?.outputTail ?? '', startedAt: prev?.startedAt ?? tool?.startedAt ?? epochMsToIso(event.time), model: event.model ?? prev?.model, thinkingEffort: event.thinkingEffort ?? prev?.thinkingEffort, })); ops.push(this.taskOp(task)); return ops; } private onSubagentRun(event: { type: 'subagent.completed' | 'subagent.failed' | 'subagent.cancelled' | 'subagent.suspended'; time: number; subagentId: string; resultSummary?: string; usage?: TokenUsage; error?: string; reason?: string; }): ServerMessage[] { const ops = this.settlePendingClear(); const taskKey = this.subagentTaskIds.get(event.subagentId) ?? event.subagentId; const existing = this.tasks.get(taskKey); if (existing === undefined) return ops; const terminal = event.type !== 'subagent.suspended'; existing.status = event.type === 'subagent.completed' ? 'completed' : event.type === 'subagent.failed' ? 'failed' : event.type === 'subagent.cancelled' ? 'killed' : 'running'; if (terminal) existing.endedAt = epochMsToIso(event.time); existing.resultSummary = event.resultSummary ?? existing.resultSummary; existing.usage = event.usage === undefined ? existing.usage : toSnakeUsage(event.usage); existing.error = event.error ?? existing.error; existing.stateReason = event.reason ?? existing.stateReason; ops.push(this.taskOp(existing)); return ops; } private onGoalUpdated(event: { time: number; snapshot: { objective: string; status: 'active' | 'paused' | 'blocked' | 'complete'; completionCriterion?: string; tokensUsed: number; budget: { tokenBudget: number | null }; } | null; }): ServerMessage[] { const snapshot = event.snapshot; const payload = snapshot === null ? undefined : { objective: snapshot.objective, status: snapshot.status, completion_criterion: snapshot.completionCriterion, budget_used: snapshot.tokensUsed, budget_limit: snapshot.budget.tokenBudget ?? undefined, }; return [this.systemOp('goal', payload, event.time)]; } private onAgentStatusUpdated(event: { time: number; planMode?: boolean; swarmMode?: boolean; }): ServerMessage[] { const ops: ServerMessage[] = []; if (event.planMode !== undefined && event.planMode !== this.planMode) { this.planMode = event.planMode; if (event.planMode) { ops.push(this.systemOp('plan.enter', undefined, event.time)); } else if (this.planExitApproved()) { ops.push(this.systemOp('plan.exit', undefined, event.time)); } } if (event.swarmMode !== undefined && event.swarmMode !== this.swarmMode) { this.swarmMode = event.swarmMode; ops.push( this.systemOp(event.swarmMode ? 'swarm.enter' : 'swarm.exit', undefined, event.time), ); } return ops; } private planExitApproved(): boolean { let latest: ToolRecord | undefined; for (const tool of this.tools.values()) { if (tool.name === 'ExitPlanMode') latest = tool; } if (latest?.approvalId === undefined) return false; return this.interactions.get(latest.approvalId)?.status === 'approved'; } private onPlanRevision(event: { time: number; id: string; version: number; key: string; sha256: string; bytes: number; }): ServerMessage[] { const path = this.lookups?.resolvePlanRevisionKey?.(event.key) ?? event.key; return [ this.systemOp( 'plan.revision', { id: event.id, version: event.version, path, sha256: event.sha256, bytes: event.bytes }, event.time, ), ]; } private onPromptSubmitted(event: { promptId: string; userMessageId: string; status: 'running' | 'queued'; content: readonly ContentPart[]; createdAt: string; }): ServerMessage[] { const prev = this.prompts.get(event.promptId); this.prompts.set(event.promptId, { promptId: event.promptId, userMessageId: event.userMessageId, content: event.content, status: event.status, createdAt: prev?.createdAt ?? event.createdAt, }); return []; } private onPromptQueued(event: { promptId: string; content: readonly ContentPart[]; }): ServerMessage[] { let prev = this.prompts.get(event.promptId); if (prev === undefined) { prev = { promptId: event.promptId, userMessageId: event.promptId, content: event.content, status: 'queued', createdAt: nowIso(), }; this.prompts.set(event.promptId, prev); } if (this.users.has(prev.userMessageId)) return []; const user: UserRecord = { messageId: prev.userMessageId, text: wireContentParts(prev.content), status: 'unread', }; this.users.set(user.messageId, user); return [this.userOp(user)]; } private onPromptStarted(event: { promptId: string }): ServerMessage[] { const prev = this.prompts.get(event.promptId); if (prev === undefined) return []; prev.status = 'running'; return []; } private onPromptCompleted(event: { promptId: string }): ServerMessage[] { const prev = this.prompts.get(event.promptId); if (prev === undefined) return []; prev.status = 'completed'; return []; } private onPromptAborted(event: { promptId: string }): ServerMessage[] { const prev = this.prompts.get(event.promptId); if (prev === undefined) return []; prev.status = 'aborted'; return []; } private onPromptSteered(event: { activePromptId: string; promptIds: string[]; content: readonly ContentPart[]; steeredAt: string; }): ServerMessage[] { for (const promptId of event.promptIds) { const prev = this.prompts.get(promptId); if (prev === undefined) continue; prev.status = 'steered'; } this.mergedSteers.push({ text: promptTextOf(event.content), promptIds: event.promptIds }); return []; } private onTurnSteered(event: { time: number; input: readonly ContentPart[]; origin: unknown; }): ServerMessage[] { const origin = event.origin as { kind?: string; skillActivations?: readonly { skillName: string; skillArgs?: string }[]; jobId?: string; cron?: string; trigger?: string; }; const kind = origin.kind; if (kind !== 'user' && kind !== 'skill_activation' && kind !== 'cron_job') return []; if (kind === 'skill_activation' && origin.trigger !== 'user-slash') return []; const ops = this.settlePendingClear(); const turn = this.currentTurn; if (turn === undefined || turn.status !== 'running') return ops; const skipBlocks = kind === 'user' ? (origin.skillActivations?.length ?? 0) : 0; const step = this.currentStep; const stepStarted = step !== undefined && step.turnId === turn.turnId; if (!stepStarted && !turn.openingSteerDeduped && turn.openingKey !== undefined) { const key = steerKeyOf(event.input, skipBlocks); if (key.text === turn.openingKey.text && key.attachments === turn.openingKey.attachments) { turn.openingSteerDeduped = true; return ops; } } const matched = kind === 'user' ? this.matchQueuedPrompt(event.input, skipBlocks) : undefined; if (matched !== undefined) { const text = promptTextOf(event.input.slice(skipBlocks)); this.mergedSteers = this.mergedSteers.filter((entry) => entry.text !== text); const existing = this.users.get(matched); if (existing !== undefined) { if (existing.status === 'unread') { existing.status = 'read'; existing.turnId = turn.turnId; existing.timestamp = event.time; ops.push(this.userOp(existing)); } return ops; } ops.push( this.steerUserMessage(turn, event.input, { origin: userOriginOf(event.origin), skillActivations: skillActivationsOf(event.origin), skipBlocks, at: event.time, messageId: matched, }), ); return ops; } if (kind === 'user') { const merged = this.matchMergedSteer(event.input, skipBlocks); if (merged !== undefined) { ops.push(...this.readSteeredUsers(merged, turn, event.time)); return ops; } } ops.push( this.steerUserMessage(turn, event.input, { origin: userOriginOf(event.origin), skillActivations: skillActivationsOf(event.origin), skipBlocks, at: event.time, }), ); return ops; } private matchQueuedPrompt( input: readonly ContentPart[], skipBlocks: number, ): string | undefined { const text = promptTextOf(input.slice(skipBlocks)); let matched: string | undefined; for (const prompt of this.prompts.values()) { if (prompt.status !== 'queued' && prompt.status !== 'steered') continue; if (promptTextOf(prompt.content) !== text) continue; if (matched !== undefined) return undefined; matched = prompt.userMessageId; } return matched; } private matchMergedSteer( input: readonly ContentPart[], skipBlocks: number, ): { promptIds: string[] } | undefined { const text = promptTextOf(input.slice(skipBlocks)); const index = this.mergedSteers.findIndex((entry) => entry.text === text); if (index < 0) return undefined; const [entry] = this.mergedSteers.splice(index, 1); return entry; } private readSteeredUsers( merged: { promptIds: string[] }, turn: TurnRecord, timestamp: number, ): ServerMessage[] { const ops: ServerMessage[] = []; for (const promptId of merged.promptIds) { const prompt = this.prompts.get(promptId); if (prompt === undefined) continue; prompt.status = 'completed'; const user = this.users.get(prompt.userMessageId); if (user === undefined || user.status !== 'unread') continue; user.status = 'read'; user.turnId = turn.turnId; user.timestamp = timestamp; ops.push(this.userOp(user)); } return ops; } private steerUserMessage( turn: TurnRecord, input: readonly ContentPart[], opts: { origin: UserMessageOrigin | undefined; skillActivations: { skill_name: string; skill_args?: string }[] | undefined; skipBlocks: number; at: number; text?: string; messageId?: string; }, ): ServerMessage { const messageId = opts.messageId ?? `${turn.turnId}.u${(this.serverUserSeq += 1)}`; const attachmentIds: string[] = []; for (const part of input.slice(opts.skipBlocks)) { if (part.type === 'text') continue; if (daemonFileRefFromPart(part) === undefined) continue; this.attachmentSeq += 1; attachmentIds.push(attachmentIdOf(turn.turnId, this.attachmentSeq)); } const user: UserRecord = { messageId, turnId: turn.turnId, text: opts.text !== undefined ? textPartsOf(opts.text) : wireContentParts(input.slice(opts.skipBlocks)), status: 'read', timestamp: opts.at, origin: opts.origin, attachmentIds: attachmentIds.length > 0 ? attachmentIds : undefined, skillActivations: opts.skillActivations, }; this.users.set(messageId, user); return this.userOp(user); } private onTaskNotified(event: { time: number; notificationType: string; title: string; body: string; severity: string; sourceKind: string; sourceId: string; }): ServerMessage[] { const ops = this.settlePendingClear(); const notification: TaskNotificationPayload = { title: event.title, body: event.body, severity: event.severity, type: event.notificationType, source_kind: event.sourceKind, source_id: event.sourceId, }; const origin = taskUserOriginOf(event.sourceId, notification); if (origin === undefined) return ops; const text = notificationTextOf(notification); const turn = this.currentTurn; if ( turn !== undefined && turn.status === 'running' && turn.origin.kind === 'task' && turn.origin.task_id === origin.task_id ) { const messageId = turn.userMessageId ?? turnUserMessageIdOf(turn.turnId); if (turn.userMessageId === undefined) { turn.userMessageId = messageId; ops.push(this.turnOp(turn)); } const existing = this.users.get(messageId); if (existing !== undefined) { if (existing.origin?.kind !== 'task' || existing.origin.title === '') { existing.text = textPartsOf(text); existing.origin = origin; ops.push(this.userOp(existing)); } return ops; } const user: UserRecord = { messageId, turnId: turn.turnId, text: textPartsOf(text), status: 'read', timestamp: event.time, origin, }; this.users.set(messageId, user); ops.push(this.userOp(user)); return ops; } if (turn !== undefined && turn.status === 'running') { ops.push( this.steerUserMessage(turn, [], { origin, skillActivations: undefined, skipBlocks: 0, at: event.time, text, }), ); return ops; } this.phantomUserSeq += 1; const turnId = turnIdOf(this.nextTurnIdHint); const user: UserRecord = { messageId: `${turnId}.u${this.phantomUserSeq}`, turnId, text: textPartsOf(text), status: 'read', timestamp: event.time, origin, }; this.users.set(user.messageId, user); ops.push(this.userOp(user)); return ops; } private onContextSpliced(event: { start: number; deleteCount: number; messages: readonly unknown[]; }): ServerMessage[] { if (event.start === 0 && event.deleteCount > 0 && event.messages.length === 0) { this.pendingFullCut = true; this.armPendingClearTimer(); } return []; } private armPendingClearTimer(): void { if (this.pendingClearTimer !== undefined) return; this.pendingClearTimer = setTimeout(() => { this.pendingClearTimer = undefined; if (!this.pendingFullCut) return; this.pendingFullCut = false; this.hooks?.onDeferred?.(this.applyClear()); }, PENDING_CLEAR_SETTLE_MS); this.pendingClearTimer.unref(); } private cancelPendingClearTimer(): void { if (this.pendingClearTimer === undefined) return; clearTimeout(this.pendingClearTimer); this.pendingClearTimer = undefined; } private settlePendingClear(): ServerMessage[] { if (!this.pendingFullCut) return []; this.cancelPendingClearTimer(); this.pendingFullCut = false; return this.applyClear(); } private applyClear(): ServerMessage[] { this.timelineRewriteCount += 1; const removed = [...this.timelineIds]; const op = this.systemOp('clear', { removed_ids: removed }, undefined); this.turns.clear(); this.steps.clear(); this.texts.clear(); this.stepTextIds.clear(); this.stepTextSeqs.clear(); this.tools.clear(); this.users.clear(); this.stepOrdinals.clear(); this.stepUsageByTurn.clear(); this.timelineIds.length = 0; this.currentTurn = undefined; this.currentStep = undefined; this.openText = undefined; this.openThinking = undefined; return [op]; } private onContextUndone(event: { time: number; turns: number; fromTurnId?: number; }): ServerMessage[] { this.cancelPendingClearTimer(); this.pendingFullCut = false; const removed = this.removedIdsForUndo(event.turns, event.fromTurnId); if (removed.length === 0) return []; this.timelineRewriteCount += 1; const op = this.systemOp('undo', { removed_ids: removed }, event.time); for (const id of removed) { if (turnOrdinalOf(id) === undefined) continue; this.dropTurnDetails(id); this.stepOrdinals.delete(id); this.stepUsageByTurn.delete(id); } const firstRemoved = this.timelineIds.indexOf(removed[0]!); if (firstRemoved >= 0) this.timelineIds.splice(firstRemoved); return [op]; } private removedIdsForUndo(turns: number, fromTurnId: number | undefined): string[] { const cut = this.findUndoCutIndex(turns, fromTurnId); if (cut === undefined) return []; return this.timelineIds.slice(cut); } private findUndoCutIndex(turns: number, fromTurnId: number | undefined): number | undefined { if (fromTurnId !== undefined) { for (let i = 0; i < this.timelineIds.length; i++) { const ordinal = turnOrdinalOf(this.timelineIds[i]!); if (ordinal !== undefined && ordinal >= fromTurnId) return i; } return undefined; } let remaining = turns; for (let i = this.timelineIds.length - 1; i >= 0; i--) { const id = this.timelineIds[i]!; if (isCompactionSystemId(id)) return undefined; const ordinal = turnOrdinalOf(id); if (ordinal === undefined) continue; if (!this.anchorTurnOrdinals.has(ordinal)) continue; remaining -= 1; if (remaining === 0) return i; } return undefined; } private dropTurnDetails(turnId: string): void { this.turns.delete(turnId); for (const [stepId, step] of this.steps) { if (step.turnId === turnId) this.steps.delete(stepId); } for (const [stepId, entry] of this.stepTextIds) { if (!stepId.startsWith(`${turnId}.`)) continue; if (entry.assistant !== undefined) this.texts.delete(entry.assistant); if (entry.thinking !== undefined) this.texts.delete(entry.thinking); this.stepTextIds.delete(stepId); this.stepTextSeqs.delete(stepId); } for (const [toolCallId, tool] of this.tools) { if (tool.turnId === turnId) this.tools.delete(toolCallId); } for (const [messageId, user] of this.users) { if (user.turnId === turnId) this.users.delete(messageId); } } private mapTurnOrigin(origin: unknown): TurnOrigin { return toTurnOrigin(origin, this.agentId, this.subagentTaskIds); } private wireInteractionRequest(interaction: ProjectorInteraction): unknown { return wireInteractionRequest(interaction.kind, interaction.payload); } private wireInteractionResponse(record: InteractionRecord, response: unknown): unknown { return wireInteractionResponse(record.kind, record.request, response); } private createTextRecord( stepId: string, turnId: string, kind: 'assistant' | 'thinking', ): TextRecord { const seq = (this.stepTextSeqs.get(stepId) ?? 0) + 1; this.stepTextSeqs.set(stepId, seq); const record: TextRecord = { messageId: textMessageIdOf(stepId, seq), kind, turnId, stepId, status: 'streaming', text: '', }; this.texts.set(record.messageId, record); const entry = this.stepTextIds.get(stepId) ?? {}; entry[kind] = record.messageId; this.stepTextIds.set(stepId, entry); return record; } private base(): { session_id: string; agent_id: string; timestamp: number } { return { session_id: this.sessionId, agent_id: this.agentId, timestamp: Date.now() }; } private turnOp(turn: TurnRecord): TurnMessage { return { type: 'turn', ...this.base(), turn_id: turn.turnId, ordinal: turn.ordinal, status: turn.status, origin: turn.origin, user_message_id: turn.userMessageId, attachment_ids: turn.attachmentIds, started_at: turn.startedAt, ended_at: turn.endedAt, usage: turn.usage === undefined ? undefined : turnUsageToWire(turn.usage), duration_ms: turn.durationMs, }; } private stepOp(step: StepRecord): StepMessage { return { type: 'step', ...this.base(), step_id: step.stepId, turn_id: step.turnId, ordinal: step.ordinal, status: step.status, started_at: step.startedAt, ended_at: step.endedAt, usage: step.usage, finish_reason: step.finishReason, timing: step.timing, retry: step.retry, end_reason: step.endReason, end_message: step.endMessage, }; } private textOp(record: TextRecord): AssistantMessage | ThinkingMessage { const base = { ...this.base(), message_id: record.messageId, turn_id: record.turnId, step_id: record.stepId, status: record.status, text: record.text, }; if (record.kind === 'assistant') return { type: 'assistant', ...base }; return { type: 'thinking', ...base }; } private textDeltaOp(record: TextRecord, delta: string): ServerMessage { if (record.kind === 'assistant') { return { type: 'assistant.delta', ...this.base(), message_id: record.messageId, text: delta, }; } return { type: 'thinking.delta', ...this.base(), message_id: record.messageId, text: delta, }; } private toolOp(tool: ToolRecord): ToolCallMessage { return { type: 'tool_call', ...this.base(), tool_call_id: tool.toolCallId, turn_id: tool.turnId, step_id: tool.stepId, name: tool.name, status: tool.status, input: tool.input, input_text: tool.inputText, output: tool.output, display: tool.display, error: tool.error, progress: tool.progress, task_id: tool.taskId, approval_id: tool.approvalId, todo_id: tool.todoId, agent_refs: tool.agentRefs.length > 0 ? tool.agentRefs : undefined, }; } private toolDeltaOp(toolCallId: string, inputText: string): ServerMessage { return { type: 'tool_call.delta', ...this.base(), tool_call_id: toolCallId, input_text: inputText, }; } private userOp(user: UserRecord): UserMessage { return { type: 'user', session_id: this.sessionId, agent_id: this.agentId, message_id: user.messageId, turn_id: user.turnId, status: user.status, timestamp: user.timestamp, text: user.text, attachment_ids: user.attachmentIds, skill_activations: user.skillActivations, origin: user.origin, }; } private taskOp(task: TaskRecord): TaskMessage { return { type: 'task', ...this.base(), task_id: task.taskId, kind: task.kind, status: task.status, detached: task.detached, description: task.description, child_agent_id: task.childAgentId, output_tail: task.outputTail, started_at: task.startedAt, ended_at: task.endedAt, result_summary: task.resultSummary, error: task.error, state_reason: task.stateReason, usage: task.usage, model: task.model, thinking_effort: task.thinkingEffort, }; } private interactionOp(record: InteractionRecord): InteractionMessage { return { type: 'interaction', ...this.base(), interaction_id: record.interactionId, kind: record.kind, status: record.status, tool_call_id: record.toolCallId, request: record.request, response: record.response, } as InteractionMessage; } private todoOp(): TodoMessage { return { type: 'todo', ...this.base(), todo_id: TODO_ENTITY_ID, items: this.todoItems ?? [], updated_at: this.todoUpdatedAt, }; } private systemOp( subtype: SystemMessage['subtype'], payload: unknown, time?: number, ): SystemMessage { const systemId = this.sysIds.next(subtype); this.timelineIds.push(systemId); return { type: 'system', ...this.base(), system_id: systemId, subtype, payload, at: time === undefined ? undefined : epochMsToIso(time), } as SystemMessage; } } export function toTurnOrigin( origin: unknown, agentId: string, subagentTaskIds: ReadonlyMap, ): TurnOrigin { const candidate = origin as | { kind?: unknown; taskId?: unknown; name?: unknown } | null | undefined; const kind = typeof candidate?.kind === 'string' ? candidate.kind : undefined; if (kind === undefined) return { kind: 'other' }; switch (kind) { case 'user': case 'skill_activation': case 'plugin_command': case 'shell_command': return { kind: 'user' }; case 'cron_job': case 'cron_missed': return { kind: 'cron' }; case 'task': case 'background_task': { const taskId = candidate?.taskId; return typeof taskId === 'string' ? { kind: 'task', task_id: taskId } : { kind: 'other' }; } case 'hook_result': return { kind: 'hook' }; case 'compaction_summary': return { kind: 'compaction' }; case 'system_trigger': { if (candidate?.name === 'goal_continuation') return { kind: 'goal' }; const taskId = subagentTaskIds.get(agentId); return taskId === undefined ? { kind: 'other' } : { kind: 'task', task_id: taskId }; } default: return { kind: 'other' }; } } function nowIso(): string { return new Date().toISOString(); } function epochMsToIso(value: number): string { return new Date(value).toISOString(); } function restOf(event: { readonly type: string; readonly time?: number; readonly agentId?: string; }): Record { const { type: _type, time: _time, agentId: _agentId, ...rest } = event; return rest; } function toSnakeUsage(usage: TokenUsage): StepUsage { return { input_other: usage.inputOther, output: usage.output, input_cache_read: usage.inputCacheRead, input_cache_creation: usage.inputCacheCreation, }; } function turnUsageToWire(usage: StepUsage): { input_tokens: number; output_tokens: number; cached_tokens: number; } { return { input_tokens: usage.input_other + usage.input_cache_creation, output_tokens: usage.output, cached_tokens: usage.input_cache_read, }; } function timingOf(event: { llmFirstTokenLatencyMs?: number; llmStreamDurationMs?: number; }): StepTiming | undefined { if (event.llmFirstTokenLatencyMs === undefined && event.llmStreamDurationMs === undefined) { return undefined; } return { llm_first_token_ms: event.llmFirstTokenLatencyMs, llm_stream_duration_ms: event.llmStreamDurationMs, }; } function mapTaskKind(kind: string): TaskMessage['kind'] { switch (kind) { case 'process': return 'shell'; case 'agent': return 'subagent'; default: return 'other'; } } function agentInfoOf(info: AgentTaskInfo): | { agentId?: string; parentToolCallId?: string; model?: string; thinkingEffort?: string; } | undefined { if (info.kind !== 'agent') return undefined; return info as { agentId?: string; parentToolCallId?: string; model?: string; thinkingEffort?: string; }; } function tailWindow(text: string): string { return text.length <= TASK_OUTPUT_TAIL_MAX ? text : text.slice(text.length - TASK_OUTPUT_TAIL_MAX); } export function parseToolArgs(args: unknown): unknown { if (typeof args !== 'string' || args.length === 0) return args; try { return JSON.parse(args) as unknown; } catch { return args; } } export function todoWriteItems(input: unknown): readonly { title: string; status: 'pending' | 'in_progress' | 'done' }[] | undefined { const todos = (input as { todos?: unknown } | undefined)?.todos; if (!Array.isArray(todos)) return undefined; const items = readTodoItems(todos); return items.length === 0 && todos.length > 0 ? undefined : items; } export function mapInteractionEndStatus( kind: 'approval' | 'question', response: unknown, ): InteractionMessage['status'] { if (isCancellation(response)) return 'cancelled'; if (kind === 'question') return response === null ? 'dismissed' : 'answered'; const decision = (response as { decision?: unknown } | null | undefined)?.decision; if (decision === 'approved' || decision === 'rejected' || decision === 'cancelled') { return decision; } return 'cancelled'; } export function isCancellation(response: unknown): boolean { return (response as { cancelled?: unknown } | null | undefined)?.cancelled === true; } export function wantsUserMessage(origin: unknown, promptText: string | undefined): boolean { const candidate = origin as { kind?: unknown; name?: unknown } | null | undefined; switch (candidate?.kind) { case 'user': case 'skill_activation': case 'plugin_command': case 'shell_command': case 'cron_job': case 'cron_missed': return true; case 'system_trigger': return ( candidate.name === 'subagent' && typeof promptText === 'string' && promptText.length > 0 ); default: return false; } } export function userOriginOf(origin: unknown): UserMessageOrigin | undefined { const candidate = origin as | { kind?: unknown; jobId?: unknown; cron?: unknown; skillName?: unknown; skillArgs?: unknown; trigger?: unknown; pluginId?: unknown; commandName?: unknown; commandArgs?: unknown; } | null | undefined; if (candidate?.kind === 'cron_job') return cronUserOrigin(candidate); if (candidate?.kind === 'cron_missed') return { kind: 'cron' }; if (candidate?.kind === 'skill_activation' && typeof candidate.skillName === 'string') { return { kind: 'skill', skill_name: candidate.skillName, args: typeof candidate.skillArgs === 'string' ? candidate.skillArgs : undefined, trigger: typeof candidate.trigger === 'string' ? candidate.trigger : undefined, }; } if (candidate?.kind === 'plugin_command') { const name = typeof candidate.commandName === 'string' ? candidate.commandName : typeof candidate.pluginId === 'string' ? candidate.pluginId : undefined; if (name === undefined) return undefined; return { kind: 'skill', skill_name: name, args: typeof candidate.commandArgs === 'string' ? candidate.commandArgs : undefined, trigger: typeof candidate.trigger === 'string' ? candidate.trigger : undefined, }; } return undefined; } export function taskUserOriginOf( taskId: unknown, notification?: TaskNotificationPayload, ): Extract | undefined { if (typeof taskId !== 'string' || taskId.length === 0) return undefined; if (notification === undefined) return { kind: 'task', task_id: taskId, title: '', body: '' }; return { kind: 'task', task_id: taskId, ...notification }; } export function taskNotificationOriginOf( origin: unknown, ): Extract | undefined { const candidate = origin as { kind?: unknown; taskId?: unknown } | null | undefined; if (candidate?.kind !== 'task' && candidate?.kind !== 'background_task') return undefined; return taskUserOriginOf(candidate.taskId); } export function notificationTextOf(notification: { title: string; body: string; }): string { return `${notification.title}\n${notification.body}`.trim(); } function cronUserOrigin(candidate: { jobId?: unknown; cron?: unknown; }): UserMessageOrigin | undefined { if (typeof candidate.jobId !== 'string' || typeof candidate.cron !== 'string') return undefined; return { kind: 'cron', cron_id: candidate.jobId, schedule: candidate.cron }; } export function skillActivationsOf( origin: unknown, ): { skill_name: string; skill_args?: string }[] | undefined { const candidate = origin as { kind?: unknown; skillActivations?: readonly { skillName: string; skillArgs?: string }[]; skillName?: unknown; skillArgs?: unknown; } | null | undefined; if (candidate?.kind === 'user') { const activations = candidate.skillActivations ?? []; if (activations.length === 0) return undefined; return activations.map((a) => ({ skill_name: a.skillName, skill_args: a.skillArgs })); } if (candidate?.kind === 'skill_activation' && typeof candidate.skillName === 'string') { return [ { skill_name: candidate.skillName, skill_args: typeof candidate.skillArgs === 'string' ? candidate.skillArgs : undefined, }, ]; } return undefined; } export function promptTextOf(content: readonly ContentPart[]): string { return content .filter((part): part is ContentPart & { type: 'text' } => part.type === 'text') .map((part) => part.text) .join(''); } export function steerKeyOf( input: readonly ContentPart[], skipBlocks: number, ): { text: string; attachments: number } { let text = ''; let attachments = 0; for (const part of input.slice(skipBlocks)) { if (part.type === 'text') { text += part.text; continue; } if (daemonFileRefFromPart(part) !== undefined) attachments += 1; } return { text, attachments }; } export function wireContentParts(content: readonly ContentPart[]): WireContentPart[] { const out: WireContentPart[] = []; for (const part of content) { switch (part.type) { case 'text': out.push({ type: 'text', text: part.text, meta: {} }); break; case 'think': out.push({ type: 'think', text: part.think, meta: {} }); break; case 'image_url': out.push({ type: 'image', text: part.imageUrl.url, meta: { id: part.imageUrl.id, name: part.imageUrl.name }, }); break; case 'audio_url': out.push({ type: 'audio', text: part.audioUrl.url, meta: { id: part.audioUrl.id } }); break; case 'video_url': out.push({ type: 'video', text: part.videoUrl.url, meta: { id: part.videoUrl.id, name: part.videoUrl.name }, }); break; default: break; } } return out; } export function textPartsOf(text: string): WireContentPart[] { return [{ type: 'text', text, meta: {} }]; } function hookPayload(event: { turnId?: number; hookEvent: string; content: string; blocked?: boolean; }): Record { return { turn_id: event.turnId, hook_event: event.hookEvent, content: event.content, blocked: event.blocked, }; } export function wireInteractionRequest(kind: 'approval' | 'question', payload: unknown): unknown { if (kind === 'approval') { const record = payload as Record | null; const toolName = typeof record?.['toolName'] === 'string' ? record['toolName'] : undefined; if (toolName === undefined || toolName.length === 0) return undefined; return { tool_name: toolName, action: typeof record?.['action'] === 'string' ? record['action'] : '', tool_input_display: record?.['display'], }; } return toV3QuestionRequest(payload); } export function wireInteractionResponse( kind: 'approval' | 'question', request: unknown, response: unknown, ): unknown { if (isCancellation(response)) { return kind === 'approval' ? { decision: 'cancelled' } : undefined; } if (kind === 'approval') { const r = response as { decision?: unknown; scope?: unknown; feedback?: unknown; selectedLabel?: unknown; } | null; if (r === null || typeof r !== 'object') return undefined; const decision = r.decision; if (decision !== 'approved' && decision !== 'rejected' && decision !== 'cancelled') { return undefined; } return { decision, scope: r.scope === 'session' ? 'session' : undefined, feedback: typeof r.feedback === 'string' ? r.feedback : undefined, selected_label: typeof r.selectedLabel === 'string' ? r.selectedLabel : undefined, }; } return mapQuestionResponse(request, response); } export function toV3QuestionRequest(payload: unknown): unknown { const request = payload as { questions?: readonly { question: string; header?: string; body?: string; options: readonly { label: string; description?: string }[]; multiSelect?: boolean; otherLabel?: string; otherDescription?: string; }[]; }; if (request.questions === undefined) return undefined; return { questions: request.questions.map((item, i) => ({ id: `q_${i}`, question: item.question, header: item.header, body: item.body, options: item.options.map((option, j) => ({ id: `opt_${i}_${j}`, label: option.label, description: option.description, })), multi_select: item.multiSelect, allow_other: true, other_label: item.otherLabel, other_description: item.otherDescription, })), }; } function mapQuestionResponse(request: unknown, response: unknown): unknown { const r = response as { answers?: unknown; method?: unknown } | null; if (r === null || typeof r !== 'object' || r.answers === null || typeof r.answers !== 'object') { return undefined; } const items = ( request as | { questions?: readonly { id: string; question: string; options: readonly { id: string; label: string }[]; }[]; } | undefined )?.questions ?? []; const answers: Record = {}; for (const [key, value] of Object.entries(r.answers as Record)) { const item = items.find((q) => q.id === key || q.question === key); if (item === undefined) continue; if (value === true) { answers[item.id] = { kind: 'skipped' }; continue; } if (typeof value !== 'string') continue; const single = item.options.find((o) => o.label === value); if (single !== undefined) { answers[item.id] = { kind: 'single', option_id: single.id }; continue; } const parts = value.split(', '); const optionIds = parts.flatMap((part) => { const found = item.options.find((o) => o.label === part); return found === undefined ? [] : [found.id]; }); if (parts.length > 1 && optionIds.length === parts.length) { answers[item.id] = { kind: 'multi', option_ids: optionIds }; continue; } answers[item.id] = { kind: 'other', text: value }; } if (Object.keys(answers).length === 0) return undefined; const method = r.method; return { answers, method: method === 'enter' || method === 'space' || method === 'number_key' || method === 'click' ? method : undefined, }; }