@getpaseo/server 0.1.32 → 0.1.34

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 (113) hide show
  1. package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
  2. package/dist/server/server/agent/activity-curator.js +15 -1
  3. package/dist/server/server/agent/activity-curator.js.map +1 -1
  4. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  5. package/dist/server/server/agent/agent-management-mcp.js +2 -4
  6. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  7. package/dist/server/server/agent/agent-manager.d.ts +25 -14
  8. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  9. package/dist/server/server/agent/agent-manager.js +383 -337
  10. package/dist/server/server/agent/agent-manager.js.map +1 -1
  11. package/dist/server/server/agent/agent-sdk-types.d.ts +16 -14
  12. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  13. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  14. package/dist/server/server/agent/mcp-server.js +2 -4
  15. package/dist/server/server/agent/mcp-server.js.map +1 -1
  16. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  17. package/dist/server/server/agent/provider-launch-config.js +15 -1
  18. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  19. package/dist/server/server/agent/provider-manifest.d.ts +1 -1
  20. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  21. package/dist/server/server/agent/provider-manifest.js +4 -4
  22. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  23. package/dist/server/server/agent/providers/claude/partial-json.js +3 -3
  24. package/dist/server/server/agent/providers/claude/partial-json.js.map +1 -1
  25. package/dist/server/server/agent/providers/claude/sdk-model-resolver.d.ts +11 -0
  26. package/dist/server/server/agent/providers/claude/sdk-model-resolver.d.ts.map +1 -0
  27. package/dist/server/server/agent/providers/claude/sdk-model-resolver.js +104 -0
  28. package/dist/server/server/agent/providers/claude/sdk-model-resolver.js.map +1 -0
  29. package/dist/server/server/agent/providers/claude-agent.d.ts +4 -13
  30. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  31. package/dist/server/server/agent/providers/claude-agent.js +382 -451
  32. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  33. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +5 -3
  34. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  35. package/dist/server/server/agent/providers/codex-app-server-agent.js +174 -151
  36. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  37. package/dist/server/server/agent/providers/opencode-agent.d.ts +3 -3
  38. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  39. package/dist/server/server/agent/providers/opencode-agent.js +106 -25
  40. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  41. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts +3 -0
  42. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.d.ts.map +1 -0
  43. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js +57 -0
  44. package/dist/server/server/agent/providers/test-utils/session-stream-adapter.js.map +1 -0
  45. package/dist/server/server/config.d.ts.map +1 -1
  46. package/dist/server/server/config.js +1 -5
  47. package/dist/server/server/config.js.map +1 -1
  48. package/dist/server/server/persisted-config.d.ts +10 -10
  49. package/dist/server/server/session.d.ts.map +1 -1
  50. package/dist/server/server/session.js +20 -12
  51. package/dist/server/server/session.js.map +1 -1
  52. package/dist/server/shared/tool-call-display.js +1 -1
  53. package/dist/server/shared/tool-call-display.js.map +1 -1
  54. package/dist/server/terminal/terminal.d.ts +4 -0
  55. package/dist/server/terminal/terminal.d.ts.map +1 -1
  56. package/dist/server/terminal/terminal.js +7 -30
  57. package/dist/server/terminal/terminal.js.map +1 -1
  58. package/dist/src/server/agent/activity-curator.js +15 -1
  59. package/dist/src/server/agent/activity-curator.js.map +1 -1
  60. package/dist/src/server/agent/agent-manager.js +383 -337
  61. package/dist/src/server/agent/agent-manager.js.map +1 -1
  62. package/dist/src/server/agent/mcp-server.js +2 -4
  63. package/dist/src/server/agent/mcp-server.js.map +1 -1
  64. package/dist/src/server/agent/provider-launch-config.js +15 -1
  65. package/dist/src/server/agent/provider-launch-config.js.map +1 -1
  66. package/dist/src/server/agent/provider-manifest.js +4 -4
  67. package/dist/src/server/agent/provider-manifest.js.map +1 -1
  68. package/dist/src/server/agent/providers/claude/partial-json.js +3 -3
  69. package/dist/src/server/agent/providers/claude/partial-json.js.map +1 -1
  70. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js +104 -0
  71. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js.map +1 -0
  72. package/dist/src/server/agent/providers/claude-agent.js +382 -451
  73. package/dist/src/server/agent/providers/claude-agent.js.map +1 -1
  74. package/dist/src/server/agent/providers/codex-app-server-agent.js +174 -151
  75. package/dist/src/server/agent/providers/codex-app-server-agent.js.map +1 -1
  76. package/dist/src/server/agent/providers/opencode-agent.js +106 -25
  77. package/dist/src/server/agent/providers/opencode-agent.js.map +1 -1
  78. package/dist/src/server/config.js +1 -5
  79. package/dist/src/server/config.js.map +1 -1
  80. package/dist/src/server/session.js +20 -12
  81. package/dist/src/server/session.js.map +1 -1
  82. package/dist/src/shared/tool-call-display.js +1 -1
  83. package/dist/src/shared/tool-call-display.js.map +1 -1
  84. package/dist/src/terminal/terminal.js +7 -30
  85. package/dist/src/terminal/terminal.js.map +1 -1
  86. package/package.json +4 -5
  87. package/agent-prompt.md +0 -339
  88. package/dist/server/server/agent/providers/claude/model-catalog.d.ts +0 -29
  89. package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +0 -1
  90. package/dist/server/server/agent/providers/claude/model-catalog.js +0 -70
  91. package/dist/server/server/agent/providers/claude/model-catalog.js.map +0 -1
  92. package/dist/server/server/agent/system-prompt.d.ts +0 -3
  93. package/dist/server/server/agent/system-prompt.d.ts.map +0 -1
  94. package/dist/server/server/agent/system-prompt.js +0 -19
  95. package/dist/server/server/agent/system-prompt.js.map +0 -1
  96. package/dist/server/server/terminal-mcp/index.d.ts +0 -4
  97. package/dist/server/server/terminal-mcp/index.d.ts.map +0 -1
  98. package/dist/server/server/terminal-mcp/index.js +0 -3
  99. package/dist/server/server/terminal-mcp/index.js.map +0 -1
  100. package/dist/server/server/terminal-mcp/server.d.ts +0 -10
  101. package/dist/server/server/terminal-mcp/server.d.ts.map +0 -1
  102. package/dist/server/server/terminal-mcp/server.js +0 -209
  103. package/dist/server/server/terminal-mcp/server.js.map +0 -1
  104. package/dist/server/server/terminal-mcp/terminal-manager.d.ts +0 -123
  105. package/dist/server/server/terminal-mcp/terminal-manager.d.ts.map +0 -1
  106. package/dist/server/server/terminal-mcp/terminal-manager.js +0 -339
  107. package/dist/server/server/terminal-mcp/terminal-manager.js.map +0 -1
  108. package/dist/server/server/terminal-mcp/tmux.d.ts +0 -207
  109. package/dist/server/server/terminal-mcp/tmux.d.ts.map +0 -1
  110. package/dist/server/server/terminal-mcp/tmux.js +0 -821
  111. package/dist/server/server/terminal-mcp/tmux.js.map +0 -1
  112. package/dist/src/server/agent/providers/claude/model-catalog.js +0 -70
  113. package/dist/src/server/agent/providers/claude/model-catalog.js.map +0 -1
@@ -6,116 +6,14 @@ 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 { coerceTaskNotificationHistoryRecordToSystemMessage, mapTaskNotificationSystemRecordToToolCall, mapTaskNotificationUserContentToToolCall, } from "./claude/task-notification-tool-call.js";
10
- import { buildClaudeModelFamilyAliases, buildClaudeSelectableModelIds, listClaudeCatalogModels, } from "./claude/model-catalog.js";
9
+ import { mapTaskNotificationSystemRecordToToolCall, mapTaskNotificationUserContentToToolCall, } from "./claude/task-notification-tool-call.js";
10
+ import { normalizeClaudeModelIdFromText, resolveClaudeModelsFromSdkModels, } from "./claude/sdk-model-resolver.js";
11
11
  import { parsePartialJsonObject } from "./claude/partial-json.js";
12
12
  import { ClaudeSidechainTracker } from "./claude/sidechain-tracker.js";
13
13
  import { applyProviderEnv } from "../provider-launch-config.js";
14
14
  import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
15
15
  const fsPromises = promises;
16
16
  const CLAUDE_SETTING_SOURCES = ["user", "project"];
17
- function normalizeModelIdCandidate(modelId) {
18
- if (typeof modelId !== "string") {
19
- return null;
20
- }
21
- const trimmed = modelId.trim();
22
- return trimmed.length > 0 ? trimmed : null;
23
- }
24
- function pickSupportedModelId(supportedModelIds, candidate) {
25
- const normalizedCandidate = normalizeModelIdCandidate(candidate);
26
- if (!normalizedCandidate) {
27
- return null;
28
- }
29
- return supportedModelIds.has(normalizedCandidate) ? normalizedCandidate : null;
30
- }
31
- function inferClaudeModelFamilyFromText(text) {
32
- if (typeof text !== "string") {
33
- return null;
34
- }
35
- const lowerText = text.toLowerCase();
36
- if (lowerText.includes("sonnet")) {
37
- return "sonnet";
38
- }
39
- if (lowerText.includes("opus")) {
40
- return "opus";
41
- }
42
- if (lowerText.includes("haiku")) {
43
- return "haiku";
44
- }
45
- return null;
46
- }
47
- function pickFamilyAliasModelId(familyAliases, family) {
48
- if (!familyAliases) {
49
- return null;
50
- }
51
- return normalizeModelIdCandidate(familyAliases.get(family) ?? null);
52
- }
53
- export function normalizeClaudeRuntimeModelId(options) {
54
- const runtimeModel = options.runtimeModelId.trim();
55
- if (!runtimeModel) {
56
- return runtimeModel;
57
- }
58
- const supportedModelIds = options.supportedModelIds;
59
- if (!supportedModelIds || supportedModelIds.size === 0) {
60
- return runtimeModel;
61
- }
62
- if (supportedModelIds.has(runtimeModel)) {
63
- return runtimeModel;
64
- }
65
- const runtimeFamily = inferClaudeModelFamilyFromText(runtimeModel);
66
- const familyAlias = runtimeFamily
67
- ? pickFamilyAliasModelId(options.supportedModelFamilyAliases, runtimeFamily)
68
- : null;
69
- if (runtimeFamily === "sonnet") {
70
- const explicitSonnet = pickSupportedModelId(supportedModelIds, "sonnet");
71
- if (explicitSonnet) {
72
- return explicitSonnet;
73
- }
74
- if (familyAlias && supportedModelIds.has(familyAlias)) {
75
- return familyAlias;
76
- }
77
- const defaultAlias = pickSupportedModelId(supportedModelIds, "default");
78
- if (defaultAlias) {
79
- return defaultAlias;
80
- }
81
- }
82
- if (runtimeFamily === "opus") {
83
- const alias = pickSupportedModelId(supportedModelIds, "opus");
84
- if (alias) {
85
- return alias;
86
- }
87
- if (familyAlias && supportedModelIds.has(familyAlias)) {
88
- return familyAlias;
89
- }
90
- }
91
- if (runtimeFamily === "haiku") {
92
- const alias = pickSupportedModelId(supportedModelIds, "haiku");
93
- if (alias) {
94
- return alias;
95
- }
96
- if (familyAlias && supportedModelIds.has(familyAlias)) {
97
- return familyAlias;
98
- }
99
- }
100
- const configuredModelId = pickSupportedModelId(supportedModelIds, options.configuredModelId);
101
- if (configuredModelId) {
102
- return configuredModelId;
103
- }
104
- const currentModelId = pickSupportedModelId(supportedModelIds, options.currentModelId);
105
- if (currentModelId) {
106
- return currentModelId;
107
- }
108
- // If Claude reports a concrete family ID we can't map directly, prefer the
109
- // provider default alias for unconfigured sessions so UI model/thinking state
110
- // can still reconcile against the current model catalog.
111
- const defaultAlias = pickSupportedModelId(supportedModelIds, "default");
112
- const hasConfiguredModel = normalizeModelIdCandidate(options.configuredModelId) !== null;
113
- const hasCurrentModel = normalizeModelIdCandidate(options.currentModelId) !== null;
114
- if (runtimeFamily && defaultAlias && !hasConfiguredModel && !hasCurrentModel) {
115
- return defaultAlias;
116
- }
117
- return runtimeModel;
118
- }
119
17
  const CLAUDE_CAPABILITIES = {
120
18
  supportsStreaming: true,
121
19
  supportsSessionPersistence: true,
@@ -176,7 +74,7 @@ function resolveClaudeSpawnCommand(spawnOptions, runtimeSettings) {
176
74
  args: [...commandConfig.argv.slice(1), ...spawnOptions.args],
177
75
  };
178
76
  }
179
- function applyRuntimeSettingsToClaudeOptions(options, runtimeSettings) {
77
+ function applyRuntimeSettingsToClaudeOptions(options, runtimeSettings, launchEnv) {
180
78
  return {
181
79
  ...options,
182
80
  spawnClaudeCodeProcess: (spawnOptions) => {
@@ -185,16 +83,33 @@ function applyRuntimeSettingsToClaudeOptions(options, runtimeSettings) {
185
83
  // running from the managed runtime bundle where node isn't in PATH.
186
84
  // Always use process.execPath — the actual node binary running the daemon.
187
85
  const command = resolved.command === spawnOptions.command ? process.execPath : resolved.command;
188
- return spawn(command, resolved.args, {
86
+ const child = spawn(command, resolved.args, {
189
87
  cwd: spawnOptions.cwd,
190
- env: applyProviderEnv(spawnOptions.env, runtimeSettings),
88
+ env: {
89
+ ...applyProviderEnv(spawnOptions.env, runtimeSettings),
90
+ ...(launchEnv ?? {}),
91
+ },
191
92
  signal: spawnOptions.signal,
192
93
  stdio: ["pipe", "pipe", "pipe"],
193
94
  });
95
+ if (typeof options.stderr === "function") {
96
+ child.stderr?.on("data", (chunk) => {
97
+ options.stderr?.(chunk.toString());
98
+ });
99
+ }
100
+ return child;
194
101
  },
195
102
  };
196
103
  }
104
+ function createEmptyClaudePrompt() {
105
+ return (async function* empty() { })();
106
+ }
107
+ function isClaudeThinkingEffort(value) {
108
+ return value === "low" || value === "medium" || value === "high" || value === "max";
109
+ }
197
110
  const MAX_RECENT_STDERR_CHARS = 4000;
111
+ const STDERR_FLUSH_WAIT_MS = 150;
112
+ const STDERR_FLUSH_POLL_INTERVAL_MS = 10;
198
113
  function summarizeClaudeOptionsForLog(options) {
199
114
  const systemPromptRaw = options.systemPrompt;
200
115
  const systemPromptSummary = (() => {
@@ -792,16 +707,17 @@ export class ClaudeAgentClient {
792
707
  this.runtimeSettings = options.runtimeSettings;
793
708
  this.queryFactory = options.queryFactory ?? query;
794
709
  }
795
- async createSession(config) {
710
+ async createSession(config, launchContext) {
796
711
  const claudeConfig = this.assertConfig(config);
797
712
  return new ClaudeAgentSession(claudeConfig, {
798
713
  defaults: this.defaults,
799
714
  runtimeSettings: this.runtimeSettings,
715
+ launchEnv: launchContext?.env,
800
716
  logger: this.logger,
801
717
  queryFactory: this.queryFactory,
802
718
  });
803
719
  }
804
- async resumeSession(handle, overrides) {
720
+ async resumeSession(handle, overrides, launchContext) {
805
721
  const metadata = coerceSessionMetadata(handle.metadata);
806
722
  const merged = { ...metadata, ...overrides };
807
723
  if (!merged.cwd) {
@@ -813,12 +729,37 @@ export class ClaudeAgentClient {
813
729
  defaults: this.defaults,
814
730
  runtimeSettings: this.runtimeSettings,
815
731
  handle,
732
+ launchEnv: launchContext?.env,
816
733
  logger: this.logger,
817
734
  queryFactory: this.queryFactory,
818
735
  });
819
736
  }
820
- async listModels(_options) {
821
- return listClaudeCatalogModels();
737
+ async listModels(options) {
738
+ const claudeQuery = this.queryFactory({
739
+ prompt: createEmptyClaudePrompt(),
740
+ options: applyRuntimeSettingsToClaudeOptions({
741
+ cwd: options?.cwd ?? process.cwd(),
742
+ permissionMode: "plan",
743
+ includePartialMessages: false,
744
+ settingSources: CLAUDE_SETTING_SOURCES,
745
+ }, this.runtimeSettings),
746
+ });
747
+ try {
748
+ const supportedModels = await claudeQuery.supportedModels();
749
+ return resolveClaudeModelsFromSdkModels(supportedModels);
750
+ }
751
+ catch (error) {
752
+ this.logger.warn({ err: error }, "Failed to query Claude supportedModels()");
753
+ throw error;
754
+ }
755
+ finally {
756
+ try {
757
+ await claudeQuery.return?.();
758
+ }
759
+ catch {
760
+ // ignore control-plane shutdown errors
761
+ }
762
+ }
822
763
  }
823
764
  async listPersistedAgents(options) {
824
765
  const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
@@ -865,30 +806,27 @@ class ClaudeAgentSession {
865
806
  this.toolUseIndexToId = new Map();
866
807
  this.toolUseInputBuffers = new Map();
867
808
  this.pendingPermissions = new Map();
868
- this.activeForegroundTurn = null;
809
+ this.activeForegroundTurnId = null;
869
810
  this.autonomousTurn = null;
870
- this.liveEventQueue = new Pushable();
811
+ this.subscribers = new Set();
871
812
  this.timelineAssembler = new TimelineAssembler();
872
813
  this.sidechainTracker = new ClaudeSidechainTracker({
873
814
  getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null,
874
815
  });
875
816
  this.persistedHistory = [];
876
817
  this.historyPending = false;
877
- this.historyOffsetSessionId = null;
878
- this.historyReadOffsetBytes = 0;
879
- this.historyLineFragment = "";
880
818
  this.turnState = "idle";
881
819
  this.nextTurnOrdinal = 1;
882
820
  this.cancelCurrentTurn = null;
883
- this.activeTurnPromise = null;
884
821
  this.cachedRuntimeInfo = null;
885
822
  this.lastOptionsModel = null;
886
- this.selectableModelIds = buildClaudeSelectableModelIds();
887
- this.selectableModelFamilyAliases = buildClaudeModelFamilyAliases();
823
+ this.lastRuntimeModel = null;
888
824
  this.compacting = false;
889
825
  this.queryPumpPromise = null;
890
826
  this.queryRestartNeeded = false;
891
827
  this.pendingInterruptAbort = false;
828
+ this.lastForegroundPromptText = null;
829
+ this.foregroundHasVisibleActivity = false;
892
830
  this.userMessageIds = [];
893
831
  this.recentStderr = "";
894
832
  this.closed = false;
@@ -956,6 +894,7 @@ class ClaudeAgentSession {
956
894
  });
957
895
  };
958
896
  this.config = config;
897
+ this.launchEnv = options.launchEnv;
959
898
  this.defaults = options.defaults;
960
899
  this.runtimeSettings = options.runtimeSettings;
961
900
  this.logger = options.logger;
@@ -992,16 +931,34 @@ class ClaudeAgentSession {
992
931
  sessionId: this.claudeSessionId,
993
932
  model: this.lastOptionsModel,
994
933
  modeId: this.currentMode ?? null,
934
+ ...(this.lastRuntimeModel
935
+ ? {
936
+ extra: {
937
+ runtimeModel: this.lastRuntimeModel,
938
+ },
939
+ }
940
+ : {}),
995
941
  };
996
942
  this.cachedRuntimeInfo = info;
997
943
  return { ...info };
998
944
  }
999
945
  async run(prompt, options) {
1000
- const events = this.stream(prompt, options);
1001
946
  const timeline = [];
1002
947
  let finalText = "";
1003
948
  let usage;
1004
- for await (const event of events) {
949
+ let turnId = null;
950
+ const bufferedEvents = [];
951
+ let settled = false;
952
+ let resolveCompletion;
953
+ let rejectCompletion;
954
+ const processEvent = (event) => {
955
+ if (settled) {
956
+ return;
957
+ }
958
+ const eventTurnId = event.turnId;
959
+ if (turnId && eventTurnId && eventTurnId !== turnId) {
960
+ return;
961
+ }
1005
962
  if (event.type === "timeline") {
1006
963
  timeline.push(event.item);
1007
964
  if (event.item.type === "assistant_message") {
@@ -1015,14 +972,48 @@ class ClaudeAgentSession {
1015
972
  finalText += event.item.text;
1016
973
  }
1017
974
  }
975
+ return;
1018
976
  }
1019
- else if (event.type === "turn_completed") {
977
+ if (event.type === "turn_completed") {
1020
978
  usage = event.usage;
979
+ settled = true;
980
+ resolveCompletion();
981
+ return;
982
+ }
983
+ if (event.type === "turn_failed") {
984
+ settled = true;
985
+ rejectCompletion(new Error(event.error));
986
+ return;
987
+ }
988
+ if (event.type === "turn_canceled") {
989
+ settled = true;
990
+ resolveCompletion();
991
+ }
992
+ };
993
+ const completion = new Promise((resolve, reject) => {
994
+ resolveCompletion = resolve;
995
+ rejectCompletion = reject;
996
+ });
997
+ const unsubscribe = this.subscribe((event) => {
998
+ if (!turnId) {
999
+ bufferedEvents.push(event);
1000
+ return;
1001
+ }
1002
+ processEvent(event);
1003
+ });
1004
+ try {
1005
+ const result = await this.startTurn(prompt, options);
1006
+ turnId = result.turnId;
1007
+ for (const event of bufferedEvents) {
1008
+ processEvent(event);
1021
1009
  }
1022
- else if (event.type === "turn_failed") {
1023
- throw new Error(event.error);
1010
+ if (!settled) {
1011
+ await completion;
1024
1012
  }
1025
1013
  }
1014
+ finally {
1015
+ unsubscribe();
1016
+ }
1026
1017
  this.cachedRuntimeInfo = {
1027
1018
  provider: "claude",
1028
1019
  sessionId: this.claudeSessionId,
@@ -1039,35 +1030,32 @@ class ClaudeAgentSession {
1039
1030
  timeline,
1040
1031
  };
1041
1032
  }
1042
- async *stream(prompt, options) {
1043
- void options;
1044
- if (this.cancelCurrentTurn) {
1045
- this.cancelCurrentTurn();
1033
+ async startTurn(prompt, _options) {
1034
+ if (this.closed) {
1035
+ throw new Error("Claude session is closed");
1036
+ }
1037
+ if (this.activeForegroundTurnId) {
1038
+ throw new Error("A foreground turn is already active");
1046
1039
  }
1047
1040
  const slashCommand = this.resolveSlashCommandInvocation(prompt);
1048
1041
  if (slashCommand?.commandName === REWIND_COMMAND_NAME) {
1049
- yield* this.streamRewindCommand(slashCommand);
1050
- return;
1042
+ const turnId = this.createTurnId("foreground");
1043
+ this.activeForegroundTurnId = turnId;
1044
+ this.transitionTurnState("foreground", "rewind command");
1045
+ void this.executeRewindTurn(turnId, slashCommand);
1046
+ return { turnId };
1051
1047
  }
1052
1048
  if (this.autonomousTurn) {
1053
1049
  this.completeAutonomousTurn();
1054
1050
  }
1055
1051
  const sdkMessage = this.toSdkUserMessage(prompt);
1056
- const queue = new Pushable();
1057
- const foregroundTurn = {
1058
- id: this.createTurnId("foreground"),
1059
- queue,
1060
- hasVisibleActivity: false,
1061
- };
1062
- this.activeForegroundTurn = foregroundTurn;
1063
- this.transitionTurnState("foreground", "foreground stream started");
1052
+ this.lastForegroundPromptText = this.extractPromptText(prompt);
1053
+ const turnId = this.createTurnId("foreground");
1054
+ this.activeForegroundTurnId = turnId;
1055
+ this.foregroundHasVisibleActivity = false;
1056
+ this.transitionTurnState("foreground", "foreground turn started");
1064
1057
  this.clearRecentStderr();
1065
- queue.push({ type: "turn_started", provider: "claude" });
1066
- let finishedNaturally = false;
1067
1058
  let cancelIssued = false;
1068
- let queueDrainedWithoutTerminal = false;
1069
- const turnPromise = Promise.resolve();
1070
- this.activeTurnPromise = turnPromise;
1071
1059
  const requestCancel = () => {
1072
1060
  if (cancelIssued) {
1073
1061
  return;
@@ -1087,6 +1075,7 @@ class ClaudeAgentSession {
1087
1075
  });
1088
1076
  };
1089
1077
  this.cancelCurrentTurn = requestCancel;
1078
+ this.notifySubscribers({ type: "turn_started", provider: "claude" });
1090
1079
  try {
1091
1080
  await this.ensureQuery();
1092
1081
  if (!this.input) {
@@ -1097,39 +1086,14 @@ class ClaudeAgentSession {
1097
1086
  }
1098
1087
  catch (error) {
1099
1088
  this.finishForegroundTurn(this.buildTurnFailedEvent(error instanceof Error ? error.message : "Claude stream failed"));
1100
- finishedNaturally = true;
1101
- }
1102
- try {
1103
- for await (const event of queue) {
1104
- const isTerminalEvent = event.type === "turn_completed" ||
1105
- event.type === "turn_failed" ||
1106
- event.type === "turn_canceled";
1107
- if (isTerminalEvent) {
1108
- finishedNaturally = true;
1109
- }
1110
- yield event;
1111
- if (isTerminalEvent) {
1112
- break;
1113
- }
1114
- }
1115
- if (!finishedNaturally && !cancelIssued) {
1116
- queueDrainedWithoutTerminal = true;
1117
- }
1118
- }
1119
- finally {
1120
- if (!finishedNaturally && !cancelIssued && !queueDrainedWithoutTerminal) {
1121
- requestCancel();
1122
- }
1123
- if (this.activeForegroundTurn === foregroundTurn) {
1124
- this.activeForegroundTurn = null;
1125
- }
1126
- if (this.cancelCurrentTurn === requestCancel) {
1127
- this.cancelCurrentTurn = null;
1128
- }
1129
- if (this.activeTurnPromise === turnPromise) {
1130
- this.activeTurnPromise = null;
1131
- }
1132
1089
  }
1090
+ return { turnId };
1091
+ }
1092
+ subscribe(callback) {
1093
+ this.subscribers.add(callback);
1094
+ return () => {
1095
+ this.subscribers.delete(callback);
1096
+ };
1133
1097
  }
1134
1098
  async interrupt() {
1135
1099
  if (this.cancelCurrentTurn) {
@@ -1137,7 +1101,8 @@ class ClaudeAgentSession {
1137
1101
  return;
1138
1102
  }
1139
1103
  if (this.autonomousTurn) {
1140
- this.cancelAutonomousTurn("Interrupted");
1104
+ this.flushPendingToolCalls();
1105
+ this.completeAutonomousTurn();
1141
1106
  }
1142
1107
  await this.interruptActiveTurn();
1143
1108
  }
@@ -1152,14 +1117,6 @@ class ClaudeAgentSession {
1152
1117
  yield { type: "timeline", item, provider: "claude" };
1153
1118
  }
1154
1119
  }
1155
- async *streamLiveEvents() {
1156
- if (this.claudeSessionId) {
1157
- this.startQueryPump();
1158
- }
1159
- for await (const event of this.liveEventQueue) {
1160
- yield event;
1161
- }
1162
- }
1163
1120
  async getAvailableModes() {
1164
1121
  return this.availableModes;
1165
1122
  }
@@ -1183,6 +1140,7 @@ class ClaudeAgentSession {
1183
1140
  await query.setModel(normalizedModelId ?? undefined);
1184
1141
  this.config.model = normalizedModelId ?? undefined;
1185
1142
  this.lastOptionsModel = normalizedModelId ?? this.lastOptionsModel;
1143
+ this.lastRuntimeModel = null;
1186
1144
  this.cachedRuntimeInfo = null;
1187
1145
  // Model change affects persistence metadata, so invalidate cached handle.
1188
1146
  this.persistence = null;
@@ -1194,11 +1152,8 @@ class ClaudeAgentSession {
1194
1152
  if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") {
1195
1153
  this.config.thinkingOptionId = undefined;
1196
1154
  }
1197
- else if (normalizedThinkingOptionId === "on") {
1198
- this.config.thinkingOptionId = "on";
1199
- }
1200
- else if (normalizedThinkingOptionId === "off") {
1201
- this.config.thinkingOptionId = "off";
1155
+ else if (isClaudeThinkingEffort(normalizedThinkingOptionId)) {
1156
+ this.config.thinkingOptionId = normalizedThinkingOptionId;
1202
1157
  }
1203
1158
  else {
1204
1159
  throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`);
@@ -1269,7 +1224,7 @@ class ClaudeAgentSession {
1269
1224
  provider: "claude",
1270
1225
  sessionId: this.claudeSessionId,
1271
1226
  nativeHandle: this.claudeSessionId,
1272
- metadata: this.config,
1227
+ metadata: { ...this.config },
1273
1228
  };
1274
1229
  return this.persistence;
1275
1230
  }
@@ -1279,20 +1234,19 @@ class ClaudeAgentSession {
1279
1234
  turnState: this.turnState,
1280
1235
  hasQuery: Boolean(this.query),
1281
1236
  hasInput: Boolean(this.input),
1282
- hasActiveForegroundTurn: Boolean(this.activeForegroundTurn),
1237
+ hasActiveForegroundTurnId: Boolean(this.activeForegroundTurnId),
1283
1238
  }, "Claude session close: start");
1284
1239
  this.closed = true;
1285
1240
  this.rejectAllPendingPermissions(new Error("Claude session closed"));
1286
1241
  this.cancelCurrentTurn?.();
1287
- this.activeForegroundTurn?.queue.end();
1288
- this.activeForegroundTurn = null;
1242
+ this.subscribers.clear();
1243
+ this.activeForegroundTurnId = null;
1289
1244
  this.autonomousTurn = null;
1290
1245
  this.cancelCurrentTurn = null;
1291
1246
  this.turnState = "idle";
1292
- this.liveEventQueue.end();
1293
- this.activeTurnPromise = null;
1294
1247
  this.sidechainTracker.clear();
1295
1248
  this.input?.end();
1249
+ this.query?.close?.();
1296
1250
  await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
1297
1251
  await this.awaitWithTimeout(this.query?.return?.(), "close query return");
1298
1252
  this.query = null;
@@ -1343,37 +1297,6 @@ class ClaudeAgentSession {
1343
1297
  ? { commandName, args: rawArgs, rawInput: trimmed }
1344
1298
  : { commandName, rawInput: trimmed };
1345
1299
  }
1346
- async *streamRewindCommand(invocation) {
1347
- yield { type: "turn_started", provider: "claude" };
1348
- try {
1349
- const rewindAttempt = await this.attemptRewind(invocation.args);
1350
- if (!rewindAttempt.messageId || !rewindAttempt.result) {
1351
- yield {
1352
- type: "turn_failed",
1353
- provider: "claude",
1354
- error: rewindAttempt.error ??
1355
- "No prior user message available to rewind. Use /rewind <user_message_uuid>.",
1356
- };
1357
- return;
1358
- }
1359
- yield {
1360
- type: "timeline",
1361
- provider: "claude",
1362
- item: {
1363
- type: "assistant_message",
1364
- text: this.buildRewindSuccessMessage(rewindAttempt.messageId, rewindAttempt.result),
1365
- },
1366
- };
1367
- yield { type: "turn_completed", provider: "claude" };
1368
- }
1369
- catch (error) {
1370
- yield {
1371
- type: "turn_failed",
1372
- provider: "claude",
1373
- error: error instanceof Error ? error.message : "Failed to rewind tracked files",
1374
- };
1375
- }
1376
- }
1377
1300
  buildRewindSuccessMessage(targetUserMessageId, rewindResult) {
1378
1301
  const fileCount = Array.isArray(rewindResult.filesChanged)
1379
1302
  ? rewindResult.filesChanged.length
@@ -1524,22 +1447,28 @@ class ClaudeAgentSession {
1524
1447
  return this.query;
1525
1448
  }
1526
1449
  if (this.queryRestartNeeded && this.query) {
1527
- this.input?.end();
1450
+ const oldQuery = this.query;
1451
+ const oldInput = this.input;
1452
+ // Null out query/input BEFORE awaiting the old iterator's return so the
1453
+ // old pump sees this.query !== activeQuery and skips failActiveTurns.
1454
+ this.query = null;
1455
+ this.input = null;
1456
+ this.queryPumpPromise = null;
1457
+ this.queryRestartNeeded = false;
1458
+ oldInput?.end();
1459
+ oldQuery.close?.();
1528
1460
  try {
1529
- await this.query.return?.();
1461
+ await oldQuery.return?.();
1530
1462
  }
1531
1463
  catch {
1532
1464
  /* ignore */
1533
1465
  }
1534
- this.query = null;
1535
- this.input = null;
1536
- this.queryRestartNeeded = false;
1537
1466
  }
1538
- const input = new Pushable();
1467
+ const input = createAsyncMessageInput();
1539
1468
  const options = this.buildOptions();
1540
1469
  this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query");
1541
1470
  this.input = input;
1542
- this.query = this.queryFactory({ prompt: input, options });
1471
+ this.query = this.queryFactory({ prompt: input.iterable, options });
1543
1472
  // Do not kick off background control-plane queries here. Methods like
1544
1473
  // supportedCommands()/setPermissionMode() may execute immediately after
1545
1474
  // ensureQuery() (for listCommands()/setMode()), and sharing the same query
@@ -1567,16 +1496,14 @@ class ClaudeAgentSession {
1567
1496
  }
1568
1497
  }
1569
1498
  buildOptions() {
1570
- const configuredThinkingOptionId = this.config.thinkingOptionId;
1571
- const thinkingOptionId = configuredThinkingOptionId && configuredThinkingOptionId !== "default"
1572
- ? configuredThinkingOptionId
1573
- : "off";
1574
- let maxThinkingTokens;
1575
- if (thinkingOptionId === "on") {
1576
- maxThinkingTokens = 10000;
1577
- }
1578
- else if (thinkingOptionId === "off") {
1579
- maxThinkingTokens = 0;
1499
+ const thinkingOptionId = this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
1500
+ ? this.config.thinkingOptionId
1501
+ : undefined;
1502
+ let thinking;
1503
+ let effort;
1504
+ if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) {
1505
+ thinking = { type: "adaptive" };
1506
+ effort = thinkingOptionId;
1580
1507
  }
1581
1508
  const appendedSystemPrompt = [
1582
1509
  getOrchestratorModeInstructions(),
@@ -1607,13 +1534,15 @@ class ClaudeAgentSession {
1607
1534
  // Increase MCP timeouts for long-running tool calls (10 minutes)
1608
1535
  MCP_TIMEOUT: "600000",
1609
1536
  MCP_TOOL_TIMEOUT: "600000",
1537
+ ...(this.launchEnv ?? {}),
1610
1538
  },
1611
1539
  // Required for provider-level /rewind support.
1612
1540
  enableFileCheckpointing: true,
1613
1541
  // If we have a session ID from a previous query (e.g., after interrupt),
1614
1542
  // resume that session to continue the conversation history.
1615
1543
  ...(this.claudeSessionId ? { resume: this.claudeSessionId } : {}),
1616
- ...(maxThinkingTokens !== undefined ? { maxThinkingTokens } : {}),
1544
+ ...(thinking ? { thinking } : {}),
1545
+ ...(effort ? { effort } : {}),
1617
1546
  ...this.config.extra?.claude,
1618
1547
  };
1619
1548
  if (this.config.mcpServers) {
@@ -1629,7 +1558,7 @@ class ClaudeAgentSession {
1629
1558
  return this.applyRuntimeSettings(base);
1630
1559
  }
1631
1560
  applyRuntimeSettings(options) {
1632
- return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
1561
+ return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings, this.launchEnv);
1633
1562
  }
1634
1563
  normalizeMcpServers(servers) {
1635
1564
  const result = {};
@@ -1681,7 +1610,7 @@ class ClaudeAgentSession {
1681
1610
  this.turnState = next;
1682
1611
  }
1683
1612
  syncTurnState(reason) {
1684
- if (this.activeForegroundTurn) {
1613
+ if (this.activeForegroundTurnId) {
1685
1614
  this.transitionTurnState("foreground", reason);
1686
1615
  return;
1687
1616
  }
@@ -1691,6 +1620,10 @@ class ClaudeAgentSession {
1691
1620
  }
1692
1621
  this.transitionTurnState("idle", reason);
1693
1622
  }
1623
+ isAbortError(message) {
1624
+ const errors = "errors" in message && Array.isArray(message.errors) ? message.errors : [];
1625
+ return errors.some((e) => /\baborted\b/i.test(e));
1626
+ }
1694
1627
  buildTurnFailedEvent(errorMessage) {
1695
1628
  const normalized = errorMessage.trim() || "Claude run failed";
1696
1629
  const exitCodeMatch = normalized.match(/\bcode\s+(\d+)\b/i);
@@ -1718,6 +1651,22 @@ class ClaudeAgentSession {
1718
1651
  getRecentStderrDiagnostic() {
1719
1652
  return this.recentStderr.trim() || undefined;
1720
1653
  }
1654
+ async awaitRecentStderrAfterProcessExit(error) {
1655
+ if (this.getRecentStderrDiagnostic()) {
1656
+ return;
1657
+ }
1658
+ const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
1659
+ if (!/\bprocess exited with code\b/i.test(message) && !/\bterminated by signal\b/i.test(message)) {
1660
+ return;
1661
+ }
1662
+ const startedAt = Date.now();
1663
+ while (!this.closed && !this.getRecentStderrDiagnostic()) {
1664
+ if (Date.now() - startedAt >= STDERR_FLUSH_WAIT_MS) {
1665
+ return;
1666
+ }
1667
+ await new Promise((resolve) => setTimeout(resolve, STDERR_FLUSH_POLL_INTERVAL_MS));
1668
+ }
1669
+ }
1721
1670
  createTurnId(owner) {
1722
1671
  return `${owner}-turn-${this.nextTurnOrdinal++}`;
1723
1672
  }
@@ -1726,6 +1675,46 @@ class ClaudeAgentSession {
1726
1675
  event.type === "turn_failed" ||
1727
1676
  event.type === "turn_canceled");
1728
1677
  }
1678
+ extractPromptText(prompt) {
1679
+ if (typeof prompt === "string") {
1680
+ return prompt;
1681
+ }
1682
+ const textParts = prompt
1683
+ .filter((block) => block.type === "text")
1684
+ .map((block) => block.text);
1685
+ return textParts.length > 0 ? textParts.join("\n") : null;
1686
+ }
1687
+ async executeRewindTurn(_turnId, invocation) {
1688
+ this.notifySubscribers({ type: "turn_started", provider: "claude" });
1689
+ try {
1690
+ const rewindAttempt = await this.attemptRewind(invocation.args);
1691
+ if (!rewindAttempt.messageId || !rewindAttempt.result) {
1692
+ this.finishForegroundTurn({
1693
+ type: "turn_failed",
1694
+ provider: "claude",
1695
+ error: rewindAttempt.error ??
1696
+ "No prior user message available to rewind. Use /rewind <user_message_uuid>.",
1697
+ });
1698
+ return;
1699
+ }
1700
+ this.notifySubscribers({
1701
+ type: "timeline",
1702
+ provider: "claude",
1703
+ item: {
1704
+ type: "assistant_message",
1705
+ text: this.buildRewindSuccessMessage(rewindAttempt.messageId, rewindAttempt.result),
1706
+ },
1707
+ });
1708
+ this.finishForegroundTurn({ type: "turn_completed", provider: "claude" });
1709
+ }
1710
+ catch (error) {
1711
+ this.finishForegroundTurn({
1712
+ type: "turn_failed",
1713
+ provider: "claude",
1714
+ error: error instanceof Error ? error.message : "Failed to rewind tracked files",
1715
+ });
1716
+ }
1717
+ }
1729
1718
  shouldRecoverInterruptedQueryAbort(error, consecutiveRecoveries) {
1730
1719
  if (consecutiveRecoveries >= 3) {
1731
1720
  return false;
@@ -1741,37 +1730,29 @@ class ClaudeAgentSession {
1741
1730
  if (event.type === "turn_failed" || event.type === "turn_canceled") {
1742
1731
  this.flushPendingToolCalls();
1743
1732
  }
1744
- this.dispatchForegroundEvents([event]);
1745
- }
1746
- dispatchForegroundEvents(events) {
1747
- const foregroundTurn = this.activeForegroundTurn;
1748
- if (!foregroundTurn) {
1749
- this.dispatchLiveEvents(events);
1750
- return;
1751
- }
1752
- let terminalSeen = false;
1753
- for (const event of events) {
1754
- foregroundTurn.queue.push(event);
1755
- terminalSeen || (terminalSeen = this.isTerminalTurnEvent(event));
1756
- }
1757
- if (!terminalSeen) {
1758
- return;
1759
- }
1760
- foregroundTurn.queue.end();
1761
- if (this.activeForegroundTurn === foregroundTurn) {
1762
- this.activeForegroundTurn = null;
1763
- }
1733
+ this.notifySubscribers(event);
1734
+ this.activeForegroundTurnId = null;
1735
+ this.lastForegroundPromptText = null;
1736
+ this.cancelCurrentTurn = null;
1764
1737
  this.syncTurnState("foreground turn terminal");
1765
1738
  }
1766
- dispatchLiveEvents(events) {
1739
+ dispatchEvents(events) {
1767
1740
  let terminalSeen = false;
1768
1741
  for (const event of events) {
1769
- this.liveEventQueue.push(event);
1742
+ this.notifySubscribers(event);
1770
1743
  terminalSeen || (terminalSeen = this.isTerminalTurnEvent(event));
1771
1744
  }
1772
- if (terminalSeen && this.autonomousTurn) {
1773
- this.autonomousTurn = null;
1774
- this.syncTurnState("autonomous turn terminal");
1745
+ if (terminalSeen) {
1746
+ if (this.activeForegroundTurnId) {
1747
+ this.activeForegroundTurnId = null;
1748
+ this.lastForegroundPromptText = null;
1749
+ this.cancelCurrentTurn = null;
1750
+ this.syncTurnState("foreground turn terminal");
1751
+ }
1752
+ else if (this.autonomousTurn) {
1753
+ this.autonomousTurn = null;
1754
+ this.syncTurnState("autonomous turn terminal");
1755
+ }
1775
1756
  }
1776
1757
  }
1777
1758
  startAutonomousTurn() {
@@ -1781,40 +1762,26 @@ class ClaudeAgentSession {
1781
1762
  this.autonomousTurn = {
1782
1763
  id: this.createTurnId("autonomous"),
1783
1764
  };
1784
- this.liveEventQueue.push({ type: "turn_started", provider: "claude" });
1765
+ this.notifySubscribers({ type: "turn_started", provider: "claude" });
1785
1766
  this.syncTurnState("autonomous turn started");
1786
1767
  }
1787
1768
  completeAutonomousTurn() {
1788
1769
  if (!this.autonomousTurn) {
1789
1770
  return;
1790
1771
  }
1772
+ this.notifySubscribers({ type: "turn_completed", provider: "claude" });
1791
1773
  this.autonomousTurn = null;
1792
- this.liveEventQueue.push({ type: "turn_completed", provider: "claude" });
1793
1774
  this.syncTurnState("autonomous turn completed");
1794
1775
  }
1795
- cancelAutonomousTurn(reason) {
1796
- if (!this.autonomousTurn) {
1797
- return;
1798
- }
1799
- this.flushPendingToolCalls();
1800
- this.autonomousTurn = null;
1801
- this.liveEventQueue.push({
1802
- type: "turn_canceled",
1803
- provider: "claude",
1804
- reason,
1805
- });
1806
- this.syncTurnState("autonomous turn canceled");
1807
- }
1808
1776
  failActiveTurns(errorMessage) {
1809
1777
  const failure = this.buildTurnFailedEvent(errorMessage);
1810
- if (this.activeForegroundTurn) {
1811
- this.flushPendingToolCalls();
1812
- this.dispatchForegroundEvents([failure]);
1778
+ this.flushPendingToolCalls();
1779
+ if (this.activeForegroundTurnId) {
1780
+ this.finishForegroundTurn(failure);
1813
1781
  return;
1814
1782
  }
1815
1783
  if (this.autonomousTurn) {
1816
- this.flushPendingToolCalls();
1817
- this.dispatchLiveEvents([failure]);
1784
+ this.dispatchEvents([failure]);
1818
1785
  }
1819
1786
  }
1820
1787
  startQueryPump() {
@@ -1846,6 +1813,12 @@ class ClaudeAgentSession {
1846
1813
  while (!this.closed && this.query === activeQuery) {
1847
1814
  try {
1848
1815
  for await (const message of activeQuery) {
1816
+ this.logger.trace({
1817
+ claudeSessionId: this.claudeSessionId,
1818
+ messageType: message.type,
1819
+ messageSubtype: "subtype" in message ? message.subtype : undefined,
1820
+ messageUuid: "uuid" in message ? message.uuid : undefined,
1821
+ }, "Claude query pump: raw SDK message");
1849
1822
  consecutiveInterruptAbortRecoveries = 0;
1850
1823
  if (await this.handleMissingResumedConversation(message, activeQuery)) {
1851
1824
  return;
@@ -1866,6 +1839,7 @@ class ClaudeAgentSession {
1866
1839
  continue;
1867
1840
  }
1868
1841
  if (!this.closed && this.query === activeQuery) {
1842
+ await this.awaitRecentStderrAfterProcessExit(error);
1869
1843
  this.failActiveTurns(error instanceof Error ? error.message : "Claude stream failed");
1870
1844
  }
1871
1845
  return;
@@ -1880,24 +1854,40 @@ class ClaudeAgentSession {
1880
1854
  }
1881
1855
  }
1882
1856
  routeSdkMessageFromPump(message) {
1883
- const routeToForeground = Boolean(this.activeForegroundTurn);
1857
+ // Suppress stale results from interrupted requests. The cancel path already
1858
+ // emitted the terminal event; this result is leftover from the killed API
1859
+ // request. Consume the flag on ANY result so it doesn't linger.
1860
+ if (message.type === "result" && this.pendingInterruptAbort) {
1861
+ this.pendingInterruptAbort = false;
1862
+ if (message.subtype !== "success") {
1863
+ this.logger.debug("Suppressing stale non-success result from interrupted request");
1864
+ return;
1865
+ }
1866
+ }
1867
+ if (message.type === "result" &&
1868
+ message.subtype !== "success" &&
1869
+ this.isAbortError(message)) {
1870
+ this.logger.debug("Suppressing abort result by content");
1871
+ return;
1872
+ }
1873
+ const isForeground = Boolean(this.activeForegroundTurnId);
1884
1874
  const assistantishMessage = message.type === "assistant" ||
1885
1875
  message.type === "stream_event" ||
1886
- message.type === "tool_progress";
1887
- if (!routeToForeground && assistantishMessage) {
1876
+ message.type === "tool_progress" ||
1877
+ (message.type === "system" && message.subtype === "task_notification");
1878
+ if (!isForeground && assistantishMessage) {
1888
1879
  this.startAutonomousTurn();
1889
1880
  }
1890
- if (!routeToForeground && !this.autonomousTurn && message.type === "result") {
1881
+ if (!isForeground && !this.autonomousTurn && message.type === "result") {
1891
1882
  return;
1892
1883
  }
1893
- const turnId = this.activeForegroundTurn?.id ?? this.autonomousTurn?.id ?? null;
1884
+ const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
1894
1885
  const identifiers = readEventIdentifiers(message);
1895
1886
  this.logger.trace({
1896
1887
  claudeSessionId: this.claudeSessionId,
1897
1888
  messageType: message.type,
1898
- routedTo: routeToForeground ? "foreground_queue" : "live_queue",
1899
1889
  turnId,
1900
- }, "Claude query pump routed SDK message");
1890
+ }, "Claude query pump: SDK message");
1901
1891
  const messageEvents = this.translateMessageToEvents(message, {
1902
1892
  suppressAssistantText: true,
1903
1893
  suppressReasoning: true,
@@ -1913,30 +1903,37 @@ class ClaudeAgentSession {
1913
1903
  item,
1914
1904
  provider: "claude",
1915
1905
  }));
1916
- const events = [...messageEvents, ...assistantTimelineEvents];
1906
+ // User message dedup: suppress echoed user messages that match the foreground prompt
1907
+ const filteredMessageEvents = messageEvents.filter((event) => {
1908
+ if (event.type === "timeline" &&
1909
+ event.item.type === "user_message" &&
1910
+ this.activeForegroundTurnId &&
1911
+ this.lastForegroundPromptText) {
1912
+ if (event.item.text.trim() === this.lastForegroundPromptText.trim()) {
1913
+ return false;
1914
+ }
1915
+ }
1916
+ return true;
1917
+ });
1918
+ const events = [...filteredMessageEvents, ...assistantTimelineEvents];
1917
1919
  if (events.length === 0) {
1918
1920
  return;
1919
1921
  }
1920
1922
  if (this.pendingInterruptAbort &&
1921
1923
  message.type === "result" &&
1922
1924
  events.some((event) => event.type === "turn_completed" || event.type === "turn_failed") &&
1923
- (!this.activeForegroundTurn || !this.activeForegroundTurn.hasVisibleActivity)) {
1925
+ (!this.activeForegroundTurnId || !this.foregroundHasVisibleActivity)) {
1924
1926
  this.pendingInterruptAbort = false;
1925
1927
  this.logger.debug("Suppressing stale Claude interrupt terminal result");
1926
1928
  return;
1927
1929
  }
1928
- if (this.activeForegroundTurn &&
1930
+ if (this.activeForegroundTurnId &&
1929
1931
  events.some((event) => event.type === "timeline" ||
1930
1932
  event.type === "permission_requested" ||
1931
1933
  event.type === "permission_resolved")) {
1932
- this.activeForegroundTurn.hasVisibleActivity = true;
1933
- this.pendingInterruptAbort = false;
1934
+ this.foregroundHasVisibleActivity = true;
1934
1935
  }
1935
- if (routeToForeground) {
1936
- this.dispatchForegroundEvents(events);
1937
- return;
1938
- }
1939
- this.dispatchLiveEvents(events);
1936
+ this.dispatchEvents(events);
1940
1937
  }
1941
1938
  async handleMissingResumedConversation(message, query) {
1942
1939
  const staleResumeError = this.readMissingResumedConversationError(message);
@@ -1958,13 +1955,10 @@ class ClaudeAgentSession {
1958
1955
  this.persistence = null;
1959
1956
  this.persistedHistory = [];
1960
1957
  this.historyPending = false;
1961
- this.historyOffsetSessionId = null;
1962
- this.historyReadOffsetBytes = 0;
1963
- this.historyLineFragment = "";
1964
1958
  this.cachedRuntimeInfo = null;
1965
1959
  this.queryRestartNeeded = false;
1966
1960
  this.autonomousTurn = null;
1967
- this.activeForegroundTurn = null;
1961
+ this.activeForegroundTurnId = null;
1968
1962
  this.syncTurnState("missing resumed conversation");
1969
1963
  return true;
1970
1964
  }
@@ -2197,15 +2191,15 @@ class ClaudeAgentSession {
2197
2191
  this.currentMode = message.permissionMode;
2198
2192
  this.persistence = null;
2199
2193
  if (message.model) {
2200
- const normalizedModel = normalizeClaudeRuntimeModelId({
2201
- runtimeModelId: message.model,
2202
- supportedModelIds: this.selectableModelIds,
2203
- supportedModelFamilyAliases: this.selectableModelFamilyAliases,
2204
- configuredModelId: this.config.model ?? null,
2205
- currentModelId: this.lastOptionsModel,
2206
- });
2207
- this.logger.debug({ model: message.model, normalizedModel }, "Captured model from SDK init");
2208
- this.lastOptionsModel = normalizedModel;
2194
+ const normalizedRuntimeModel = normalizeClaudeModelIdFromText(message.model);
2195
+ this.logger.debug({ runtimeModel: message.model, normalizedRuntimeModel }, "Captured runtime model from SDK init");
2196
+ if (normalizedRuntimeModel) {
2197
+ this.lastOptionsModel = normalizedRuntimeModel;
2198
+ }
2199
+ else if (!this.lastOptionsModel) {
2200
+ this.lastOptionsModel = this.config.model ?? null;
2201
+ }
2202
+ this.lastRuntimeModel = message.model;
2209
2203
  this.cachedRuntimeInfo = null;
2210
2204
  }
2211
2205
  return threadStartedSessionId;
@@ -2271,12 +2265,19 @@ class ClaudeAgentSession {
2271
2265
  this.enqueueTimeline(item);
2272
2266
  }
2273
2267
  pushEvent(event) {
2274
- const foregroundTurn = this.activeForegroundTurn;
2275
- if (foregroundTurn) {
2276
- foregroundTurn.queue.push(event);
2277
- return;
2268
+ this.notifySubscribers(event);
2269
+ }
2270
+ notifySubscribers(event) {
2271
+ const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id;
2272
+ const tagged = turnId ? { ...event, turnId } : event;
2273
+ for (const callback of this.subscribers) {
2274
+ try {
2275
+ callback(tagged);
2276
+ }
2277
+ catch (error) {
2278
+ this.logger.warn({ err: error }, "Subscriber callback threw");
2279
+ }
2278
2280
  }
2279
- this.liveEventQueue.push(event);
2280
2281
  }
2281
2282
  normalizePermissionUpdates(updates) {
2282
2283
  if (!updates || updates.length === 0) {
@@ -2292,123 +2293,52 @@ class ClaudeAgentSession {
2292
2293
  this.pendingPermissions.delete(id);
2293
2294
  }
2294
2295
  }
2295
- loadPersistedHistory(sessionId, options) {
2296
+ loadPersistedHistory(sessionId) {
2296
2297
  try {
2297
2298
  const historyPath = this.resolveHistoryPath(sessionId);
2298
2299
  if (!historyPath || !fs.existsSync(historyPath)) {
2299
2300
  return;
2300
2301
  }
2301
- if (this.historyOffsetSessionId !== sessionId) {
2302
- this.historyOffsetSessionId = sessionId;
2303
- this.historyReadOffsetBytes = 0;
2304
- this.historyLineFragment = "";
2305
- }
2306
- const content = fs.readFileSync(historyPath);
2307
- if (content.byteLength < this.historyReadOffsetBytes) {
2308
- this.historyReadOffsetBytes = 0;
2309
- this.historyLineFragment = "";
2310
- }
2311
- if (content.byteLength === this.historyReadOffsetBytes) {
2312
- return;
2313
- }
2314
- const unreadChunk = content.subarray(this.historyReadOffsetBytes).toString("utf8");
2315
- this.historyReadOffsetBytes = content.byteLength;
2316
- this.ingestPersistedHistoryChunk(unreadChunk, {
2317
- dispatchLive: options?.dispatchLive ?? false,
2318
- });
2302
+ this.ingestPersistedHistory(fs.readFileSync(historyPath, "utf8"));
2319
2303
  }
2320
2304
  catch (error) {
2321
2305
  // ignore history load failures
2322
2306
  }
2323
2307
  }
2324
- ingestPersistedHistoryChunk(chunk, options) {
2325
- if (!chunk) {
2308
+ ingestPersistedHistory(content) {
2309
+ if (!content) {
2326
2310
  return;
2327
2311
  }
2328
- const combined = `${this.historyLineFragment}${chunk}`;
2329
- this.historyLineFragment = "";
2330
- const lines = combined.split(/\r?\n/);
2331
- const trailing = lines.pop() ?? "";
2332
2312
  const timeline = [];
2333
- for (const line of lines) {
2334
- this.ingestPersistedHistoryLine(line, {
2335
- dispatchLive: options.dispatchLive,
2336
- timeline,
2337
- });
2313
+ for (const line of content.split(/\r?\n/)) {
2314
+ this.ingestPersistedHistoryLine(line, timeline);
2338
2315
  }
2339
- if (trailing.trim().length > 0) {
2340
- const handled = this.ingestPersistedHistoryLine(trailing, {
2341
- dispatchLive: options.dispatchLive,
2342
- timeline,
2343
- });
2344
- if (!handled) {
2345
- this.historyLineFragment = trailing;
2346
- }
2347
- }
2348
- if (!options.dispatchLive && timeline.length > 0) {
2316
+ if (timeline.length > 0) {
2349
2317
  this.persistedHistory = [...this.persistedHistory, ...timeline];
2350
2318
  this.historyPending = true;
2351
2319
  }
2352
2320
  }
2353
- ingestPersistedHistoryLine(line, options) {
2321
+ ingestPersistedHistoryLine(line, timeline) {
2354
2322
  const trimmed = line.trim();
2355
2323
  if (!trimmed) {
2356
- return true;
2324
+ return;
2357
2325
  }
2358
2326
  let entry;
2359
2327
  try {
2360
2328
  entry = JSON.parse(trimmed);
2361
2329
  }
2362
2330
  catch {
2363
- return false;
2331
+ return;
2364
2332
  }
2365
2333
  if (entry.isSidechain) {
2366
- return true;
2334
+ return;
2367
2335
  }
2368
2336
  if (entry.type === "user" && typeof entry.uuid === "string") {
2369
2337
  this.rememberUserMessageId(entry.uuid);
2370
2338
  }
2371
- if (options.dispatchLive) {
2372
- this.dispatchPersistedHistoryEntry(entry);
2373
- return true;
2374
- }
2375
2339
  const items = this.convertHistoryEntry(entry);
2376
2340
  if (items.length > 0) {
2377
- options.timeline.push(...items);
2378
- }
2379
- return true;
2380
- }
2381
- dispatchPersistedHistoryEntry(entry) {
2382
- const liveMessage = this.normalizePersistedHistoryEntryToLiveMessage(entry);
2383
- if (liveMessage) {
2384
- this.routeSdkMessageFromPump(liveMessage);
2385
- return;
2386
- }
2387
- const items = this.convertHistoryEntry(entry);
2388
- for (const item of items) {
2389
- this.pushEvent({
2390
- type: "timeline",
2391
- item,
2392
- provider: "claude",
2393
- });
2394
- }
2395
- }
2396
- normalizePersistedHistoryEntryToLiveMessage(entry) {
2397
- const taskNotificationMessage = coerceTaskNotificationHistoryRecordToSystemMessage(entry);
2398
- if (taskNotificationMessage) {
2399
- return taskNotificationMessage;
2400
- }
2401
- const type = readTrimmedString(entry.type);
2402
- switch (type) {
2403
- case "assistant":
2404
- case "result":
2405
- case "stream_event":
2406
- case "system":
2407
- case "tool_progress":
2408
- case "user":
2409
- return entry;
2410
- default:
2411
- return null;
2341
+ timeline.push(...items);
2412
2342
  }
2413
2343
  }
2414
2344
  resolveHistoryPath(sessionId) {
@@ -3016,49 +2946,50 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
3016
2946
  }
3017
2947
  return timeline;
3018
2948
  }
3019
- class Pushable {
3020
- constructor() {
3021
- this.queue = [];
3022
- this.resolvers = [];
3023
- this.closed = false;
3024
- }
3025
- push(item) {
3026
- if (this.closed) {
3027
- return;
3028
- }
3029
- if (this.resolvers.length > 0) {
3030
- const resolve = this.resolvers.shift();
3031
- resolve({ value: item, done: false });
3032
- }
3033
- else {
3034
- this.queue.push(item);
3035
- }
3036
- }
3037
- end() {
3038
- this.closed = true;
3039
- while (this.resolvers.length > 0) {
3040
- const resolve = this.resolvers.shift();
3041
- resolve({ value: undefined, done: true });
3042
- }
3043
- }
3044
- [Symbol.asyncIterator]() {
3045
- return {
3046
- next: () => {
3047
- if (this.queue.length > 0) {
3048
- const value = this.queue.shift();
3049
- if (value !== undefined) {
3050
- return Promise.resolve({ value, done: false });
3051
- }
3052
- }
3053
- if (this.closed) {
3054
- return Promise.resolve({ value: undefined, done: true });
3055
- }
3056
- return new Promise((resolve) => {
3057
- this.resolvers.push(resolve);
3058
- });
2949
+ function createAsyncMessageInput() {
2950
+ const queue = [];
2951
+ const resolvers = [];
2952
+ let closed = false;
2953
+ return {
2954
+ push(item) {
2955
+ if (closed) {
2956
+ return;
2957
+ }
2958
+ const resolve = resolvers.shift();
2959
+ if (resolve) {
2960
+ resolve({ value: item, done: false });
2961
+ return;
2962
+ }
2963
+ queue.push(item);
2964
+ },
2965
+ end() {
2966
+ closed = true;
2967
+ while (resolvers.length > 0) {
2968
+ const resolve = resolvers.shift();
2969
+ resolve?.({ value: undefined, done: true });
2970
+ }
2971
+ },
2972
+ iterable: {
2973
+ [Symbol.asyncIterator]() {
2974
+ return {
2975
+ next: () => {
2976
+ if (queue.length > 0) {
2977
+ const value = queue.shift();
2978
+ if (value !== undefined) {
2979
+ return Promise.resolve({ value, done: false });
2980
+ }
2981
+ }
2982
+ if (closed) {
2983
+ return Promise.resolve({ value: undefined, done: true });
2984
+ }
2985
+ return new Promise((resolve) => {
2986
+ resolvers.push(resolve);
2987
+ });
2988
+ },
2989
+ };
3059
2990
  },
3060
- };
3061
- }
2991
+ },
2992
+ };
3062
2993
  }
3063
2994
  async function pathExists(target) {
3064
2995
  try {