@getpaseo/server 0.1.15 → 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 -232
  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
  }
@@ -1111,13 +1738,30 @@ class ClaudeAgentSession {
1111
1738
  }
1112
1739
  const input = new Pushable();
1113
1740
  const options = this.buildOptions();
1114
- this.logger.debug({ options }, "claude query");
1741
+ this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query");
1115
1742
  this.input = input;
1116
1743
  this.query = query({ prompt: input, options });
1117
- await this.primeSelectableModelIds(this.query);
1118
- 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");
1119
1747
  return this.query;
1120
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
+ }
1121
1765
  buildOptions() {
1122
1766
  const configuredThinkingOptionId = this.config.thinkingOptionId;
1123
1767
  const thinkingOptionId = configuredThinkingOptionId && configuredThinkingOptionId !== "default"
@@ -1181,24 +1825,7 @@ class ClaudeAgentSession {
1181
1825
  return this.applyRuntimeSettings(base);
1182
1826
  }
1183
1827
  applyRuntimeSettings(options) {
1184
- const hasEnvOverrides = Object.keys(this.runtimeSettings?.env ?? {}).length > 0;
1185
- const commandMode = this.runtimeSettings?.command?.mode;
1186
- const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
1187
- if (!needsCustomSpawn) {
1188
- return options;
1189
- }
1190
- return {
1191
- ...options,
1192
- spawnClaudeCodeProcess: (spawnOptions) => {
1193
- const resolved = resolveClaudeSpawnCommand(spawnOptions, this.runtimeSettings);
1194
- return spawn(resolved.command, resolved.args, {
1195
- cwd: spawnOptions.cwd,
1196
- env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
1197
- signal: spawnOptions.signal,
1198
- stdio: ["pipe", "pipe", "pipe"],
1199
- });
1200
- },
1201
- };
1828
+ return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
1202
1829
  }
1203
1830
  normalizeMcpServers(servers) {
1204
1831
  const result = {};
@@ -1231,6 +1858,7 @@ class ClaudeAgentSession {
1231
1858
  }
1232
1859
  const messageId = randomUUID();
1233
1860
  this.rememberUserMessageId(messageId);
1861
+ this.localUserMessageIds.add(messageId);
1234
1862
  return {
1235
1863
  type: "user",
1236
1864
  message: {
@@ -1242,175 +1870,716 @@ class ClaudeAgentSession {
1242
1870
  session_id: this.claudeSessionId ?? "",
1243
1871
  };
1244
1872
  }
1245
- async *processPrompt(sdkMessage, turnId) {
1246
- // If there's a pending interrupt, await it BEFORE calling ensureQuery().
1247
- // interruptActiveTurn() clears this.query after interrupt() returns,
1248
- // so we must wait for it to complete before we try to get the query.
1249
- if (this.pendingInterruptPromise) {
1250
- await this.pendingInterruptPromise;
1251
- this.pendingInterruptPromise = null;
1873
+ async awaitPendingInterruptPromise() {
1874
+ if (!this.pendingInterruptPromise) {
1875
+ return;
1252
1876
  }
1253
- // Check if we were superseded while waiting for the interrupt
1254
- 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) {
1255
1893
  return;
1256
1894
  }
1257
- const query = await this.ensureQuery();
1258
- if (!this.input) {
1259
- 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;
1260
1902
  }
1261
- this.input.push(sdkMessage);
1262
- while (true) {
1263
- // Check if this turn has been superseded by a new one.
1264
- if (this.currentTurnId !== turnId) {
1265
- 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();
1266
1927
  }
1267
- const { value, done } = await query.next();
1268
- if (done) {
1269
- 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");
1270
1979
  }
1271
- if (!value) {
1272
- 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" };
1273
1994
  }
1274
- // Double-check turn ID after awaiting, in case a new turn started while we waited
1275
- if (this.currentTurnId !== turnId) {
1276
- 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
+ }
1277
2011
  }
1278
- yield value;
1279
- if (value.type === "result") {
1280
- 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
+ };
1281
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" };
1282
2043
  }
2044
+ const autonomousRun = this.createRun("autonomous", null);
2045
+ this.emitRunEvent(autonomousRun, { type: "turn_started", provider: "claude" });
2046
+ return { run: autonomousRun, reason: "fallback" };
1283
2047
  }
1284
- async forwardPromptEvents(message, queue, turnId) {
1285
- // Create a turn-local context to track streaming state.
1286
- // This prevents race conditions when a new stream() call interrupts a running one.
1287
- const turnContext = {
1288
- streamedAssistantTextThisTurn: false,
1289
- streamedReasoningThisTurn: false,
1290
- };
1291
- let completedNormally = false;
1292
- try {
1293
- for await (const sdkEvent of this.processPrompt(message, turnId)) {
1294
- // Check if this turn has been superseded before pushing events
1295
- if (this.currentTurnId !== turnId) {
1296
- 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;
2167
+ }
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;
1297
2177
  }
1298
- const events = this.translateMessageToEvents(sdkEvent, turnContext);
1299
- for (const event of events) {
1300
- queue.push(event);
1301
- if (event.type === "turn_completed") {
1302
- completedNormally = true;
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");
1303
2182
  }
1304
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");
1305
2196
  }
1306
2197
  }
1307
- catch (error) {
1308
- if (!this.turnCancelRequested && this.currentTurnId === turnId) {
1309
- queue.push({
1310
- type: "turn_failed",
1311
- provider: "claude",
1312
- error: error instanceof Error ? error.message : "Claude stream failed",
1313
- });
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);
1314
2219
  }
1315
2220
  }
1316
- finally {
1317
- // Emit terminal event for superseded turns so consumers get proper lifecycle signals.
1318
- // Use turn_canceled (not turn_failed) to distinguish intentional interruption from errors.
1319
- // Only emit if not already emitted by requestCancel() (indicated by turnCancelRequested).
1320
- const wasSuperseded = this.currentTurnId !== turnId;
1321
- if (wasSuperseded && !completedNormally && !this.turnCancelRequested) {
1322
- this.flushPendingToolCalls();
1323
- queue.push({
1324
- type: "turn_canceled",
1325
- provider: "claude",
1326
- reason: "Interrupted by new message",
1327
- });
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;
1328
2321
  }
1329
- this.turnCancelRequested = false;
1330
- queue.end();
2322
+ return true;
2323
+ }
2324
+ if (message.type === "user") {
2325
+ this.suppressLocalReplayActivity = false;
2326
+ return false;
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;
1331
2338
  }
2339
+ return this.localUserMessageIds.has(uuid);
1332
2340
  }
1333
2341
  async interruptActiveTurn() {
1334
2342
  const queryToInterrupt = this.query;
1335
2343
  if (!queryToInterrupt || typeof queryToInterrupt.interrupt !== "function") {
1336
- this.logger.info("interruptActiveTurn: no query to interrupt");
2344
+ this.logger.debug("interruptActiveTurn: no query to interrupt");
1337
2345
  return;
1338
2346
  }
1339
2347
  try {
1340
- this.logger.info("interruptActiveTurn: calling query.interrupt()...");
2348
+ this.logger.debug("interruptActiveTurn: calling query.interrupt()...");
1341
2349
  const t0 = Date.now();
1342
2350
  await queryToInterrupt.interrupt();
1343
- this.logger.info({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
2351
+ this.logger.debug({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
1344
2352
  // After interrupt(), the query iterator is done (returns done: true).
1345
2353
  // Clear it so ensureQuery() creates a fresh query for the next turn.
1346
2354
  // Also end the input stream and call return() to clean up the SDK process.
1347
2355
  this.input?.end();
1348
- this.logger.info("interruptActiveTurn: calling query.return()...");
2356
+ this.logger.debug("interruptActiveTurn: calling query.return()...");
1349
2357
  const t1 = Date.now();
1350
2358
  await queryToInterrupt.return?.();
1351
- this.logger.info({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
2359
+ this.logger.debug({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
1352
2360
  this.query = null;
1353
2361
  this.input = null;
1354
2362
  this.queryRestartNeeded = false;
1355
2363
  }
1356
2364
  catch (error) {
1357
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;
1358
2369
  }
1359
2370
  }
1360
2371
  handleSidechainMessage(message, parentToolUseId) {
1361
- let toolName;
1362
- if (message.type === "assistant") {
1363
- const content = message.message?.content;
1364
- if (Array.isArray(content)) {
1365
- for (const block of content) {
1366
- if (isClaudeContentChunk(block) &&
1367
- (block.type === "tool_use" || block.type === "mcp_tool_use" || block.type === "server_tool_use") &&
1368
- typeof block.name === "string") {
1369
- toolName = block.name;
1370
- break;
1371
- }
1372
- }
1373
- }
1374
- }
1375
- else if (message.type === "stream_event") {
1376
- const event = message.event;
1377
- if (event.type === "content_block_start") {
1378
- const cb = isClaudeContentChunk(event.content_block) ? event.content_block : null;
1379
- if (cb?.type === "tool_use" && typeof cb.name === "string") {
1380
- toolName = cb.name;
1381
- }
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;
1382
2386
  }
1383
2387
  }
1384
- else if (message.type === "tool_progress") {
1385
- toolName = message.tool_name;
1386
- }
1387
- if (!toolName) {
2388
+ if (!contextUpdated && !actionUpdated) {
1388
2389
  return [];
1389
2390
  }
1390
- const prev = this.activeSidechains.get(parentToolUseId);
1391
- if (prev === toolName) {
1392
- return [];
1393
- }
1394
- this.activeSidechains.set(parentToolUseId, toolName);
1395
2391
  const toolCall = mapClaudeRunningToolCall({
1396
2392
  name: "Task",
1397
2393
  callId: parentToolUseId,
1398
2394
  input: null,
1399
2395
  output: null,
1400
- metadata: { subAgentActivity: toolName },
1401
2396
  });
1402
2397
  if (!toolCall) {
1403
2398
  return [];
1404
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
+ };
1405
2415
  return [
1406
2416
  {
1407
2417
  type: "timeline",
1408
- item: toolCall,
2418
+ item: {
2419
+ ...toolCall,
2420
+ detail,
2421
+ },
1409
2422
  provider: "claude",
1410
2423
  },
1411
2424
  ];
1412
2425
  }
1413
- 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) {
1414
2583
  const parentToolUseId = "parent_tool_use_id" in message
1415
2584
  ? message.parent_tool_use_id
1416
2585
  : null;
@@ -1418,6 +2587,14 @@ class ClaudeAgentSession {
1418
2587
  return this.handleSidechainMessage(message, parentToolUseId);
1419
2588
  }
1420
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
+ }
1421
2598
  switch (message.type) {
1422
2599
  case "system":
1423
2600
  if (message.subtype === "init") {
@@ -1442,14 +2619,14 @@ class ClaudeAgentSession {
1442
2619
  }
1443
2620
  }
1444
2621
  else if (message.subtype === "compact_boundary") {
1445
- const meta = message.compact_metadata;
2622
+ const compactMetadata = readCompactionMetadata(message);
1446
2623
  events.push({
1447
2624
  type: "timeline",
1448
2625
  item: {
1449
2626
  type: "compaction",
1450
2627
  status: "completed",
1451
- trigger: meta?.trigger === "manual" ? "manual" : "auto",
1452
- preTokens: meta?.pre_tokens,
2628
+ trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
2629
+ preTokens: compactMetadata?.preTokens,
1453
2630
  },
1454
2631
  provider: "claude",
1455
2632
  });
@@ -1478,7 +2655,7 @@ class ClaudeAgentSession {
1478
2655
  });
1479
2656
  }
1480
2657
  else if (Array.isArray(content)) {
1481
- const timelineItems = this.mapBlocksToTimeline(content, { turnContext });
2658
+ const timelineItems = this.mapBlocksToTimeline(content);
1482
2659
  for (const item of timelineItems) {
1483
2660
  if (item.type === "user_message" && messageId && !item.messageId) {
1484
2661
  events.push({
@@ -1495,9 +2672,8 @@ class ClaudeAgentSession {
1495
2672
  }
1496
2673
  case "assistant": {
1497
2674
  const timelineItems = this.mapBlocksToTimeline(message.message.content, {
1498
- turnContext,
1499
- suppressAssistantText: turnContext.streamedAssistantTextThisTurn,
1500
- suppressReasoning: turnContext.streamedReasoningThisTurn,
2675
+ suppressAssistantText: options?.suppressAssistantText ?? false,
2676
+ suppressReasoning: options?.suppressReasoning ?? false,
1501
2677
  });
1502
2678
  for (const item of timelineItems) {
1503
2679
  events.push({ type: "timeline", item, provider: "claude" });
@@ -1505,13 +2681,19 @@ class ClaudeAgentSession {
1505
2681
  break;
1506
2682
  }
1507
2683
  case "stream_event": {
1508
- 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
+ });
1509
2688
  for (const item of timelineItems) {
1510
2689
  events.push({ type: "timeline", item, provider: "claude" });
1511
2690
  }
1512
2691
  break;
1513
2692
  }
1514
2693
  case "result": {
2694
+ if (options?.suppressTerminalEvents) {
2695
+ break;
2696
+ }
1515
2697
  const usage = this.convertUsage(message);
1516
2698
  if (message.subtype === "success") {
1517
2699
  events.push({ type: "turn_completed", provider: "claude", usage });
@@ -1529,6 +2711,31 @@ class ClaudeAgentSession {
1529
2711
  }
1530
2712
  return events;
1531
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
+ }
1532
2739
  handleSystemMessage(message) {
1533
2740
  if (message.subtype !== "init") {
1534
2741
  return null;
@@ -1608,6 +2815,7 @@ class ClaudeAgentSession {
1608
2815
  }
1609
2816
  }
1610
2817
  this.toolUseCache.clear();
2818
+ this.activeSidechains.clear();
1611
2819
  }
1612
2820
  pushToolCall(item, target) {
1613
2821
  if (!item) {
@@ -1620,9 +2828,19 @@ class ClaudeAgentSession {
1620
2828
  this.enqueueTimeline(item);
1621
2829
  }
1622
2830
  pushEvent(event) {
1623
- if (this.eventQueue) {
1624
- 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;
1625
2842
  }
2843
+ this.liveEventQueue.push(event);
1626
2844
  }
1627
2845
  normalizePermissionUpdates(updates) {
1628
2846
  if (!updates || updates.length === 0) {
@@ -1638,6 +2856,9 @@ class ClaudeAgentSession {
1638
2856
  this.pendingPermissions.delete(id);
1639
2857
  }
1640
2858
  }
2859
+ waitForLiveHistoryPoll() {
2860
+ return new Promise((resolve) => setTimeout(resolve, 250));
2861
+ }
1641
2862
  loadPersistedHistory(sessionId) {
1642
2863
  try {
1643
2864
  const historyPath = this.resolveHistoryPath(sessionId);
@@ -1687,20 +2908,15 @@ class ClaudeAgentSession {
1687
2908
  return path.join(dir, `${sessionId}.jsonl`);
1688
2909
  }
1689
2910
  convertHistoryEntry(entry) {
1690
- return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content, { context: "history" }));
2911
+ return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content));
1691
2912
  }
1692
2913
  mapBlocksToTimeline(content, options) {
1693
- const context = options?.context ?? "live";
1694
- const turnContext = options?.turnContext;
1695
2914
  const suppressAssistant = options?.suppressAssistantText ?? false;
1696
2915
  const suppressReasoning = options?.suppressReasoning ?? false;
1697
2916
  if (typeof content === "string") {
1698
- if (!content || content === "[Request interrupted by user for tool use]") {
2917
+ if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
1699
2918
  return [];
1700
2919
  }
1701
- if (context === "live" && turnContext) {
1702
- turnContext.streamedAssistantTextThisTurn = true;
1703
- }
1704
2920
  if (suppressAssistant) {
1705
2921
  return [];
1706
2922
  }
@@ -1711,10 +2927,7 @@ class ClaudeAgentSession {
1711
2927
  switch (block.type) {
1712
2928
  case "text":
1713
2929
  case "text_delta":
1714
- if (block.text && block.text !== "[Request interrupted by user for tool use]") {
1715
- if (context === "live" && turnContext) {
1716
- turnContext.streamedAssistantTextThisTurn = true;
1717
- }
2930
+ if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
1718
2931
  if (!suppressAssistant) {
1719
2932
  items.push({ type: "assistant_message", text: block.text });
1720
2933
  }
@@ -1723,9 +2936,6 @@ class ClaudeAgentSession {
1723
2936
  case "thinking":
1724
2937
  case "thinking_delta":
1725
2938
  if (block.thinking) {
1726
- if (context === "live" && turnContext) {
1727
- turnContext.streamedReasoningThisTurn = true;
1728
- }
1729
2939
  if (!suppressReasoning) {
1730
2940
  items.push({ type: "reasoning", text: block.thinking });
1731
2941
  }
@@ -1797,6 +3007,7 @@ class ClaudeAgentSession {
1797
3007
  }
1798
3008
  if (typeof block.tool_use_id === "string") {
1799
3009
  this.toolUseCache.delete(block.tool_use_id);
3010
+ this.activeSidechains.delete(block.tool_use_id);
1800
3011
  }
1801
3012
  }
1802
3013
  buildToolOutput(block, entry) {
@@ -1805,7 +3016,7 @@ class ClaudeAgentSession {
1805
3016
  }
1806
3017
  const server = entry?.server ?? block.server ?? "tool";
1807
3018
  const tool = entry?.name ?? block.tool_name ?? "tool";
1808
- const content = typeof block.content === "string" ? block.content : "";
3019
+ const content = coerceToolResultContentToString(block.content);
1809
3020
  const input = entry?.input;
1810
3021
  // Build structured result based on tool type
1811
3022
  const structured = this.buildStructuredToolResult(server, tool, content, input);
@@ -1894,7 +3105,7 @@ class ClaudeAgentSession {
1894
3105
  }
1895
3106
  return undefined;
1896
3107
  }
1897
- mapPartialEvent(event, turnContext) {
3108
+ mapPartialEvent(event, options) {
1898
3109
  if (event.type === "content_block_start") {
1899
3110
  const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
1900
3111
  if (block?.type === "tool_use" && typeof event.index === "number" && typeof block.id === "string") {
@@ -1920,10 +3131,18 @@ class ClaudeAgentSession {
1920
3131
  switch (event.type) {
1921
3132
  case "content_block_start":
1922
3133
  return isClaudeContentChunk(event.content_block)
1923
- ? this.mapBlocksToTimeline([event.content_block], { turnContext })
3134
+ ? this.mapBlocksToTimeline([event.content_block], {
3135
+ suppressAssistantText: options?.suppressAssistantText,
3136
+ suppressReasoning: options?.suppressReasoning,
3137
+ })
1924
3138
  : [];
1925
3139
  case "content_block_delta":
1926
- 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
+ : [];
1927
3146
  default:
1928
3147
  return [];
1929
3148
  }
@@ -2122,6 +3341,24 @@ function hasToolLikeBlock(block) {
2122
3341
  const type = typeof block.type === "string" ? block.type.toLowerCase() : "";
2123
3342
  return type.includes("tool");
2124
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
+ }
2125
3362
  function normalizeHistoryBlocks(content) {
2126
3363
  if (Array.isArray(content)) {
2127
3364
  const blocks = content.filter((entry) => isClaudeContentChunk(entry));
@@ -2134,11 +3371,12 @@ function normalizeHistoryBlocks(content) {
2134
3371
  }
2135
3372
  export function convertClaudeHistoryEntry(entry, mapBlocks) {
2136
3373
  if (entry.type === "system" && entry.subtype === "compact_boundary") {
3374
+ const compactMetadata = readCompactionMetadata(entry);
2137
3375
  return [{
2138
3376
  type: "compaction",
2139
3377
  status: "completed",
2140
- trigger: entry.compactMetadata?.trigger === "manual" ? "manual" : "auto",
2141
- preTokens: entry.compactMetadata?.preTokens,
3378
+ trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
3379
+ preTokens: compactMetadata?.preTokens,
2142
3380
  }];
2143
3381
  }
2144
3382
  if (entry.isCompactSummary) {