@getpaseo/server 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/server/client/daemon-client.d.ts +41 -4
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +355 -84
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +10 -0
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +261 -18
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-projections.d.ts +5 -0
  10. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  11. package/dist/server/server/agent/agent-projections.js +24 -0
  12. package/dist/server/server/agent/agent-projections.js.map +1 -1
  13. package/dist/server/server/agent/agent-sdk-types.d.ts +11 -0
  14. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  15. package/dist/server/server/agent/agent-storage.d.ts +15 -5
  16. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-storage.js +2 -0
  18. package/dist/server/server/agent/agent-storage.js.map +1 -1
  19. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
  20. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +2 -0
  21. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  22. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  23. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +2 -0
  24. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  25. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -1
  26. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  27. package/dist/server/server/agent/providers/claude-agent.js +1470 -237
  28. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  29. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  30. package/dist/server/server/agent/providers/codex-app-server-agent.js +19 -4
  31. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  32. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +40 -0
  33. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  34. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +1 -0
  35. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  36. package/dist/server/server/client-message-id.d.ts +3 -0
  37. package/dist/server/server/client-message-id.d.ts.map +1 -0
  38. package/dist/server/server/client-message-id.js +12 -0
  39. package/dist/server/server/client-message-id.js.map +1 -0
  40. package/dist/server/server/persisted-config.d.ts +8 -8
  41. package/dist/server/server/persistence-hooks.js +1 -1
  42. package/dist/server/server/persistence-hooks.js.map +1 -1
  43. package/dist/server/server/relay-transport.d.ts.map +1 -1
  44. package/dist/server/server/relay-transport.js +27 -28
  45. package/dist/server/server/relay-transport.js.map +1 -1
  46. package/dist/server/server/session.d.ts +4 -2
  47. package/dist/server/server/session.d.ts.map +1 -1
  48. package/dist/server/server/session.js +122 -31
  49. package/dist/server/server/session.js.map +1 -1
  50. package/dist/server/server/websocket-server.d.ts +8 -4
  51. package/dist/server/server/websocket-server.d.ts.map +1 -1
  52. package/dist/server/server/websocket-server.js +272 -75
  53. package/dist/server/server/websocket-server.js.map +1 -1
  54. package/dist/server/shared/daemon-endpoints.d.ts +9 -1
  55. package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
  56. package/dist/server/shared/daemon-endpoints.js +18 -3
  57. package/dist/server/shared/daemon-endpoints.js.map +1 -1
  58. package/dist/server/shared/messages.d.ts +2065 -313
  59. package/dist/server/shared/messages.d.ts.map +1 -1
  60. package/dist/server/shared/messages.js +40 -1
  61. package/dist/server/shared/messages.js.map +1 -1
  62. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  63. package/dist/server/shared/tool-call-display.js +4 -0
  64. package/dist/server/shared/tool-call-display.js.map +1 -1
  65. package/package.json +3 -3
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { query, } from "@anthropic-ai/claude-agent-sdk";
8
8
  import { mapClaudeCanceledToolCall, mapClaudeCompletedToolCall, mapClaudeFailedToolCall, mapClaudeRunningToolCall, } from "./claude/tool-call-mapper.js";
9
+ import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
9
10
  import { applyProviderEnv, isProviderCommandAvailable, } from "../provider-launch-config.js";
10
11
  import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
11
12
  const fsPromises = promises;
@@ -118,6 +119,7 @@ const REWIND_COMMAND = {
118
119
  description: "Rewind tracked files to a previous user message",
119
120
  argumentHint: "[user_message_uuid]",
120
121
  };
122
+ const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
121
123
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
122
124
  function resolveClaudeBinary() {
123
125
  try {
@@ -150,6 +152,148 @@ function resolveClaudeSpawnCommand(spawnOptions, runtimeSettings) {
150
152
  args: [...commandConfig.argv.slice(1), ...spawnOptions.args],
151
153
  };
152
154
  }
155
+ function applyRuntimeSettingsToClaudeOptions(options, runtimeSettings) {
156
+ const hasEnvOverrides = Object.keys(runtimeSettings?.env ?? {}).length > 0;
157
+ const commandMode = runtimeSettings?.command?.mode;
158
+ const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
159
+ if (!needsCustomSpawn) {
160
+ return options;
161
+ }
162
+ return {
163
+ ...options,
164
+ spawnClaudeCodeProcess: (spawnOptions) => {
165
+ const resolved = resolveClaudeSpawnCommand(spawnOptions, runtimeSettings);
166
+ return spawn(resolved.command, resolved.args, {
167
+ cwd: spawnOptions.cwd,
168
+ env: applyProviderEnv(spawnOptions.env, runtimeSettings),
169
+ signal: spawnOptions.signal,
170
+ stdio: ["pipe", "pipe", "pipe"],
171
+ });
172
+ },
173
+ };
174
+ }
175
+ function summarizeClaudeOptionsForLog(options) {
176
+ const systemPromptRaw = options.systemPrompt;
177
+ const systemPromptSummary = (() => {
178
+ if (!systemPromptRaw) {
179
+ return { mode: "none", preset: null };
180
+ }
181
+ if (typeof systemPromptRaw === "string") {
182
+ return { mode: "string", preset: null };
183
+ }
184
+ const prompt = systemPromptRaw;
185
+ const promptType = typeof prompt.type === "string" ? prompt.type : "custom";
186
+ return {
187
+ mode: promptType === "preset"
188
+ ? "preset"
189
+ : "custom",
190
+ preset: typeof prompt.preset === "string" && prompt.preset.length > 0
191
+ ? prompt.preset
192
+ : null,
193
+ };
194
+ })();
195
+ const mcpServerNames = options.mcpServers
196
+ ? Object.keys(options.mcpServers).sort()
197
+ : [];
198
+ return {
199
+ cwd: typeof options.cwd === "string" ? options.cwd : null,
200
+ permissionMode: typeof options.permissionMode === "string"
201
+ ? options.permissionMode
202
+ : null,
203
+ model: typeof options.model === "string" ? options.model : null,
204
+ includePartialMessages: options.includePartialMessages === true,
205
+ settingSources: Array.isArray(options.settingSources)
206
+ ? options.settingSources
207
+ : [],
208
+ enableFileCheckpointing: options.enableFileCheckpointing === true,
209
+ hasResume: typeof options.resume === "string" && options.resume.length > 0,
210
+ maxThinkingTokens: typeof options.maxThinkingTokens === "number"
211
+ ? options.maxThinkingTokens
212
+ : null,
213
+ hasEnv: !!options.env,
214
+ envKeyCount: Object.keys(options.env ?? {}).length,
215
+ hasMcpServers: mcpServerNames.length > 0,
216
+ mcpServerNames,
217
+ systemPromptMode: systemPromptSummary.mode,
218
+ systemPromptPreset: systemPromptSummary.preset,
219
+ hasCanUseTool: typeof options.canUseTool === "function",
220
+ hasSpawnOverride: typeof options.spawnClaudeCodeProcess === "function",
221
+ hasStderrHandler: typeof options.stderr === "function",
222
+ };
223
+ }
224
+ function isToolResultTextBlock(value) {
225
+ return (!!value &&
226
+ typeof value === "object" &&
227
+ value.type === "text" &&
228
+ typeof value.text === "string");
229
+ }
230
+ function normalizeForDeterministicString(value, seen) {
231
+ if (value === null ||
232
+ typeof value === "string" ||
233
+ typeof value === "number" ||
234
+ typeof value === "boolean") {
235
+ return value;
236
+ }
237
+ if (typeof value === "bigint") {
238
+ return value.toString();
239
+ }
240
+ if (typeof value === "function") {
241
+ return "[function]";
242
+ }
243
+ if (typeof value === "symbol") {
244
+ return value.toString();
245
+ }
246
+ if (typeof value === "undefined") {
247
+ return "[undefined]";
248
+ }
249
+ if (Array.isArray(value)) {
250
+ return value.map((entry) => normalizeForDeterministicString(entry, seen));
251
+ }
252
+ if (typeof value === "object") {
253
+ const objectValue = value;
254
+ if (seen.has(objectValue)) {
255
+ return "[circular]";
256
+ }
257
+ seen.add(objectValue);
258
+ const record = value;
259
+ const normalized = {};
260
+ for (const key of Object.keys(record).sort()) {
261
+ normalized[key] = normalizeForDeterministicString(record[key], seen);
262
+ }
263
+ seen.delete(objectValue);
264
+ return normalized;
265
+ }
266
+ return String(value);
267
+ }
268
+ function deterministicStringify(value) {
269
+ if (typeof value === "undefined") {
270
+ return "";
271
+ }
272
+ try {
273
+ const normalized = normalizeForDeterministicString(value, new WeakSet());
274
+ if (typeof normalized === "string") {
275
+ return normalized;
276
+ }
277
+ return JSON.stringify(normalized);
278
+ }
279
+ catch {
280
+ try {
281
+ return String(value);
282
+ }
283
+ catch {
284
+ return "[unserializable]";
285
+ }
286
+ }
287
+ }
288
+ function coerceToolResultContentToString(content) {
289
+ if (typeof content === "string") {
290
+ return content;
291
+ }
292
+ if (Array.isArray(content) && content.every((block) => isToolResultTextBlock(block))) {
293
+ return content.map((block) => block.text).join("");
294
+ }
295
+ return deterministicStringify(content);
296
+ }
153
297
  export function extractUserMessageText(content) {
154
298
  if (typeof content === "string") {
155
299
  const normalized = content.trim();
@@ -179,10 +323,18 @@ export function extractUserMessageText(content) {
179
323
  const combined = parts.join("\n\n").trim();
180
324
  return combined.length > 0 ? combined : null;
181
325
  }
182
- const DEFAULT_PERMISSION_TIMEOUT_MS = 120000;
326
+ const MAX_SUB_AGENT_LOG_ENTRIES = 200;
327
+ const MAX_SUB_AGENT_SUMMARY_CHARS = 160;
183
328
  function isMetadata(value) {
184
329
  return typeof value === "object" && value !== null;
185
330
  }
331
+ function readTrimmedString(value) {
332
+ if (typeof value !== "string") {
333
+ return undefined;
334
+ }
335
+ const trimmed = value.trim();
336
+ return trimmed.length > 0 ? trimmed : undefined;
337
+ }
186
338
  function isMcpServerConfig(value) {
187
339
  if (!isMetadata(value)) {
188
340
  return false;
@@ -312,6 +464,448 @@ function resolvePermissionKind(toolName, input) {
312
464
  }
313
465
  return "tool";
314
466
  }
467
+ const ACTIVE_RUN_STATES = new Set([
468
+ "queued",
469
+ "awaiting_response",
470
+ "streaming",
471
+ "finalizing",
472
+ ]);
473
+ class RunTracker {
474
+ constructor() {
475
+ this.runs = new Map();
476
+ this.runByTaskId = new Map();
477
+ this.runByParentMessageId = new Map();
478
+ this.runByMessageId = new Map();
479
+ }
480
+ createRun(input) {
481
+ const run = {
482
+ id: input.id,
483
+ owner: input.owner,
484
+ queue: input.queue,
485
+ state: "queued",
486
+ promptReplaySeen: input.promptReplaySeen ?? true,
487
+ taskIds: new Set(),
488
+ parentMessageIds: new Set(),
489
+ messageIds: new Set(),
490
+ };
491
+ this.runs.set(run.id, run);
492
+ return run;
493
+ }
494
+ getRun(runId) {
495
+ return this.runs.get(runId) ?? null;
496
+ }
497
+ getForegroundRun() {
498
+ for (const run of this.runs.values()) {
499
+ if (run.owner === "foreground" && this.isActive(run.state)) {
500
+ return run;
501
+ }
502
+ }
503
+ return null;
504
+ }
505
+ listActiveRuns(owner) {
506
+ const runs = [];
507
+ for (const run of this.runs.values()) {
508
+ if (!this.isActive(run.state)) {
509
+ continue;
510
+ }
511
+ if (owner && run.owner !== owner) {
512
+ continue;
513
+ }
514
+ runs.push(run);
515
+ }
516
+ return runs;
517
+ }
518
+ hasActiveRuns(owner) {
519
+ for (const run of this.runs.values()) {
520
+ if (!this.isActive(run.state)) {
521
+ continue;
522
+ }
523
+ if (owner && run.owner !== owner) {
524
+ continue;
525
+ }
526
+ return true;
527
+ }
528
+ return false;
529
+ }
530
+ getLatestActiveRun(owner) {
531
+ let latest = null;
532
+ for (const run of this.runs.values()) {
533
+ if (!this.isActive(run.state)) {
534
+ continue;
535
+ }
536
+ if (owner && run.owner !== owner) {
537
+ continue;
538
+ }
539
+ latest = run;
540
+ }
541
+ return latest;
542
+ }
543
+ isRunActive(run) {
544
+ if (!run) {
545
+ return false;
546
+ }
547
+ return this.isActive(run.state);
548
+ }
549
+ resolveByIdentifiers(identifiers) {
550
+ if (identifiers.taskId) {
551
+ const run = this.resolveMappedRun(this.runByTaskId, identifiers.taskId);
552
+ if (run) {
553
+ return { run, reason: "task_id" };
554
+ }
555
+ }
556
+ if (identifiers.parentMessageId) {
557
+ const run = this.resolveMappedRun(this.runByParentMessageId, identifiers.parentMessageId);
558
+ if (run) {
559
+ return { run, reason: "parent_message_id" };
560
+ }
561
+ }
562
+ if (identifiers.messageId) {
563
+ const run = this.resolveMappedRun(this.runByMessageId, identifiers.messageId);
564
+ if (run) {
565
+ return { run, reason: "message_id" };
566
+ }
567
+ }
568
+ return { run: null, reason: "metadata" };
569
+ }
570
+ bindIdentifiers(run, identifiers) {
571
+ if (identifiers.taskId) {
572
+ run.taskIds.add(identifiers.taskId);
573
+ this.runByTaskId.set(identifiers.taskId, run.id);
574
+ }
575
+ if (identifiers.parentMessageId) {
576
+ run.parentMessageIds.add(identifiers.parentMessageId);
577
+ this.runByParentMessageId.set(identifiers.parentMessageId, run.id);
578
+ }
579
+ if (identifiers.messageId) {
580
+ run.messageIds.add(identifiers.messageId);
581
+ this.runByMessageId.set(identifiers.messageId, run.id);
582
+ }
583
+ }
584
+ transition(run, nextState) {
585
+ run.state = nextState;
586
+ }
587
+ complete(run, terminalState) {
588
+ run.state = terminalState;
589
+ this.clearRunIndex(run);
590
+ }
591
+ deriveLifecycle(pendingPermissionCount) {
592
+ for (const run of this.runs.values()) {
593
+ if (this.isActive(run.state)) {
594
+ return "running";
595
+ }
596
+ }
597
+ if (pendingPermissionCount > 0) {
598
+ return "permission";
599
+ }
600
+ for (const run of this.runs.values()) {
601
+ if (run.state === "error") {
602
+ return "error";
603
+ }
604
+ }
605
+ return "idle";
606
+ }
607
+ resolveMappedRun(mapping, identifier) {
608
+ const runId = mapping.get(identifier);
609
+ if (!runId) {
610
+ return null;
611
+ }
612
+ const run = this.runs.get(runId);
613
+ if (!run || !this.isActive(run.state)) {
614
+ mapping.delete(identifier);
615
+ return null;
616
+ }
617
+ return run;
618
+ }
619
+ clearRunIndex(run) {
620
+ for (const taskId of run.taskIds) {
621
+ this.runByTaskId.delete(taskId);
622
+ }
623
+ for (const parentMessageId of run.parentMessageIds) {
624
+ this.runByParentMessageId.delete(parentMessageId);
625
+ }
626
+ for (const messageId of run.messageIds) {
627
+ this.runByMessageId.delete(messageId);
628
+ }
629
+ run.taskIds.clear();
630
+ run.parentMessageIds.clear();
631
+ run.messageIds.clear();
632
+ }
633
+ isActive(state) {
634
+ return ACTIVE_RUN_STATES.has(state);
635
+ }
636
+ }
637
+ class TimelineAssembler {
638
+ constructor() {
639
+ this.messages = new Map();
640
+ this.activeMessageByRun = new Map();
641
+ this.syntheticMessageCounter = 0;
642
+ }
643
+ consume(input) {
644
+ if (input.message.type === "assistant") {
645
+ return this.consumeAssistantMessage(input.message, input.runId, input.messageIdHint ?? null);
646
+ }
647
+ if (input.message.type === "stream_event") {
648
+ return this.consumeStreamEvent(input.message, input.runId, input.messageIdHint ?? null);
649
+ }
650
+ return [];
651
+ }
652
+ consumeAssistantMessage(message, runId, messageIdHint) {
653
+ const messageId = this.readMessageIdFromAssistantMessage(message) ??
654
+ messageIdHint ??
655
+ this.resolveMessageId({ runId, createIfMissing: true, messageId: null });
656
+ if (!messageId) {
657
+ return [];
658
+ }
659
+ const state = this.ensureMessageState(messageId, runId);
660
+ const fragments = this.extractFragments(message.message?.content);
661
+ return this.applyAbsoluteFragments(state, fragments);
662
+ }
663
+ consumeStreamEvent(message, runId, messageIdHint) {
664
+ const event = message.event;
665
+ const eventType = readTrimmedString(event.type);
666
+ const streamEventMessageId = this.readMessageIdFromStreamEvent(event) ?? messageIdHint;
667
+ if (eventType === "message_start") {
668
+ const messageId = this.resolveMessageId({
669
+ runId,
670
+ createIfMissing: true,
671
+ messageId: streamEventMessageId,
672
+ });
673
+ if (!messageId) {
674
+ return [];
675
+ }
676
+ this.ensureMessageState(messageId, runId);
677
+ return [];
678
+ }
679
+ if (eventType === "message_stop") {
680
+ const messageId = this.resolveMessageId({
681
+ runId,
682
+ createIfMissing: false,
683
+ messageId: streamEventMessageId,
684
+ });
685
+ if (!messageId) {
686
+ return [];
687
+ }
688
+ return this.finalizeMessage(messageId, runId);
689
+ }
690
+ if (eventType === "content_block_start") {
691
+ return this.consumeDeltaContent(event.content_block, runId, streamEventMessageId);
692
+ }
693
+ if (eventType === "content_block_delta") {
694
+ return this.consumeDeltaContent(event.delta, runId, streamEventMessageId);
695
+ }
696
+ return [];
697
+ }
698
+ consumeDeltaContent(content, runId, messageIdHint) {
699
+ const fragments = this.extractFragments(content);
700
+ if (fragments.length === 0) {
701
+ return [];
702
+ }
703
+ const messageId = this.resolveMessageId({
704
+ runId,
705
+ createIfMissing: true,
706
+ messageId: messageIdHint,
707
+ });
708
+ if (!messageId) {
709
+ return [];
710
+ }
711
+ const state = this.ensureMessageState(messageId, runId);
712
+ return this.appendFragments(state, fragments);
713
+ }
714
+ appendFragments(state, fragments) {
715
+ for (const fragment of fragments) {
716
+ if (fragment.kind === "assistant") {
717
+ state.assistantText += fragment.text;
718
+ }
719
+ else {
720
+ state.reasoningText += fragment.text;
721
+ }
722
+ }
723
+ return this.emitNewContent(state);
724
+ }
725
+ applyAbsoluteFragments(state, fragments) {
726
+ const assistantText = fragments
727
+ .filter((fragment) => fragment.kind === "assistant")
728
+ .map((fragment) => fragment.text)
729
+ .join("");
730
+ const reasoningText = fragments
731
+ .filter((fragment) => fragment.kind === "reasoning")
732
+ .map((fragment) => fragment.text)
733
+ .join("");
734
+ if (assistantText.length > 0) {
735
+ if (!assistantText.startsWith(state.assistantText)) {
736
+ state.emittedAssistantLength = 0;
737
+ }
738
+ state.assistantText = assistantText;
739
+ }
740
+ if (reasoningText.length > 0) {
741
+ if (!reasoningText.startsWith(state.reasoningText)) {
742
+ state.emittedReasoningLength = 0;
743
+ }
744
+ state.reasoningText = reasoningText;
745
+ }
746
+ return this.emitNewContent(state);
747
+ }
748
+ finalizeMessage(messageId, runId) {
749
+ const state = this.messages.get(messageId);
750
+ if (!state) {
751
+ return [];
752
+ }
753
+ state.stopped = true;
754
+ const items = this.emitNewContent(state);
755
+ if (runId && this.activeMessageByRun.get(runId) === messageId) {
756
+ this.activeMessageByRun.delete(runId);
757
+ }
758
+ return items;
759
+ }
760
+ emitNewContent(state) {
761
+ const items = [];
762
+ const nextAssistantText = state.assistantText.slice(state.emittedAssistantLength);
763
+ if (nextAssistantText.length > 0 &&
764
+ nextAssistantText !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
765
+ state.emittedAssistantLength = state.assistantText.length;
766
+ items.push({ type: "assistant_message", text: nextAssistantText });
767
+ }
768
+ const nextReasoningText = state.reasoningText.slice(state.emittedReasoningLength);
769
+ if (nextReasoningText.length > 0) {
770
+ state.emittedReasoningLength = state.reasoningText.length;
771
+ items.push({ type: "reasoning", text: nextReasoningText });
772
+ }
773
+ return items;
774
+ }
775
+ ensureMessageState(messageId, runId) {
776
+ const existing = this.messages.get(messageId);
777
+ if (existing) {
778
+ existing.stopped = false;
779
+ if (runId) {
780
+ this.activeMessageByRun.set(runId, messageId);
781
+ }
782
+ return existing;
783
+ }
784
+ const created = {
785
+ id: messageId,
786
+ assistantText: "",
787
+ reasoningText: "",
788
+ emittedAssistantLength: 0,
789
+ emittedReasoningLength: 0,
790
+ stopped: false,
791
+ };
792
+ this.messages.set(messageId, created);
793
+ if (runId) {
794
+ this.activeMessageByRun.set(runId, messageId);
795
+ }
796
+ return created;
797
+ }
798
+ resolveMessageId(input) {
799
+ if (input.messageId) {
800
+ return input.messageId;
801
+ }
802
+ if (input.runId) {
803
+ const active = this.activeMessageByRun.get(input.runId);
804
+ if (active) {
805
+ return active;
806
+ }
807
+ }
808
+ if (!input.createIfMissing) {
809
+ return null;
810
+ }
811
+ const synthetic = `synthetic-message-${++this.syntheticMessageCounter}`;
812
+ if (input.runId) {
813
+ this.activeMessageByRun.set(input.runId, synthetic);
814
+ }
815
+ return synthetic;
816
+ }
817
+ extractFragments(content) {
818
+ if (typeof content === "string") {
819
+ if (content.length === 0) {
820
+ return [];
821
+ }
822
+ return [{ kind: "assistant", text: content }];
823
+ }
824
+ const blocks = Array.isArray(content) ? content : [content];
825
+ const fragments = [];
826
+ for (const rawBlock of blocks) {
827
+ if (!isClaudeContentChunk(rawBlock)) {
828
+ continue;
829
+ }
830
+ if ((rawBlock.type === "text" || rawBlock.type === "text_delta") &&
831
+ typeof rawBlock.text === "string" &&
832
+ rawBlock.text.length > 0) {
833
+ fragments.push({ kind: "assistant", text: rawBlock.text });
834
+ }
835
+ if ((rawBlock.type === "thinking" || rawBlock.type === "thinking_delta") &&
836
+ typeof rawBlock.thinking === "string" &&
837
+ rawBlock.thinking.length > 0) {
838
+ fragments.push({ kind: "reasoning", text: rawBlock.thinking });
839
+ }
840
+ }
841
+ return fragments;
842
+ }
843
+ readMessageIdFromAssistantMessage(message) {
844
+ const candidate = message;
845
+ return readTrimmedString(candidate.message_id) ??
846
+ readTrimmedString(candidate.message?.id) ??
847
+ null;
848
+ }
849
+ readMessageIdFromStreamEvent(event) {
850
+ const message = event.message;
851
+ return (readTrimmedString(event.message_id) ??
852
+ readTrimmedString(message?.id) ??
853
+ null);
854
+ }
855
+ }
856
+ function isMetadataOnlySdkMessage(message) {
857
+ if (message.type === "system") {
858
+ return true;
859
+ }
860
+ if (message.type !== "user") {
861
+ return false;
862
+ }
863
+ return isTaskNotificationUserContent(message.message?.content);
864
+ }
865
+ export function readEventIdentifiers(message) {
866
+ const root = message;
867
+ const messageType = readTrimmedString(root.type);
868
+ const streamEvent = root.event;
869
+ const streamEventMessage = streamEvent?.message;
870
+ const messageContainer = root.message;
871
+ return {
872
+ taskId: readTrimmedString(root.task_id) ??
873
+ readTrimmedString(streamEvent?.task_id) ??
874
+ readTrimmedString(streamEventMessage?.task_id) ??
875
+ readTrimmedString(messageContainer?.task_id) ??
876
+ null,
877
+ parentMessageId: readTrimmedString(root.parent_message_id) ??
878
+ readTrimmedString(streamEvent?.parent_message_id) ??
879
+ readTrimmedString(streamEventMessage?.parent_message_id) ??
880
+ readTrimmedString(messageContainer?.parent_message_id) ??
881
+ null,
882
+ messageId: readTrimmedString(root.message_id) ??
883
+ readTrimmedString(streamEvent?.message_id) ??
884
+ readTrimmedString(streamEventMessage?.id) ??
885
+ readTrimmedString(streamEventMessage?.message_id) ??
886
+ readTrimmedString(messageContainer?.id) ??
887
+ readTrimmedString(messageContainer?.message_id) ??
888
+ (messageType === "user" ? readTrimmedString(root.uuid) : null) ??
889
+ null,
890
+ };
891
+ }
892
+ function isTaskNotificationUserContent(content) {
893
+ if (typeof content === "string") {
894
+ return content.includes("<task-notification>");
895
+ }
896
+ if (!Array.isArray(content)) {
897
+ return false;
898
+ }
899
+ for (const block of content) {
900
+ if (block &&
901
+ typeof block === "object" &&
902
+ typeof block.text === "string" &&
903
+ block.text.includes("<task-notification>")) {
904
+ return true;
905
+ }
906
+ }
907
+ return false;
908
+ }
315
909
  export class ClaudeAgentClient {
316
910
  constructor(options) {
317
911
  this.provider = "claude";
@@ -327,24 +921,7 @@ export class ClaudeAgentClient {
327
921
  }
328
922
  }
329
923
  applyRuntimeSettings(options) {
330
- const hasEnvOverrides = Object.keys(this.runtimeSettings?.env ?? {}).length > 0;
331
- const commandMode = this.runtimeSettings?.command?.mode;
332
- const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
333
- if (!needsCustomSpawn) {
334
- return options;
335
- }
336
- return {
337
- ...options,
338
- spawnClaudeCodeProcess: (spawnOptions) => {
339
- const resolved = resolveClaudeSpawnCommand(spawnOptions, this.runtimeSettings);
340
- return spawn(resolved.command, resolved.args, {
341
- cwd: spawnOptions.cwd,
342
- env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
343
- signal: spawnOptions.signal,
344
- stdio: ["pipe", "pipe", "pipe"],
345
- });
346
- },
347
- };
924
+ return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
348
925
  }
349
926
  async createSession(config) {
350
927
  const claudeConfig = this.assertConfig(config);
@@ -456,28 +1033,30 @@ class ClaudeAgentSession {
456
1033
  this.toolUseIndexToId = new Map();
457
1034
  this.toolUseInputBuffers = new Map();
458
1035
  this.pendingPermissions = new Map();
459
- this.eventQueue = null;
1036
+ this.activeForegroundTurn = null;
1037
+ this.liveEventQueue = new Pushable();
1038
+ this.runTracker = new RunTracker();
1039
+ this.timelineAssembler = new TimelineAssembler();
460
1040
  this.persistedHistory = [];
461
1041
  this.historyPending = false;
462
- this.turnCancelRequested = false;
463
- // NOTE: streamedAssistantTextThisTurn and streamedReasoningThisTurn were removed
464
- // These flags are now tracked per-turn via TurnContext to prevent race conditions
465
- // when multiple stream() calls overlap (e.g., interrupt + new message)
1042
+ this.turnState = "idle";
1043
+ this.preReplayMetadataSeen = false;
1044
+ this.pendingAutonomousWakeReservations = 0;
1045
+ this.nextRunOrdinal = 1;
466
1046
  this.cancelCurrentTurn = null;
467
- // Track the pending interrupt promise so we can await it in processPrompt
468
- // This ensures the interrupt's response is consumed before we call query.next()
469
1047
  this.pendingInterruptPromise = null;
470
- // Track the current turn ID and active turn promise to serialize concurrent stream() calls
471
- // and prevent race conditions where two processPrompt() loops run against the same query
472
- this.currentTurnId = 0;
473
1048
  this.activeTurnPromise = null;
474
1049
  this.cachedRuntimeInfo = null;
475
1050
  this.lastOptionsModel = null;
476
1051
  this.selectableModelIds = null;
477
1052
  this.activeSidechains = new Map();
478
1053
  this.compacting = false;
1054
+ this.queryPumpPromise = null;
479
1055
  this.queryRestartNeeded = false;
480
1056
  this.userMessageIds = [];
1057
+ this.localUserMessageIds = new Set();
1058
+ this.suppressLocalReplayActivity = false;
1059
+ this.closed = false;
481
1060
  this.handlePermissionRequest = async (toolName, input, options) => {
482
1061
  const requestId = `permission-${randomUUID()}`;
483
1062
  const kind = resolvePermissionKind(toolName, input);
@@ -520,19 +1099,6 @@ class ClaudeAgentSession {
520
1099
  }
521
1100
  }
522
1101
  };
523
- const timeout = setTimeout(() => {
524
- this.pendingPermissions.delete(requestId);
525
- cleanup();
526
- const error = new Error("Permission request timed out");
527
- this.pushEvent({
528
- type: "permission_resolved",
529
- provider: "claude",
530
- requestId,
531
- resolution: { behavior: "deny", message: "timeout" },
532
- });
533
- reject(error);
534
- }, DEFAULT_PERMISSION_TIMEOUT_MS);
535
- cleanupFns.push(() => clearTimeout(timeout));
536
1102
  const abortHandler = () => {
537
1103
  this.pendingPermissions.delete(requestId);
538
1104
  cleanup();
@@ -640,81 +1206,124 @@ class ClaudeAgentSession {
640
1206
  }
641
1207
  async *stream(prompt, options) {
642
1208
  void options;
643
- // Increment turn ID to invalidate any in-flight processPrompt() loops from previous turns.
644
- // This prevents race conditions where an interrupted turn's events get mixed with the new turn.
645
- const turnId = ++this.currentTurnId;
646
- // Cancel the previous turn if one exists. The caller of interrupt() is responsible
647
- // for awaiting completion - the new turn just signals cancellation and proceeds.
648
1209
  if (this.cancelCurrentTurn) {
649
1210
  this.cancelCurrentTurn();
650
1211
  }
651
- // Reset cancel flag at the start of each turn to prevent stale state from previous turns
652
- this.turnCancelRequested = false;
1212
+ this.suppressLocalReplayActivity = false;
1213
+ this.pendingAutonomousWakeReservations = 0;
653
1214
  const slashCommand = this.resolveSlashCommandInvocation(prompt);
654
1215
  if (slashCommand?.commandName === REWIND_COMMAND_NAME) {
655
1216
  yield* this.streamRewindCommand(slashCommand);
656
1217
  return;
657
1218
  }
1219
+ await this.awaitPendingInterruptPromise();
1220
+ if (this.turnState === "autonomous" &&
1221
+ this.runTracker.hasActiveRuns("autonomous")) {
1222
+ await this.transitionAutonomousToForeground();
1223
+ }
658
1224
  const sdkMessage = this.toSdkUserMessage(prompt);
659
1225
  const queue = new Pushable();
660
- this.eventQueue = queue;
1226
+ const run = this.createRun("foreground", queue);
1227
+ this.runTracker.bindIdentifiers(run, {
1228
+ taskId: null,
1229
+ parentMessageId: null,
1230
+ messageId: typeof sdkMessage.uuid === "string" ? sdkMessage.uuid : null,
1231
+ });
1232
+ const foregroundTurn = {
1233
+ runId: run.id,
1234
+ queue,
1235
+ };
1236
+ this.activeForegroundTurn = foregroundTurn;
1237
+ this.preReplayMetadataSeen = false;
1238
+ this.transitionTurnState("foreground", "foreground stream started");
661
1239
  let finishedNaturally = false;
662
1240
  let cancelIssued = false;
1241
+ let queueDrainedWithoutTerminal = false;
1242
+ const turnPromise = Promise.resolve();
1243
+ this.activeTurnPromise = turnPromise;
663
1244
  const requestCancel = () => {
664
1245
  if (cancelIssued) {
665
1246
  return;
666
1247
  }
667
1248
  cancelIssued = true;
668
- this.turnCancelRequested = true;
669
- // Store the interrupt promise so processPrompt can await it before calling query.next()
670
- this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
671
- this.logger.warn({ err: error }, "Failed to interrupt during cancel");
672
- });
673
- this.flushPendingToolCalls();
674
- // Push turn_canceled before ending the queue so consumers get proper lifecycle signals
675
- queue.push({
1249
+ if (this.activeForegroundTurn?.runId === run.id) {
1250
+ this.activeForegroundTurn = null;
1251
+ }
1252
+ if (this.cancelCurrentTurn === requestCancel) {
1253
+ this.cancelCurrentTurn = null;
1254
+ }
1255
+ this.rejectAllPendingPermissions(new Error("Permission request aborted"));
1256
+ this.cancelRun(run, {
676
1257
  type: "turn_canceled",
677
1258
  provider: "claude",
678
1259
  reason: "Interrupted",
679
1260
  });
680
- queue.end();
1261
+ this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
1262
+ this.logger.warn({ err: error }, "Failed to interrupt during cancel");
1263
+ });
681
1264
  };
682
1265
  this.cancelCurrentTurn = requestCancel;
683
- // Start forwarding events and track the promise so future turns can wait for completion
684
- const forwardPromise = this.forwardPromptEvents(sdkMessage, queue, turnId);
685
- this.activeTurnPromise = forwardPromise;
686
- forwardPromise.catch((error) => {
687
- this.logger.error({ err: error }, "Unexpected error in forwardPromptEvents");
688
- });
1266
+ try {
1267
+ await this.ensureQuery();
1268
+ if (!this.input) {
1269
+ throw new Error("Claude session input stream not initialized");
1270
+ }
1271
+ this.startQueryPump();
1272
+ this.input.push(sdkMessage);
1273
+ }
1274
+ catch (error) {
1275
+ this.failRun(run, error instanceof Error ? error.message : "Claude stream failed");
1276
+ finishedNaturally = true;
1277
+ }
689
1278
  try {
690
1279
  for await (const event of queue) {
691
- yield event;
692
- if (event.type === "turn_completed" ||
1280
+ const isTerminalEvent = event.type === "turn_completed" ||
693
1281
  event.type === "turn_failed" ||
694
- event.type === "turn_canceled") {
1282
+ event.type === "turn_canceled";
1283
+ if (isTerminalEvent) {
695
1284
  finishedNaturally = true;
1285
+ }
1286
+ yield event;
1287
+ if (isTerminalEvent) {
696
1288
  break;
697
1289
  }
698
1290
  }
1291
+ if (!finishedNaturally && !cancelIssued) {
1292
+ queueDrainedWithoutTerminal = true;
1293
+ }
699
1294
  }
700
1295
  finally {
701
- if (!finishedNaturally && !cancelIssued) {
1296
+ if (!finishedNaturally && !cancelIssued && !queueDrainedWithoutTerminal) {
702
1297
  requestCancel();
703
1298
  }
704
- if (this.eventQueue === queue) {
705
- this.eventQueue = null;
1299
+ if (this.activeForegroundTurn === foregroundTurn) {
1300
+ this.activeForegroundTurn = null;
706
1301
  }
707
1302
  if (this.cancelCurrentTurn === requestCancel) {
708
1303
  this.cancelCurrentTurn = null;
709
1304
  }
710
- // Clear the active turn promise if it's still ours
711
- if (this.activeTurnPromise === forwardPromise) {
1305
+ if (this.activeTurnPromise === turnPromise) {
712
1306
  this.activeTurnPromise = null;
713
1307
  }
714
1308
  }
715
1309
  }
716
1310
  async interrupt() {
717
- this.cancelCurrentTurn?.();
1311
+ if (this.cancelCurrentTurn) {
1312
+ this.cancelCurrentTurn();
1313
+ return;
1314
+ }
1315
+ const autonomousRuns = this.runTracker.listActiveRuns("autonomous");
1316
+ if (autonomousRuns.length > 0) {
1317
+ this.flushPendingToolCalls();
1318
+ for (const run of autonomousRuns) {
1319
+ this.emitRunEvent(run, {
1320
+ type: "turn_canceled",
1321
+ provider: "claude",
1322
+ reason: "Interrupted",
1323
+ });
1324
+ }
1325
+ }
1326
+ await this.interruptActiveTurn();
718
1327
  }
719
1328
  async *streamHistory() {
720
1329
  if (!this.historyPending || this.persistedHistory.length === 0) {
@@ -727,6 +1336,14 @@ class ClaudeAgentSession {
727
1336
  yield { type: "timeline", item, provider: "claude" };
728
1337
  }
729
1338
  }
1339
+ async *streamLiveEvents() {
1340
+ if (this.claudeSessionId) {
1341
+ this.startQueryPump();
1342
+ }
1343
+ for await (const event of this.liveEventQueue) {
1344
+ yield event;
1345
+ }
1346
+ }
730
1347
  async getAvailableModes() {
731
1348
  return this.availableModes;
732
1349
  }
@@ -841,10 +1458,20 @@ class ClaudeAgentSession {
841
1458
  return this.persistence;
842
1459
  }
843
1460
  async close() {
1461
+ this.closed = true;
844
1462
  this.rejectAllPendingPermissions(new Error("Claude session closed"));
1463
+ this.cancelCurrentTurn?.();
1464
+ this.activeForegroundTurn?.queue.end();
1465
+ this.activeForegroundTurn = null;
1466
+ this.cancelCurrentTurn = null;
1467
+ this.turnState = "idle";
1468
+ this.suppressLocalReplayActivity = false;
1469
+ this.pendingAutonomousWakeReservations = 0;
1470
+ this.liveEventQueue.end();
1471
+ this.activeTurnPromise = null;
845
1472
  this.input?.end();
846
- await this.query?.interrupt?.();
847
- await this.query?.return?.();
1473
+ await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
1474
+ await this.awaitWithTimeout(this.query?.return?.(), "close query return");
848
1475
  this.query = null;
849
1476
  this.input = null;
850
1477
  }
@@ -898,11 +1525,6 @@ class ClaudeAgentSession {
898
1525
  }
899
1526
  async *streamRewindCommand(invocation) {
900
1527
  yield { type: "turn_started", provider: "claude" };
901
- yield {
902
- type: "timeline",
903
- provider: "claude",
904
- item: { type: "user_message", text: invocation.rawInput },
905
- };
906
1528
  try {
907
1529
  const rewindAttempt = await this.attemptRewind(invocation.args);
908
1530
  if (!rewindAttempt.messageId || !rewindAttempt.result) {
@@ -1116,13 +1738,30 @@ class ClaudeAgentSession {
1116
1738
  }
1117
1739
  const input = new Pushable();
1118
1740
  const options = this.buildOptions();
1119
- this.logger.debug({ options }, "claude query");
1741
+ this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query");
1120
1742
  this.input = input;
1121
1743
  this.query = query({ prompt: input, options });
1122
- await this.primeSelectableModelIds(this.query);
1123
- await this.query.setPermissionMode(this.currentMode);
1744
+ // Do not block query readiness on control-plane calls. We need `next()` to
1745
+ // start immediately so autonomous wake events are not missed between turns.
1746
+ void this.awaitWithTimeout(this.primeSelectableModelIds(this.query), "prime selectable model ids");
1124
1747
  return this.query;
1125
1748
  }
1749
+ async awaitWithTimeout(promise, label) {
1750
+ if (!promise) {
1751
+ return;
1752
+ }
1753
+ try {
1754
+ await Promise.race([
1755
+ promise,
1756
+ new Promise((_, reject) => {
1757
+ setTimeout(() => reject(new Error("timeout")), 3000);
1758
+ }),
1759
+ ]);
1760
+ }
1761
+ catch (error) {
1762
+ this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
1763
+ }
1764
+ }
1126
1765
  buildOptions() {
1127
1766
  const configuredThinkingOptionId = this.config.thinkingOptionId;
1128
1767
  const thinkingOptionId = configuredThinkingOptionId && configuredThinkingOptionId !== "default"
@@ -1186,24 +1825,7 @@ class ClaudeAgentSession {
1186
1825
  return this.applyRuntimeSettings(base);
1187
1826
  }
1188
1827
  applyRuntimeSettings(options) {
1189
- const hasEnvOverrides = Object.keys(this.runtimeSettings?.env ?? {}).length > 0;
1190
- const commandMode = this.runtimeSettings?.command?.mode;
1191
- const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
1192
- if (!needsCustomSpawn) {
1193
- return options;
1194
- }
1195
- return {
1196
- ...options,
1197
- spawnClaudeCodeProcess: (spawnOptions) => {
1198
- const resolved = resolveClaudeSpawnCommand(spawnOptions, this.runtimeSettings);
1199
- return spawn(resolved.command, resolved.args, {
1200
- cwd: spawnOptions.cwd,
1201
- env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
1202
- signal: spawnOptions.signal,
1203
- stdio: ["pipe", "pipe", "pipe"],
1204
- });
1205
- },
1206
- };
1828
+ return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
1207
1829
  }
1208
1830
  normalizeMcpServers(servers) {
1209
1831
  const result = {};
@@ -1236,6 +1858,7 @@ class ClaudeAgentSession {
1236
1858
  }
1237
1859
  const messageId = randomUUID();
1238
1860
  this.rememberUserMessageId(messageId);
1861
+ this.localUserMessageIds.add(messageId);
1239
1862
  return {
1240
1863
  type: "user",
1241
1864
  message: {
@@ -1247,175 +1870,716 @@ class ClaudeAgentSession {
1247
1870
  session_id: this.claudeSessionId ?? "",
1248
1871
  };
1249
1872
  }
1250
- async *processPrompt(sdkMessage, turnId) {
1251
- // If there's a pending interrupt, await it BEFORE calling ensureQuery().
1252
- // interruptActiveTurn() clears this.query after interrupt() returns,
1253
- // so we must wait for it to complete before we try to get the query.
1254
- if (this.pendingInterruptPromise) {
1255
- await this.pendingInterruptPromise;
1256
- this.pendingInterruptPromise = null;
1873
+ async awaitPendingInterruptPromise() {
1874
+ if (!this.pendingInterruptPromise) {
1875
+ return;
1257
1876
  }
1258
- // Check if we were superseded while waiting for the interrupt
1259
- if (this.currentTurnId !== turnId) {
1877
+ await this.pendingInterruptPromise;
1878
+ this.pendingInterruptPromise = null;
1879
+ }
1880
+ createRun(owner, queue) {
1881
+ const runId = `${owner}-run-${this.nextRunOrdinal++}`;
1882
+ const run = this.runTracker.createRun({
1883
+ id: runId,
1884
+ owner,
1885
+ queue,
1886
+ promptReplaySeen: owner === "autonomous",
1887
+ });
1888
+ this.logger.debug({ runId, owner, state: run.state }, "Created Claude run");
1889
+ return run;
1890
+ }
1891
+ transitionTurnState(next, reason) {
1892
+ if (this.turnState === next) {
1260
1893
  return;
1261
1894
  }
1262
- const query = await this.ensureQuery();
1263
- if (!this.input) {
1264
- throw new Error("Claude session input stream not initialized");
1895
+ this.logger.debug({ from: this.turnState, to: next, reason }, "Claude turn state transition");
1896
+ this.turnState = next;
1897
+ }
1898
+ transitionTurnStateFromActiveRuns(reason) {
1899
+ if (this.runTracker.hasActiveRuns("foreground")) {
1900
+ this.transitionTurnState("foreground", reason);
1901
+ return;
1265
1902
  }
1266
- this.input.push(sdkMessage);
1267
- while (true) {
1268
- // Check if this turn has been superseded by a new one.
1269
- if (this.currentTurnId !== turnId) {
1270
- break;
1903
+ if (this.runTracker.hasActiveRuns("autonomous")) {
1904
+ this.transitionTurnState("autonomous", reason);
1905
+ return;
1906
+ }
1907
+ this.transitionTurnState("idle", reason);
1908
+ }
1909
+ failRun(run, errorMessage) {
1910
+ this.emitRunEvent(run, {
1911
+ type: "turn_failed",
1912
+ provider: "claude",
1913
+ error: errorMessage,
1914
+ });
1915
+ }
1916
+ cancelRun(run, event) {
1917
+ this.flushPendingToolCalls();
1918
+ this.emitRunEvent(run, event);
1919
+ }
1920
+ emitRunEvent(run, event) {
1921
+ if (run.owner === "foreground" && run.queue) {
1922
+ run.queue.push(event);
1923
+ if (event.type === "turn_completed" ||
1924
+ event.type === "turn_failed" ||
1925
+ event.type === "turn_canceled") {
1926
+ run.queue.end();
1271
1927
  }
1272
- const { value, done } = await query.next();
1273
- if (done) {
1274
- break;
1928
+ }
1929
+ else {
1930
+ this.liveEventQueue.push(event);
1931
+ }
1932
+ this.handleRunTerminalEvent(run, event);
1933
+ }
1934
+ handleRunTerminalEvent(run, event) {
1935
+ if (event.type === "turn_completed") {
1936
+ this.runTracker.complete(run, "completed");
1937
+ }
1938
+ else if (event.type === "turn_failed") {
1939
+ this.runTracker.complete(run, "error");
1940
+ }
1941
+ else if (event.type === "turn_canceled") {
1942
+ this.runTracker.complete(run, "interrupted");
1943
+ }
1944
+ else {
1945
+ return;
1946
+ }
1947
+ if (this.activeForegroundTurn?.runId === run.id) {
1948
+ this.activeForegroundTurn = null;
1949
+ this.preReplayMetadataSeen = false;
1950
+ }
1951
+ this.transitionTurnStateFromActiveRuns(`run ${run.id} terminal`);
1952
+ }
1953
+ async transitionAutonomousToForeground() {
1954
+ const autonomousRuns = this.runTracker.listActiveRuns("autonomous");
1955
+ if (autonomousRuns.length === 0) {
1956
+ this.transitionTurnStateFromActiveRuns("no autonomous runs to transition");
1957
+ return;
1958
+ }
1959
+ this.logger.debug({ runIds: autonomousRuns.map((run) => run.id) }, "Transitioning autonomous runs to foreground ownership");
1960
+ this.flushPendingToolCalls();
1961
+ for (const run of autonomousRuns) {
1962
+ this.emitRunEvent(run, {
1963
+ type: "turn_canceled",
1964
+ provider: "claude",
1965
+ reason: "Interrupted by foreground prompt",
1966
+ });
1967
+ }
1968
+ this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
1969
+ this.logger.warn({ err: error }, "Failed to interrupt autonomous run during foreground transition");
1970
+ });
1971
+ await this.awaitPendingInterruptPromise();
1972
+ this.transitionTurnStateFromActiveRuns("autonomous interrupted for foreground");
1973
+ }
1974
+ routeMessage(normalized) {
1975
+ if (normalized.metadataOnly) {
1976
+ if (normalized.message.type === "user" &&
1977
+ isTaskNotificationUserContent(normalized.message.message?.content)) {
1978
+ this.reserveAutonomousWake("task_notification");
1275
1979
  }
1276
- if (!value) {
1277
- continue;
1980
+ this.notePreReplayMetadata(normalized.message);
1981
+ return { run: null, reason: "metadata" };
1982
+ }
1983
+ const hasIdentifiers = Boolean(normalized.identifiers.taskId ||
1984
+ normalized.identifiers.parentMessageId ||
1985
+ normalized.identifiers.messageId);
1986
+ const byIdentifiers = this.runTracker.resolveByIdentifiers(normalized.identifiers);
1987
+ if (byIdentifiers.run) {
1988
+ return byIdentifiers;
1989
+ }
1990
+ if (this.turnState === "autonomous") {
1991
+ const activeAutonomousRun = this.runTracker.getLatestActiveRun("autonomous");
1992
+ if (activeAutonomousRun) {
1993
+ return { run: activeAutonomousRun, reason: "unbound_autonomous" };
1278
1994
  }
1279
- // Double-check turn ID after awaiting, in case a new turn started while we waited
1280
- if (this.currentTurnId !== turnId) {
1281
- break;
1995
+ }
1996
+ const foregroundRun = this.activeForegroundTurn
1997
+ ? this.runTracker.getRun(this.activeForegroundTurn.runId)
1998
+ : null;
1999
+ // A previously unseen task_id during foreground ownership is deterministic
2000
+ // evidence of a distinct autonomous wake/run, not foreground response text.
2001
+ if (this.turnState === "foreground" &&
2002
+ foregroundRun &&
2003
+ normalized.identifiers.taskId) {
2004
+ const incomingTaskId = normalized.identifiers.taskId;
2005
+ // Foreground must claim its first task_id; otherwise early foreground
2006
+ // result events can be misrouted to autonomous fallback runs.
2007
+ if (foregroundRun.taskIds.size === 0) {
2008
+ if (foregroundRun.state !== "finalizing") {
2009
+ return { run: foregroundRun, reason: "foreground" };
2010
+ }
1282
2011
  }
1283
- yield value;
1284
- if (value.type === "result") {
1285
- break;
2012
+ else if (foregroundRun.taskIds.has(incomingTaskId)) {
2013
+ return { run: foregroundRun, reason: "foreground" };
2014
+ }
2015
+ const autonomousRun = this.createRun("autonomous", null);
2016
+ this.emitRunEvent(autonomousRun, { type: "turn_started", provider: "claude" });
2017
+ return { run: autonomousRun, reason: "task_id_new" };
2018
+ }
2019
+ if (this.turnState === "foreground" &&
2020
+ foregroundRun &&
2021
+ this.shouldPreferForegroundRun({
2022
+ run: foregroundRun,
2023
+ message: normalized.message,
2024
+ })) {
2025
+ return { run: foregroundRun, reason: "foreground" };
2026
+ }
2027
+ if (!hasIdentifiers) {
2028
+ if (this.pendingAutonomousWakeReservations > 0) {
2029
+ const reservedAutonomousRun = this.claimOrCreateAutonomousRun("reservation_unbound");
2030
+ return {
2031
+ run: reservedAutonomousRun,
2032
+ reason: "unbound_autonomous",
2033
+ };
1286
2034
  }
2035
+ const activeAutonomousRun = this.runTracker.getLatestActiveRun("autonomous");
2036
+ if (activeAutonomousRun) {
2037
+ return { run: activeAutonomousRun, reason: "unbound_autonomous" };
2038
+ }
2039
+ }
2040
+ if (this.pendingAutonomousWakeReservations > 0) {
2041
+ const reservedAutonomousRun = this.claimOrCreateAutonomousRun("reservation_fallback");
2042
+ return { run: reservedAutonomousRun, reason: "fallback" };
1287
2043
  }
2044
+ const autonomousRun = this.createRun("autonomous", null);
2045
+ this.emitRunEvent(autonomousRun, { type: "turn_started", provider: "claude" });
2046
+ return { run: autonomousRun, reason: "fallback" };
1288
2047
  }
1289
- async forwardPromptEvents(message, queue, turnId) {
1290
- // Create a turn-local context to track streaming state.
1291
- // This prevents race conditions when a new stream() call interrupts a running one.
1292
- const turnContext = {
1293
- streamedAssistantTextThisTurn: false,
1294
- streamedReasoningThisTurn: false,
1295
- };
1296
- let completedNormally = false;
1297
- try {
1298
- for await (const sdkEvent of this.processPrompt(message, turnId)) {
1299
- // Check if this turn has been superseded before pushing events
1300
- if (this.currentTurnId !== turnId) {
1301
- break;
2048
+ shouldPreferForegroundRun(input) {
2049
+ const { run, message } = input;
2050
+ if (run.state === "completed" ||
2051
+ run.state === "interrupted" ||
2052
+ run.state === "error") {
2053
+ return false;
2054
+ }
2055
+ // Before prompt replay is observed, prefer foreground by default so the
2056
+ // first turn cannot be stranded in autonomous fallback. If metadata churn
2057
+ // was observed pre-replay, stay conservative and wait for replay.
2058
+ if (!run.promptReplaySeen) {
2059
+ // Keep pre-replay result events with the foreground run so stale result
2060
+ // bursts cannot consume autonomous wake reservations.
2061
+ if (message.type === "result") {
2062
+ return true;
2063
+ }
2064
+ if (message.type === "assistant" ||
2065
+ message.type === "stream_event" ||
2066
+ message.type === "tool_progress") {
2067
+ return !this.preReplayMetadataSeen;
2068
+ }
2069
+ return true;
2070
+ }
2071
+ if (run.state === "finalizing" &&
2072
+ (message.type === "assistant" || message.type === "stream_event")) {
2073
+ return false;
2074
+ }
2075
+ return true;
2076
+ }
2077
+ notePreReplayMetadata(message) {
2078
+ if (this.turnState !== "foreground") {
2079
+ return;
2080
+ }
2081
+ const foregroundRun = this.activeForegroundTurn
2082
+ ? this.runTracker.getRun(this.activeForegroundTurn.runId)
2083
+ : null;
2084
+ if (!foregroundRun || foregroundRun.promptReplaySeen) {
2085
+ return;
2086
+ }
2087
+ if (message.type === "system" && message.subtype === "init") {
2088
+ return;
2089
+ }
2090
+ this.preReplayMetadataSeen = true;
2091
+ }
2092
+ reserveAutonomousWake(reason) {
2093
+ this.pendingAutonomousWakeReservations += 1;
2094
+ this.logger.debug({
2095
+ reason,
2096
+ pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
2097
+ }, "Reserved autonomous wake");
2098
+ }
2099
+ claimOrCreateAutonomousRun(reason) {
2100
+ const existing = this.runTracker.getLatestActiveRun("autonomous");
2101
+ if (existing) {
2102
+ if (this.pendingAutonomousWakeReservations > 0) {
2103
+ this.pendingAutonomousWakeReservations -= 1;
2104
+ }
2105
+ this.logger.debug({
2106
+ reason,
2107
+ runId: existing.id,
2108
+ pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
2109
+ }, "Claimed autonomous wake reservation on existing run");
2110
+ return existing;
2111
+ }
2112
+ const run = this.createRun("autonomous", null);
2113
+ this.emitRunEvent(run, { type: "turn_started", provider: "claude" });
2114
+ if (this.pendingAutonomousWakeReservations > 0) {
2115
+ this.pendingAutonomousWakeReservations -= 1;
2116
+ }
2117
+ this.logger.debug({
2118
+ reason,
2119
+ runId: run.id,
2120
+ pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
2121
+ }, "Claimed autonomous wake reservation with new run");
2122
+ return run;
2123
+ }
2124
+ startQueryPump() {
2125
+ if (this.closed || this.queryPumpPromise) {
2126
+ return;
2127
+ }
2128
+ const pump = this.runQueryPump().catch((error) => {
2129
+ this.logger.warn({ err: error }, "Claude query pump exited unexpectedly");
2130
+ });
2131
+ this.queryPumpPromise = pump;
2132
+ pump.finally(() => {
2133
+ if (this.queryPumpPromise === pump) {
2134
+ this.queryPumpPromise = null;
2135
+ }
2136
+ });
2137
+ }
2138
+ async runQueryPump() {
2139
+ while (!this.closed) {
2140
+ if (!this.claudeSessionId && !this.activeForegroundTurn && !this.query) {
2141
+ await this.waitForLiveHistoryPoll();
2142
+ continue;
2143
+ }
2144
+ let q;
2145
+ try {
2146
+ q = await this.ensureQuery();
2147
+ }
2148
+ catch (error) {
2149
+ this.logger.warn({ err: error }, "Failed to initialize Claude query pump");
2150
+ await this.waitForLiveHistoryPoll();
2151
+ continue;
2152
+ }
2153
+ let next;
2154
+ try {
2155
+ next = await q.next();
2156
+ }
2157
+ catch (error) {
2158
+ this.logger.warn({ err: error }, "Claude query pump next() failed");
2159
+ for (const run of this.runTracker.listActiveRuns()) {
2160
+ this.failRun(run, error instanceof Error ? error.message : "Claude stream failed");
2161
+ }
2162
+ this.input?.end();
2163
+ await this.awaitWithTimeout(q.return?.(), "query pump return after failure");
2164
+ if (this.query === q) {
2165
+ this.query = null;
2166
+ this.input = null;
1302
2167
  }
1303
- const events = this.translateMessageToEvents(sdkEvent, turnContext);
1304
- for (const event of events) {
1305
- queue.push(event);
1306
- if (event.type === "turn_completed") {
1307
- completedNormally = true;
2168
+ await this.waitForLiveHistoryPoll();
2169
+ continue;
2170
+ }
2171
+ if (next.done) {
2172
+ this.input?.end();
2173
+ await this.awaitWithTimeout(q.return?.(), "query pump return on done");
2174
+ if (this.query === q) {
2175
+ this.query = null;
2176
+ this.input = null;
2177
+ }
2178
+ const activeRuns = this.runTracker.listActiveRuns();
2179
+ if (activeRuns.length > 0) {
2180
+ for (const run of activeRuns) {
2181
+ this.failRun(run, "Claude stream ended before terminal result");
1308
2182
  }
1309
2183
  }
2184
+ await this.waitForLiveHistoryPoll();
2185
+ continue;
2186
+ }
2187
+ const sdkMessage = next.value;
2188
+ if (!sdkMessage) {
2189
+ continue;
2190
+ }
2191
+ try {
2192
+ this.routeSdkMessageFromPump(sdkMessage);
2193
+ }
2194
+ catch (error) {
2195
+ this.logger.warn({ err: error }, "Failed to route Claude SDK message from query pump");
1310
2196
  }
1311
2197
  }
1312
- catch (error) {
1313
- if (!this.turnCancelRequested && this.currentTurnId === turnId) {
1314
- queue.push({
1315
- type: "turn_failed",
1316
- provider: "claude",
1317
- error: error instanceof Error ? error.message : "Claude stream failed",
1318
- });
2198
+ }
2199
+ routeSdkMessageFromPump(message) {
2200
+ if (this.shouldSuppressLocalReplayActivity(message)) {
2201
+ return;
2202
+ }
2203
+ const identifiers = readEventIdentifiers(message);
2204
+ const metadataOnly = isMetadataOnlySdkMessage(message);
2205
+ const route = this.routeMessage({
2206
+ message,
2207
+ identifiers,
2208
+ metadataOnly,
2209
+ });
2210
+ const suppressTerminalEvents = this.shouldSuppressReplayResultTerminal({
2211
+ run: route.run,
2212
+ message,
2213
+ });
2214
+ if (route.run) {
2215
+ this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
2216
+ this.runTracker.bindIdentifiers(route.run, identifiers);
2217
+ if (!suppressTerminalEvents) {
2218
+ this.updateRunLifecycleForMessage(route.run, message, identifiers);
1319
2219
  }
1320
2220
  }
1321
- finally {
1322
- // Emit terminal event for superseded turns so consumers get proper lifecycle signals.
1323
- // Use turn_canceled (not turn_failed) to distinguish intentional interruption from errors.
1324
- // Only emit if not already emitted by requestCancel() (indicated by turnCancelRequested).
1325
- const wasSuperseded = this.currentTurnId !== turnId;
1326
- if (wasSuperseded && !completedNormally && !this.turnCancelRequested) {
1327
- this.flushPendingToolCalls();
1328
- queue.push({
1329
- type: "turn_canceled",
1330
- provider: "claude",
1331
- reason: "Interrupted by new message",
1332
- });
2221
+ const messageEvents = this.translateMessageToEvents(message, {
2222
+ suppressAssistantText: true,
2223
+ suppressReasoning: true,
2224
+ suppressTerminalEvents,
2225
+ });
2226
+ const assistantTimelineItems = this.timelineAssembler.consume({
2227
+ message,
2228
+ runId: route.run?.id ?? null,
2229
+ messageIdHint: identifiers.messageId,
2230
+ });
2231
+ const assistantTimelineEvents = assistantTimelineItems.map((item) => ({
2232
+ type: "timeline",
2233
+ item,
2234
+ provider: "claude",
2235
+ }));
2236
+ const events = [...messageEvents, ...assistantTimelineEvents];
2237
+ if (events.length === 0) {
2238
+ return;
2239
+ }
2240
+ if (!route.run) {
2241
+ this.dispatchMetadataEvents(events);
2242
+ return;
2243
+ }
2244
+ for (const event of events) {
2245
+ this.emitRunEvent(route.run, event);
2246
+ }
2247
+ }
2248
+ shouldSuppressReplayResultTerminal(input) {
2249
+ const { run, message } = input;
2250
+ if (!run || run.owner !== "foreground" || message.type !== "result") {
2251
+ return false;
2252
+ }
2253
+ if (run.promptReplaySeen) {
2254
+ return false;
2255
+ }
2256
+ if (run.state === "streaming" || run.state === "finalizing") {
2257
+ return false;
2258
+ }
2259
+ const resultSubtype = "subtype" in message && typeof message.subtype === "string"
2260
+ ? message.subtype
2261
+ : null;
2262
+ // Pre-replay success results are stale in practice (leftover from an
2263
+ // earlier query segment) and must not end the current foreground run.
2264
+ if (resultSubtype === "success") {
2265
+ return true;
2266
+ }
2267
+ // For non-success results, keep the metadata-churn guard to avoid
2268
+ // suppressing legitimate hard failures.
2269
+ return this.preReplayMetadataSeen;
2270
+ }
2271
+ dispatchMetadataEvents(events) {
2272
+ for (const event of events) {
2273
+ this.pushEvent(event);
2274
+ }
2275
+ }
2276
+ updateRunLifecycleForMessage(run, message, identifiers) {
2277
+ if (message.type === "user" &&
2278
+ identifiers.messageId &&
2279
+ run.messageIds.has(identifiers.messageId)) {
2280
+ run.promptReplaySeen = true;
2281
+ this.preReplayMetadataSeen = false;
2282
+ }
2283
+ if (run.state === "queued") {
2284
+ this.runTracker.transition(run, "awaiting_response");
2285
+ }
2286
+ if (message.type === "assistant" ||
2287
+ message.type === "stream_event" ||
2288
+ message.type === "tool_progress") {
2289
+ this.runTracker.transition(run, "streaming");
2290
+ return;
2291
+ }
2292
+ if (message.type === "result") {
2293
+ this.runTracker.transition(run, "finalizing");
2294
+ return;
2295
+ }
2296
+ }
2297
+ shouldSuppressLocalReplayActivity(message) {
2298
+ const localReplay = this.isLocalReplayUserMessage(message);
2299
+ if (!this.activeForegroundTurn && localReplay) {
2300
+ this.suppressLocalReplayActivity = true;
2301
+ this.logger.debug({ uuid: message.uuid }, "Suppressing local replay user message from live pump");
2302
+ return true;
2303
+ }
2304
+ if (!this.suppressLocalReplayActivity) {
2305
+ return false;
2306
+ }
2307
+ // Suppress only replay scaffolding. Do not suppress autonomous
2308
+ // assistant/result events; otherwise task-notification replies can be dropped.
2309
+ if (localReplay) {
2310
+ return true;
2311
+ }
2312
+ if (message.type === "system") {
2313
+ return true;
2314
+ }
2315
+ const identifiers = readEventIdentifiers(message);
2316
+ const hasIdentifiers = Boolean(identifiers.taskId || identifiers.parentMessageId || identifiers.messageId);
2317
+ if (message.type !== "user" && !hasIdentifiers) {
2318
+ if (this.pendingAutonomousWakeReservations > 0) {
2319
+ this.suppressLocalReplayActivity = false;
2320
+ return false;
1333
2321
  }
1334
- this.turnCancelRequested = false;
1335
- queue.end();
2322
+ return true;
2323
+ }
2324
+ if (message.type === "user") {
2325
+ this.suppressLocalReplayActivity = false;
2326
+ return false;
1336
2327
  }
2328
+ this.suppressLocalReplayActivity = false;
2329
+ return false;
2330
+ }
2331
+ isLocalReplayUserMessage(message) {
2332
+ if (message.type !== "user") {
2333
+ return false;
2334
+ }
2335
+ const uuid = readTrimmedString(message.uuid);
2336
+ if (!uuid) {
2337
+ return false;
2338
+ }
2339
+ return this.localUserMessageIds.has(uuid);
1337
2340
  }
1338
2341
  async interruptActiveTurn() {
1339
2342
  const queryToInterrupt = this.query;
1340
2343
  if (!queryToInterrupt || typeof queryToInterrupt.interrupt !== "function") {
1341
- this.logger.info("interruptActiveTurn: no query to interrupt");
2344
+ this.logger.debug("interruptActiveTurn: no query to interrupt");
1342
2345
  return;
1343
2346
  }
1344
2347
  try {
1345
- this.logger.info("interruptActiveTurn: calling query.interrupt()...");
2348
+ this.logger.debug("interruptActiveTurn: calling query.interrupt()...");
1346
2349
  const t0 = Date.now();
1347
2350
  await queryToInterrupt.interrupt();
1348
- this.logger.info({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
2351
+ this.logger.debug({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
1349
2352
  // After interrupt(), the query iterator is done (returns done: true).
1350
2353
  // Clear it so ensureQuery() creates a fresh query for the next turn.
1351
2354
  // Also end the input stream and call return() to clean up the SDK process.
1352
2355
  this.input?.end();
1353
- this.logger.info("interruptActiveTurn: calling query.return()...");
2356
+ this.logger.debug("interruptActiveTurn: calling query.return()...");
1354
2357
  const t1 = Date.now();
1355
2358
  await queryToInterrupt.return?.();
1356
- this.logger.info({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
2359
+ this.logger.debug({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
1357
2360
  this.query = null;
1358
2361
  this.input = null;
1359
2362
  this.queryRestartNeeded = false;
1360
2363
  }
1361
2364
  catch (error) {
1362
2365
  this.logger.warn({ err: error }, "Failed to interrupt active turn");
2366
+ // If interrupt fails, the SDK iterator may remain in an indeterminate state.
2367
+ // Force a teardown/recreate path so the next turn cannot reuse stale query state.
2368
+ this.queryRestartNeeded = true;
1363
2369
  }
1364
2370
  }
1365
2371
  handleSidechainMessage(message, parentToolUseId) {
1366
- let toolName;
1367
- if (message.type === "assistant") {
1368
- const content = message.message?.content;
1369
- if (Array.isArray(content)) {
1370
- for (const block of content) {
1371
- if (isClaudeContentChunk(block) &&
1372
- (block.type === "tool_use" || block.type === "mcp_tool_use" || block.type === "server_tool_use") &&
1373
- typeof block.name === "string") {
1374
- toolName = block.name;
1375
- break;
1376
- }
1377
- }
1378
- }
1379
- }
1380
- else if (message.type === "stream_event") {
1381
- const event = message.event;
1382
- if (event.type === "content_block_start") {
1383
- const cb = isClaudeContentChunk(event.content_block) ? event.content_block : null;
1384
- if (cb?.type === "tool_use" && typeof cb.name === "string") {
1385
- toolName = cb.name;
1386
- }
2372
+ const state = this.activeSidechains.get(parentToolUseId) ??
2373
+ {
2374
+ actions: [],
2375
+ actionKeys: [],
2376
+ nextActionIndex: 1,
2377
+ actionIndexByKey: new Map(),
2378
+ };
2379
+ this.activeSidechains.set(parentToolUseId, state);
2380
+ const contextUpdated = this.updateSubAgentContextFromTaskInput(state, parentToolUseId);
2381
+ const actionCandidates = this.extractSubAgentActionCandidates(message);
2382
+ let actionUpdated = false;
2383
+ for (const action of actionCandidates) {
2384
+ if (this.appendSubAgentAction(state, action)) {
2385
+ actionUpdated = true;
1387
2386
  }
1388
2387
  }
1389
- else if (message.type === "tool_progress") {
1390
- toolName = message.tool_name;
1391
- }
1392
- if (!toolName) {
1393
- return [];
1394
- }
1395
- const prev = this.activeSidechains.get(parentToolUseId);
1396
- if (prev === toolName) {
2388
+ if (!contextUpdated && !actionUpdated) {
1397
2389
  return [];
1398
2390
  }
1399
- this.activeSidechains.set(parentToolUseId, toolName);
1400
2391
  const toolCall = mapClaudeRunningToolCall({
1401
2392
  name: "Task",
1402
2393
  callId: parentToolUseId,
1403
2394
  input: null,
1404
2395
  output: null,
1405
- metadata: { subAgentActivity: toolName },
1406
2396
  });
1407
2397
  if (!toolCall) {
1408
2398
  return [];
1409
2399
  }
2400
+ const detail = {
2401
+ type: "sub_agent",
2402
+ ...(state.subAgentType ? { subAgentType: state.subAgentType } : {}),
2403
+ ...(state.description ? { description: state.description } : {}),
2404
+ log: state.actions
2405
+ .map((action) => action.summary
2406
+ ? `[${action.toolName}] ${action.summary}`
2407
+ : `[${action.toolName}]`)
2408
+ .join("\n"),
2409
+ actions: state.actions.map((action) => ({
2410
+ index: action.index,
2411
+ toolName: action.toolName,
2412
+ ...(action.summary ? { summary: action.summary } : {}),
2413
+ })),
2414
+ };
1410
2415
  return [
1411
2416
  {
1412
2417
  type: "timeline",
1413
- item: toolCall,
2418
+ item: {
2419
+ ...toolCall,
2420
+ detail,
2421
+ },
1414
2422
  provider: "claude",
1415
2423
  },
1416
2424
  ];
1417
2425
  }
1418
- translateMessageToEvents(message, turnContext) {
2426
+ updateSubAgentContextFromTaskInput(state, parentToolUseId) {
2427
+ const taskInput = this.toolUseCache.get(parentToolUseId)?.input;
2428
+ const nextSubAgentType = this.normalizeSubAgentText(taskInput?.subagent_type);
2429
+ const nextDescription = this.normalizeSubAgentText(taskInput?.description);
2430
+ let changed = false;
2431
+ if (nextSubAgentType && nextSubAgentType !== state.subAgentType) {
2432
+ state.subAgentType = nextSubAgentType;
2433
+ changed = true;
2434
+ }
2435
+ if (nextDescription && nextDescription !== state.description) {
2436
+ state.description = nextDescription;
2437
+ changed = true;
2438
+ }
2439
+ return changed;
2440
+ }
2441
+ normalizeSubAgentText(value) {
2442
+ const normalized = readTrimmedString(value)?.replace(/\s+/g, " ");
2443
+ if (!normalized) {
2444
+ return undefined;
2445
+ }
2446
+ if (normalized.length <= MAX_SUB_AGENT_SUMMARY_CHARS) {
2447
+ return normalized;
2448
+ }
2449
+ return `${normalized.slice(0, MAX_SUB_AGENT_SUMMARY_CHARS)}...`;
2450
+ }
2451
+ extractSubAgentActionCandidates(message) {
2452
+ if (message.type === "assistant") {
2453
+ const content = message.message?.content;
2454
+ if (!Array.isArray(content)) {
2455
+ return [];
2456
+ }
2457
+ const actions = [];
2458
+ for (const block of content) {
2459
+ if (!isClaudeContentChunk(block) ||
2460
+ !(block.type === "tool_use" ||
2461
+ block.type === "mcp_tool_use" ||
2462
+ block.type === "server_tool_use") ||
2463
+ typeof block.name !== "string") {
2464
+ continue;
2465
+ }
2466
+ const key = readTrimmedString(block.id) ??
2467
+ `assistant:${block.name}:${actions.length}`;
2468
+ actions.push({
2469
+ key,
2470
+ toolName: block.name,
2471
+ input: block.input ?? null,
2472
+ });
2473
+ }
2474
+ return actions;
2475
+ }
2476
+ if (message.type === "stream_event") {
2477
+ const event = message.event;
2478
+ if (event.type !== "content_block_start") {
2479
+ return [];
2480
+ }
2481
+ const block = isClaudeContentChunk(event.content_block)
2482
+ ? event.content_block
2483
+ : null;
2484
+ if (!block ||
2485
+ !(block.type === "tool_use" ||
2486
+ block.type === "mcp_tool_use" ||
2487
+ block.type === "server_tool_use") ||
2488
+ typeof block.name !== "string") {
2489
+ return [];
2490
+ }
2491
+ const key = readTrimmedString(block.id) ??
2492
+ `stream:${block.name}:${typeof event.index === "number" ? event.index : 0}`;
2493
+ return [
2494
+ {
2495
+ key,
2496
+ toolName: block.name,
2497
+ input: block.input ?? null,
2498
+ },
2499
+ ];
2500
+ }
2501
+ if (message.type === "tool_progress") {
2502
+ const toolName = readTrimmedString(message.tool_name);
2503
+ if (!toolName) {
2504
+ return [];
2505
+ }
2506
+ const key = readTrimmedString(message.tool_use_id) ?? `progress:${toolName}`;
2507
+ return [{ key, toolName, input: null }];
2508
+ }
2509
+ return [];
2510
+ }
2511
+ appendSubAgentAction(state, candidate) {
2512
+ const normalizedToolName = readTrimmedString(candidate.toolName);
2513
+ if (!normalizedToolName) {
2514
+ return false;
2515
+ }
2516
+ const summary = this.deriveSubAgentActionSummary(normalizedToolName, candidate.input);
2517
+ const existingIndex = state.actionIndexByKey.get(candidate.key);
2518
+ if (existingIndex !== undefined) {
2519
+ const existing = state.actions[existingIndex];
2520
+ if (!existing) {
2521
+ return false;
2522
+ }
2523
+ const nextSummary = existing.summary ?? summary;
2524
+ const unchanged = existing.toolName === normalizedToolName &&
2525
+ existing.summary === nextSummary;
2526
+ if (unchanged) {
2527
+ return false;
2528
+ }
2529
+ state.actions[existingIndex] = {
2530
+ ...existing,
2531
+ toolName: normalizedToolName,
2532
+ ...(nextSummary ? { summary: nextSummary } : {}),
2533
+ };
2534
+ return true;
2535
+ }
2536
+ const nextEntry = {
2537
+ index: state.nextActionIndex,
2538
+ toolName: normalizedToolName,
2539
+ ...(summary ? { summary } : {}),
2540
+ };
2541
+ state.nextActionIndex += 1;
2542
+ state.actions.push(nextEntry);
2543
+ state.actionKeys.push(candidate.key);
2544
+ this.trimSubAgentTail(state);
2545
+ this.rebuildSubAgentActionIndex(state);
2546
+ return true;
2547
+ }
2548
+ trimSubAgentTail(state) {
2549
+ while (state.actions.length > MAX_SUB_AGENT_LOG_ENTRIES) {
2550
+ state.actions.shift();
2551
+ state.actionKeys.shift();
2552
+ }
2553
+ }
2554
+ rebuildSubAgentActionIndex(state) {
2555
+ state.actionIndexByKey.clear();
2556
+ for (let index = 0; index < state.actionKeys.length; index += 1) {
2557
+ const key = state.actionKeys[index];
2558
+ if (key) {
2559
+ state.actionIndexByKey.set(key, index);
2560
+ }
2561
+ }
2562
+ }
2563
+ deriveSubAgentActionSummary(toolName, input) {
2564
+ const runningToolCall = mapClaudeRunningToolCall({
2565
+ name: toolName,
2566
+ callId: `sub-agent-summary-${toolName}`,
2567
+ input,
2568
+ output: null,
2569
+ });
2570
+ if (!runningToolCall) {
2571
+ return undefined;
2572
+ }
2573
+ const display = buildToolCallDisplayModel({
2574
+ name: runningToolCall.name,
2575
+ status: runningToolCall.status,
2576
+ error: runningToolCall.error,
2577
+ detail: runningToolCall.detail,
2578
+ metadata: runningToolCall.metadata,
2579
+ });
2580
+ return this.normalizeSubAgentText(display.summary);
2581
+ }
2582
+ translateMessageToEvents(message, options) {
1419
2583
  const parentToolUseId = "parent_tool_use_id" in message
1420
2584
  ? message.parent_tool_use_id
1421
2585
  : null;
@@ -1423,6 +2587,14 @@ class ClaudeAgentSession {
1423
2587
  return this.handleSidechainMessage(message, parentToolUseId);
1424
2588
  }
1425
2589
  const events = [];
2590
+ const fallbackThreadSessionId = this.captureSessionIdFromMessage(message);
2591
+ if (fallbackThreadSessionId) {
2592
+ events.push({
2593
+ type: "thread_started",
2594
+ provider: "claude",
2595
+ sessionId: fallbackThreadSessionId,
2596
+ });
2597
+ }
1426
2598
  switch (message.type) {
1427
2599
  case "system":
1428
2600
  if (message.subtype === "init") {
@@ -1447,14 +2619,14 @@ class ClaudeAgentSession {
1447
2619
  }
1448
2620
  }
1449
2621
  else if (message.subtype === "compact_boundary") {
1450
- const meta = message.compact_metadata;
2622
+ const compactMetadata = readCompactionMetadata(message);
1451
2623
  events.push({
1452
2624
  type: "timeline",
1453
2625
  item: {
1454
2626
  type: "compaction",
1455
2627
  status: "completed",
1456
- trigger: meta?.trigger === "manual" ? "manual" : "auto",
1457
- preTokens: meta?.pre_tokens,
2628
+ trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
2629
+ preTokens: compactMetadata?.preTokens,
1458
2630
  },
1459
2631
  provider: "claude",
1460
2632
  });
@@ -1483,7 +2655,7 @@ class ClaudeAgentSession {
1483
2655
  });
1484
2656
  }
1485
2657
  else if (Array.isArray(content)) {
1486
- const timelineItems = this.mapBlocksToTimeline(content, { turnContext });
2658
+ const timelineItems = this.mapBlocksToTimeline(content);
1487
2659
  for (const item of timelineItems) {
1488
2660
  if (item.type === "user_message" && messageId && !item.messageId) {
1489
2661
  events.push({
@@ -1500,9 +2672,8 @@ class ClaudeAgentSession {
1500
2672
  }
1501
2673
  case "assistant": {
1502
2674
  const timelineItems = this.mapBlocksToTimeline(message.message.content, {
1503
- turnContext,
1504
- suppressAssistantText: turnContext.streamedAssistantTextThisTurn,
1505
- suppressReasoning: turnContext.streamedReasoningThisTurn,
2675
+ suppressAssistantText: options?.suppressAssistantText ?? false,
2676
+ suppressReasoning: options?.suppressReasoning ?? false,
1506
2677
  });
1507
2678
  for (const item of timelineItems) {
1508
2679
  events.push({ type: "timeline", item, provider: "claude" });
@@ -1510,13 +2681,19 @@ class ClaudeAgentSession {
1510
2681
  break;
1511
2682
  }
1512
2683
  case "stream_event": {
1513
- const timelineItems = this.mapPartialEvent(message.event, turnContext);
2684
+ const timelineItems = this.mapPartialEvent(message.event, {
2685
+ suppressAssistantText: options?.suppressAssistantText ?? false,
2686
+ suppressReasoning: options?.suppressReasoning ?? false,
2687
+ });
1514
2688
  for (const item of timelineItems) {
1515
2689
  events.push({ type: "timeline", item, provider: "claude" });
1516
2690
  }
1517
2691
  break;
1518
2692
  }
1519
2693
  case "result": {
2694
+ if (options?.suppressTerminalEvents) {
2695
+ break;
2696
+ }
1520
2697
  const usage = this.convertUsage(message);
1521
2698
  if (message.subtype === "success") {
1522
2699
  events.push({ type: "turn_completed", provider: "claude", usage });
@@ -1534,6 +2711,31 @@ class ClaudeAgentSession {
1534
2711
  }
1535
2712
  return events;
1536
2713
  }
2714
+ captureSessionIdFromMessage(message) {
2715
+ const msg = message;
2716
+ const sessionIdRaw = typeof msg.session_id === "string"
2717
+ ? msg.session_id
2718
+ : typeof msg.sessionId === "string"
2719
+ ? msg.sessionId
2720
+ : typeof msg.session?.id === "string"
2721
+ ? msg.session.id
2722
+ : "";
2723
+ const sessionId = sessionIdRaw.trim();
2724
+ if (!sessionId) {
2725
+ return null;
2726
+ }
2727
+ if (this.claudeSessionId === null) {
2728
+ this.claudeSessionId = sessionId;
2729
+ this.persistence = null;
2730
+ return sessionId;
2731
+ }
2732
+ if (this.claudeSessionId === sessionId) {
2733
+ return null;
2734
+ }
2735
+ throw new Error(`CRITICAL: Claude session ID overwrite detected! ` +
2736
+ `Existing: ${this.claudeSessionId}, New: ${sessionId}. ` +
2737
+ `This indicates a session identity corruption bug.`);
2738
+ }
1537
2739
  handleSystemMessage(message) {
1538
2740
  if (message.subtype !== "init") {
1539
2741
  return null;
@@ -1613,6 +2815,7 @@ class ClaudeAgentSession {
1613
2815
  }
1614
2816
  }
1615
2817
  this.toolUseCache.clear();
2818
+ this.activeSidechains.clear();
1616
2819
  }
1617
2820
  pushToolCall(item, target) {
1618
2821
  if (!item) {
@@ -1625,9 +2828,19 @@ class ClaudeAgentSession {
1625
2828
  this.enqueueTimeline(item);
1626
2829
  }
1627
2830
  pushEvent(event) {
1628
- if (this.eventQueue) {
1629
- this.eventQueue.push(event);
2831
+ const foregroundTurn = this.activeForegroundTurn;
2832
+ if (foregroundTurn) {
2833
+ const run = this.runTracker.getRun(foregroundTurn.runId);
2834
+ if (run &&
2835
+ run.owner === "foreground" &&
2836
+ run.queue === foregroundTurn.queue &&
2837
+ this.runTracker.isRunActive(run)) {
2838
+ foregroundTurn.queue.push(event);
2839
+ return;
2840
+ }
2841
+ this.activeForegroundTurn = null;
1630
2842
  }
2843
+ this.liveEventQueue.push(event);
1631
2844
  }
1632
2845
  normalizePermissionUpdates(updates) {
1633
2846
  if (!updates || updates.length === 0) {
@@ -1643,6 +2856,9 @@ class ClaudeAgentSession {
1643
2856
  this.pendingPermissions.delete(id);
1644
2857
  }
1645
2858
  }
2859
+ waitForLiveHistoryPoll() {
2860
+ return new Promise((resolve) => setTimeout(resolve, 250));
2861
+ }
1646
2862
  loadPersistedHistory(sessionId) {
1647
2863
  try {
1648
2864
  const historyPath = this.resolveHistoryPath(sessionId);
@@ -1692,20 +2908,15 @@ class ClaudeAgentSession {
1692
2908
  return path.join(dir, `${sessionId}.jsonl`);
1693
2909
  }
1694
2910
  convertHistoryEntry(entry) {
1695
- return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content, { context: "history" }));
2911
+ return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content));
1696
2912
  }
1697
2913
  mapBlocksToTimeline(content, options) {
1698
- const context = options?.context ?? "live";
1699
- const turnContext = options?.turnContext;
1700
2914
  const suppressAssistant = options?.suppressAssistantText ?? false;
1701
2915
  const suppressReasoning = options?.suppressReasoning ?? false;
1702
2916
  if (typeof content === "string") {
1703
- if (!content || content === "[Request interrupted by user for tool use]") {
2917
+ if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
1704
2918
  return [];
1705
2919
  }
1706
- if (context === "live" && turnContext) {
1707
- turnContext.streamedAssistantTextThisTurn = true;
1708
- }
1709
2920
  if (suppressAssistant) {
1710
2921
  return [];
1711
2922
  }
@@ -1716,10 +2927,7 @@ class ClaudeAgentSession {
1716
2927
  switch (block.type) {
1717
2928
  case "text":
1718
2929
  case "text_delta":
1719
- if (block.text && block.text !== "[Request interrupted by user for tool use]") {
1720
- if (context === "live" && turnContext) {
1721
- turnContext.streamedAssistantTextThisTurn = true;
1722
- }
2930
+ if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
1723
2931
  if (!suppressAssistant) {
1724
2932
  items.push({ type: "assistant_message", text: block.text });
1725
2933
  }
@@ -1728,9 +2936,6 @@ class ClaudeAgentSession {
1728
2936
  case "thinking":
1729
2937
  case "thinking_delta":
1730
2938
  if (block.thinking) {
1731
- if (context === "live" && turnContext) {
1732
- turnContext.streamedReasoningThisTurn = true;
1733
- }
1734
2939
  if (!suppressReasoning) {
1735
2940
  items.push({ type: "reasoning", text: block.thinking });
1736
2941
  }
@@ -1802,6 +3007,7 @@ class ClaudeAgentSession {
1802
3007
  }
1803
3008
  if (typeof block.tool_use_id === "string") {
1804
3009
  this.toolUseCache.delete(block.tool_use_id);
3010
+ this.activeSidechains.delete(block.tool_use_id);
1805
3011
  }
1806
3012
  }
1807
3013
  buildToolOutput(block, entry) {
@@ -1810,7 +3016,7 @@ class ClaudeAgentSession {
1810
3016
  }
1811
3017
  const server = entry?.server ?? block.server ?? "tool";
1812
3018
  const tool = entry?.name ?? block.tool_name ?? "tool";
1813
- const content = typeof block.content === "string" ? block.content : "";
3019
+ const content = coerceToolResultContentToString(block.content);
1814
3020
  const input = entry?.input;
1815
3021
  // Build structured result based on tool type
1816
3022
  const structured = this.buildStructuredToolResult(server, tool, content, input);
@@ -1899,7 +3105,7 @@ class ClaudeAgentSession {
1899
3105
  }
1900
3106
  return undefined;
1901
3107
  }
1902
- mapPartialEvent(event, turnContext) {
3108
+ mapPartialEvent(event, options) {
1903
3109
  if (event.type === "content_block_start") {
1904
3110
  const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
1905
3111
  if (block?.type === "tool_use" && typeof event.index === "number" && typeof block.id === "string") {
@@ -1925,10 +3131,18 @@ class ClaudeAgentSession {
1925
3131
  switch (event.type) {
1926
3132
  case "content_block_start":
1927
3133
  return isClaudeContentChunk(event.content_block)
1928
- ? this.mapBlocksToTimeline([event.content_block], { turnContext })
3134
+ ? this.mapBlocksToTimeline([event.content_block], {
3135
+ suppressAssistantText: options?.suppressAssistantText,
3136
+ suppressReasoning: options?.suppressReasoning,
3137
+ })
1929
3138
  : [];
1930
3139
  case "content_block_delta":
1931
- return isClaudeContentChunk(event.delta) ? this.mapBlocksToTimeline([event.delta], { turnContext }) : [];
3140
+ return isClaudeContentChunk(event.delta)
3141
+ ? this.mapBlocksToTimeline([event.delta], {
3142
+ suppressAssistantText: options?.suppressAssistantText,
3143
+ suppressReasoning: options?.suppressReasoning,
3144
+ })
3145
+ : [];
1932
3146
  default:
1933
3147
  return [];
1934
3148
  }
@@ -2127,6 +3341,24 @@ function hasToolLikeBlock(block) {
2127
3341
  const type = typeof block.type === "string" ? block.type.toLowerCase() : "";
2128
3342
  return type.includes("tool");
2129
3343
  }
3344
+ function readCompactionMetadata(source) {
3345
+ const candidates = [
3346
+ source.compact_metadata,
3347
+ source.compactMetadata,
3348
+ source.compactionMetadata,
3349
+ ];
3350
+ for (const candidate of candidates) {
3351
+ if (!candidate || typeof candidate !== "object") {
3352
+ continue;
3353
+ }
3354
+ const metadata = candidate;
3355
+ const trigger = typeof metadata.trigger === "string" ? metadata.trigger : undefined;
3356
+ const preTokensRaw = metadata.preTokens ?? metadata.pre_tokens;
3357
+ const preTokens = typeof preTokensRaw === "number" ? preTokensRaw : undefined;
3358
+ return { trigger, preTokens };
3359
+ }
3360
+ return null;
3361
+ }
2130
3362
  function normalizeHistoryBlocks(content) {
2131
3363
  if (Array.isArray(content)) {
2132
3364
  const blocks = content.filter((entry) => isClaudeContentChunk(entry));
@@ -2139,11 +3371,12 @@ function normalizeHistoryBlocks(content) {
2139
3371
  }
2140
3372
  export function convertClaudeHistoryEntry(entry, mapBlocks) {
2141
3373
  if (entry.type === "system" && entry.subtype === "compact_boundary") {
3374
+ const compactMetadata = readCompactionMetadata(entry);
2142
3375
  return [{
2143
3376
  type: "compaction",
2144
3377
  status: "completed",
2145
- trigger: entry.compactMetadata?.trigger === "manual" ? "manual" : "auto",
2146
- preTokens: entry.compactMetadata?.preTokens,
3378
+ trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
3379
+ preTokens: compactMetadata?.preTokens,
2147
3380
  }];
2148
3381
  }
2149
3382
  if (entry.isCompactSummary) {