@getpaseo/server 0.1.33 → 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 (106) 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 -453
  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/session.d.ts.map +1 -1
  49. package/dist/server/server/session.js +14 -12
  50. package/dist/server/server/session.js.map +1 -1
  51. package/dist/server/shared/tool-call-display.js +1 -1
  52. package/dist/server/shared/tool-call-display.js.map +1 -1
  53. package/dist/src/server/agent/activity-curator.js +15 -1
  54. package/dist/src/server/agent/activity-curator.js.map +1 -1
  55. package/dist/src/server/agent/agent-manager.js +383 -337
  56. package/dist/src/server/agent/agent-manager.js.map +1 -1
  57. package/dist/src/server/agent/mcp-server.js +2 -4
  58. package/dist/src/server/agent/mcp-server.js.map +1 -1
  59. package/dist/src/server/agent/provider-launch-config.js +15 -1
  60. package/dist/src/server/agent/provider-launch-config.js.map +1 -1
  61. package/dist/src/server/agent/provider-manifest.js +4 -4
  62. package/dist/src/server/agent/provider-manifest.js.map +1 -1
  63. package/dist/src/server/agent/providers/claude/partial-json.js +3 -3
  64. package/dist/src/server/agent/providers/claude/partial-json.js.map +1 -1
  65. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js +104 -0
  66. package/dist/src/server/agent/providers/claude/sdk-model-resolver.js.map +1 -0
  67. package/dist/src/server/agent/providers/claude-agent.js +382 -453
  68. package/dist/src/server/agent/providers/claude-agent.js.map +1 -1
  69. package/dist/src/server/agent/providers/codex-app-server-agent.js +174 -151
  70. package/dist/src/server/agent/providers/codex-app-server-agent.js.map +1 -1
  71. package/dist/src/server/agent/providers/opencode-agent.js +106 -25
  72. package/dist/src/server/agent/providers/opencode-agent.js.map +1 -1
  73. package/dist/src/server/config.js +1 -5
  74. package/dist/src/server/config.js.map +1 -1
  75. package/dist/src/server/session.js +14 -12
  76. package/dist/src/server/session.js.map +1 -1
  77. package/dist/src/shared/tool-call-display.js +1 -1
  78. package/dist/src/shared/tool-call-display.js.map +1 -1
  79. package/package.json +4 -5
  80. package/agent-prompt.md +0 -339
  81. package/dist/server/server/agent/providers/claude/model-catalog.d.ts +0 -29
  82. package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +0 -1
  83. package/dist/server/server/agent/providers/claude/model-catalog.js +0 -70
  84. package/dist/server/server/agent/providers/claude/model-catalog.js.map +0 -1
  85. package/dist/server/server/agent/system-prompt.d.ts +0 -3
  86. package/dist/server/server/agent/system-prompt.d.ts.map +0 -1
  87. package/dist/server/server/agent/system-prompt.js +0 -19
  88. package/dist/server/server/agent/system-prompt.js.map +0 -1
  89. package/dist/server/server/terminal-mcp/index.d.ts +0 -4
  90. package/dist/server/server/terminal-mcp/index.d.ts.map +0 -1
  91. package/dist/server/server/terminal-mcp/index.js +0 -3
  92. package/dist/server/server/terminal-mcp/index.js.map +0 -1
  93. package/dist/server/server/terminal-mcp/server.d.ts +0 -10
  94. package/dist/server/server/terminal-mcp/server.d.ts.map +0 -1
  95. package/dist/server/server/terminal-mcp/server.js +0 -209
  96. package/dist/server/server/terminal-mcp/server.js.map +0 -1
  97. package/dist/server/server/terminal-mcp/terminal-manager.d.ts +0 -123
  98. package/dist/server/server/terminal-mcp/terminal-manager.d.ts.map +0 -1
  99. package/dist/server/server/terminal-mcp/terminal-manager.js +0 -339
  100. package/dist/server/server/terminal-mcp/terminal-manager.js.map +0 -1
  101. package/dist/server/server/terminal-mcp/tmux.d.ts +0 -207
  102. package/dist/server/server/terminal-mcp/tmux.d.ts.map +0 -1
  103. package/dist/server/server/terminal-mcp/tmux.js +0 -821
  104. package/dist/server/server/terminal-mcp/tmux.js.map +0 -1
  105. package/dist/src/server/agent/providers/claude/model-catalog.js +0 -70
  106. 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,21 +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();
1296
- this.query?.close();
1249
+ this.query?.close?.();
1297
1250
  await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
1298
1251
  await this.awaitWithTimeout(this.query?.return?.(), "close query return");
1299
1252
  this.query = null;
@@ -1344,37 +1297,6 @@ class ClaudeAgentSession {
1344
1297
  ? { commandName, args: rawArgs, rawInput: trimmed }
1345
1298
  : { commandName, rawInput: trimmed };
1346
1299
  }
1347
- async *streamRewindCommand(invocation) {
1348
- yield { type: "turn_started", provider: "claude" };
1349
- try {
1350
- const rewindAttempt = await this.attemptRewind(invocation.args);
1351
- if (!rewindAttempt.messageId || !rewindAttempt.result) {
1352
- yield {
1353
- type: "turn_failed",
1354
- provider: "claude",
1355
- error: rewindAttempt.error ??
1356
- "No prior user message available to rewind. Use /rewind <user_message_uuid>.",
1357
- };
1358
- return;
1359
- }
1360
- yield {
1361
- type: "timeline",
1362
- provider: "claude",
1363
- item: {
1364
- type: "assistant_message",
1365
- text: this.buildRewindSuccessMessage(rewindAttempt.messageId, rewindAttempt.result),
1366
- },
1367
- };
1368
- yield { type: "turn_completed", provider: "claude" };
1369
- }
1370
- catch (error) {
1371
- yield {
1372
- type: "turn_failed",
1373
- provider: "claude",
1374
- error: error instanceof Error ? error.message : "Failed to rewind tracked files",
1375
- };
1376
- }
1377
- }
1378
1300
  buildRewindSuccessMessage(targetUserMessageId, rewindResult) {
1379
1301
  const fileCount = Array.isArray(rewindResult.filesChanged)
1380
1302
  ? rewindResult.filesChanged.length
@@ -1525,23 +1447,28 @@ class ClaudeAgentSession {
1525
1447
  return this.query;
1526
1448
  }
1527
1449
  if (this.queryRestartNeeded && this.query) {
1528
- this.input?.end();
1529
- this.query.close();
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?.();
1530
1460
  try {
1531
- await this.query.return?.();
1461
+ await oldQuery.return?.();
1532
1462
  }
1533
1463
  catch {
1534
1464
  /* ignore */
1535
1465
  }
1536
- this.query = null;
1537
- this.input = null;
1538
- this.queryRestartNeeded = false;
1539
1466
  }
1540
- const input = new Pushable();
1467
+ const input = createAsyncMessageInput();
1541
1468
  const options = this.buildOptions();
1542
1469
  this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query");
1543
1470
  this.input = input;
1544
- this.query = this.queryFactory({ prompt: input, options });
1471
+ this.query = this.queryFactory({ prompt: input.iterable, options });
1545
1472
  // Do not kick off background control-plane queries here. Methods like
1546
1473
  // supportedCommands()/setPermissionMode() may execute immediately after
1547
1474
  // ensureQuery() (for listCommands()/setMode()), and sharing the same query
@@ -1569,16 +1496,14 @@ class ClaudeAgentSession {
1569
1496
  }
1570
1497
  }
1571
1498
  buildOptions() {
1572
- const configuredThinkingOptionId = this.config.thinkingOptionId;
1573
- const thinkingOptionId = configuredThinkingOptionId && configuredThinkingOptionId !== "default"
1574
- ? configuredThinkingOptionId
1575
- : "off";
1576
- let maxThinkingTokens;
1577
- if (thinkingOptionId === "on") {
1578
- maxThinkingTokens = 10000;
1579
- }
1580
- else if (thinkingOptionId === "off") {
1581
- 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;
1582
1507
  }
1583
1508
  const appendedSystemPrompt = [
1584
1509
  getOrchestratorModeInstructions(),
@@ -1609,13 +1534,15 @@ class ClaudeAgentSession {
1609
1534
  // Increase MCP timeouts for long-running tool calls (10 minutes)
1610
1535
  MCP_TIMEOUT: "600000",
1611
1536
  MCP_TOOL_TIMEOUT: "600000",
1537
+ ...(this.launchEnv ?? {}),
1612
1538
  },
1613
1539
  // Required for provider-level /rewind support.
1614
1540
  enableFileCheckpointing: true,
1615
1541
  // If we have a session ID from a previous query (e.g., after interrupt),
1616
1542
  // resume that session to continue the conversation history.
1617
1543
  ...(this.claudeSessionId ? { resume: this.claudeSessionId } : {}),
1618
- ...(maxThinkingTokens !== undefined ? { maxThinkingTokens } : {}),
1544
+ ...(thinking ? { thinking } : {}),
1545
+ ...(effort ? { effort } : {}),
1619
1546
  ...this.config.extra?.claude,
1620
1547
  };
1621
1548
  if (this.config.mcpServers) {
@@ -1631,7 +1558,7 @@ class ClaudeAgentSession {
1631
1558
  return this.applyRuntimeSettings(base);
1632
1559
  }
1633
1560
  applyRuntimeSettings(options) {
1634
- return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
1561
+ return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings, this.launchEnv);
1635
1562
  }
1636
1563
  normalizeMcpServers(servers) {
1637
1564
  const result = {};
@@ -1683,7 +1610,7 @@ class ClaudeAgentSession {
1683
1610
  this.turnState = next;
1684
1611
  }
1685
1612
  syncTurnState(reason) {
1686
- if (this.activeForegroundTurn) {
1613
+ if (this.activeForegroundTurnId) {
1687
1614
  this.transitionTurnState("foreground", reason);
1688
1615
  return;
1689
1616
  }
@@ -1693,6 +1620,10 @@ class ClaudeAgentSession {
1693
1620
  }
1694
1621
  this.transitionTurnState("idle", reason);
1695
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
+ }
1696
1627
  buildTurnFailedEvent(errorMessage) {
1697
1628
  const normalized = errorMessage.trim() || "Claude run failed";
1698
1629
  const exitCodeMatch = normalized.match(/\bcode\s+(\d+)\b/i);
@@ -1720,6 +1651,22 @@ class ClaudeAgentSession {
1720
1651
  getRecentStderrDiagnostic() {
1721
1652
  return this.recentStderr.trim() || undefined;
1722
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
+ }
1723
1670
  createTurnId(owner) {
1724
1671
  return `${owner}-turn-${this.nextTurnOrdinal++}`;
1725
1672
  }
@@ -1728,6 +1675,46 @@ class ClaudeAgentSession {
1728
1675
  event.type === "turn_failed" ||
1729
1676
  event.type === "turn_canceled");
1730
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
+ }
1731
1718
  shouldRecoverInterruptedQueryAbort(error, consecutiveRecoveries) {
1732
1719
  if (consecutiveRecoveries >= 3) {
1733
1720
  return false;
@@ -1743,37 +1730,29 @@ class ClaudeAgentSession {
1743
1730
  if (event.type === "turn_failed" || event.type === "turn_canceled") {
1744
1731
  this.flushPendingToolCalls();
1745
1732
  }
1746
- this.dispatchForegroundEvents([event]);
1747
- }
1748
- dispatchForegroundEvents(events) {
1749
- const foregroundTurn = this.activeForegroundTurn;
1750
- if (!foregroundTurn) {
1751
- this.dispatchLiveEvents(events);
1752
- return;
1753
- }
1754
- let terminalSeen = false;
1755
- for (const event of events) {
1756
- foregroundTurn.queue.push(event);
1757
- terminalSeen || (terminalSeen = this.isTerminalTurnEvent(event));
1758
- }
1759
- if (!terminalSeen) {
1760
- return;
1761
- }
1762
- foregroundTurn.queue.end();
1763
- if (this.activeForegroundTurn === foregroundTurn) {
1764
- this.activeForegroundTurn = null;
1765
- }
1733
+ this.notifySubscribers(event);
1734
+ this.activeForegroundTurnId = null;
1735
+ this.lastForegroundPromptText = null;
1736
+ this.cancelCurrentTurn = null;
1766
1737
  this.syncTurnState("foreground turn terminal");
1767
1738
  }
1768
- dispatchLiveEvents(events) {
1739
+ dispatchEvents(events) {
1769
1740
  let terminalSeen = false;
1770
1741
  for (const event of events) {
1771
- this.liveEventQueue.push(event);
1742
+ this.notifySubscribers(event);
1772
1743
  terminalSeen || (terminalSeen = this.isTerminalTurnEvent(event));
1773
1744
  }
1774
- if (terminalSeen && this.autonomousTurn) {
1775
- this.autonomousTurn = null;
1776
- 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
+ }
1777
1756
  }
1778
1757
  }
1779
1758
  startAutonomousTurn() {
@@ -1783,40 +1762,26 @@ class ClaudeAgentSession {
1783
1762
  this.autonomousTurn = {
1784
1763
  id: this.createTurnId("autonomous"),
1785
1764
  };
1786
- this.liveEventQueue.push({ type: "turn_started", provider: "claude" });
1765
+ this.notifySubscribers({ type: "turn_started", provider: "claude" });
1787
1766
  this.syncTurnState("autonomous turn started");
1788
1767
  }
1789
1768
  completeAutonomousTurn() {
1790
1769
  if (!this.autonomousTurn) {
1791
1770
  return;
1792
1771
  }
1772
+ this.notifySubscribers({ type: "turn_completed", provider: "claude" });
1793
1773
  this.autonomousTurn = null;
1794
- this.liveEventQueue.push({ type: "turn_completed", provider: "claude" });
1795
1774
  this.syncTurnState("autonomous turn completed");
1796
1775
  }
1797
- cancelAutonomousTurn(reason) {
1798
- if (!this.autonomousTurn) {
1799
- return;
1800
- }
1801
- this.flushPendingToolCalls();
1802
- this.autonomousTurn = null;
1803
- this.liveEventQueue.push({
1804
- type: "turn_canceled",
1805
- provider: "claude",
1806
- reason,
1807
- });
1808
- this.syncTurnState("autonomous turn canceled");
1809
- }
1810
1776
  failActiveTurns(errorMessage) {
1811
1777
  const failure = this.buildTurnFailedEvent(errorMessage);
1812
- if (this.activeForegroundTurn) {
1813
- this.flushPendingToolCalls();
1814
- this.dispatchForegroundEvents([failure]);
1778
+ this.flushPendingToolCalls();
1779
+ if (this.activeForegroundTurnId) {
1780
+ this.finishForegroundTurn(failure);
1815
1781
  return;
1816
1782
  }
1817
1783
  if (this.autonomousTurn) {
1818
- this.flushPendingToolCalls();
1819
- this.dispatchLiveEvents([failure]);
1784
+ this.dispatchEvents([failure]);
1820
1785
  }
1821
1786
  }
1822
1787
  startQueryPump() {
@@ -1848,6 +1813,12 @@ class ClaudeAgentSession {
1848
1813
  while (!this.closed && this.query === activeQuery) {
1849
1814
  try {
1850
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");
1851
1822
  consecutiveInterruptAbortRecoveries = 0;
1852
1823
  if (await this.handleMissingResumedConversation(message, activeQuery)) {
1853
1824
  return;
@@ -1868,6 +1839,7 @@ class ClaudeAgentSession {
1868
1839
  continue;
1869
1840
  }
1870
1841
  if (!this.closed && this.query === activeQuery) {
1842
+ await this.awaitRecentStderrAfterProcessExit(error);
1871
1843
  this.failActiveTurns(error instanceof Error ? error.message : "Claude stream failed");
1872
1844
  }
1873
1845
  return;
@@ -1882,24 +1854,40 @@ class ClaudeAgentSession {
1882
1854
  }
1883
1855
  }
1884
1856
  routeSdkMessageFromPump(message) {
1885
- 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);
1886
1874
  const assistantishMessage = message.type === "assistant" ||
1887
1875
  message.type === "stream_event" ||
1888
- message.type === "tool_progress";
1889
- if (!routeToForeground && assistantishMessage) {
1876
+ message.type === "tool_progress" ||
1877
+ (message.type === "system" && message.subtype === "task_notification");
1878
+ if (!isForeground && assistantishMessage) {
1890
1879
  this.startAutonomousTurn();
1891
1880
  }
1892
- if (!routeToForeground && !this.autonomousTurn && message.type === "result") {
1881
+ if (!isForeground && !this.autonomousTurn && message.type === "result") {
1893
1882
  return;
1894
1883
  }
1895
- const turnId = this.activeForegroundTurn?.id ?? this.autonomousTurn?.id ?? null;
1884
+ const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
1896
1885
  const identifiers = readEventIdentifiers(message);
1897
1886
  this.logger.trace({
1898
1887
  claudeSessionId: this.claudeSessionId,
1899
1888
  messageType: message.type,
1900
- routedTo: routeToForeground ? "foreground_queue" : "live_queue",
1901
1889
  turnId,
1902
- }, "Claude query pump routed SDK message");
1890
+ }, "Claude query pump: SDK message");
1903
1891
  const messageEvents = this.translateMessageToEvents(message, {
1904
1892
  suppressAssistantText: true,
1905
1893
  suppressReasoning: true,
@@ -1915,30 +1903,37 @@ class ClaudeAgentSession {
1915
1903
  item,
1916
1904
  provider: "claude",
1917
1905
  }));
1918
- 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];
1919
1919
  if (events.length === 0) {
1920
1920
  return;
1921
1921
  }
1922
1922
  if (this.pendingInterruptAbort &&
1923
1923
  message.type === "result" &&
1924
1924
  events.some((event) => event.type === "turn_completed" || event.type === "turn_failed") &&
1925
- (!this.activeForegroundTurn || !this.activeForegroundTurn.hasVisibleActivity)) {
1925
+ (!this.activeForegroundTurnId || !this.foregroundHasVisibleActivity)) {
1926
1926
  this.pendingInterruptAbort = false;
1927
1927
  this.logger.debug("Suppressing stale Claude interrupt terminal result");
1928
1928
  return;
1929
1929
  }
1930
- if (this.activeForegroundTurn &&
1930
+ if (this.activeForegroundTurnId &&
1931
1931
  events.some((event) => event.type === "timeline" ||
1932
1932
  event.type === "permission_requested" ||
1933
1933
  event.type === "permission_resolved")) {
1934
- this.activeForegroundTurn.hasVisibleActivity = true;
1935
- this.pendingInterruptAbort = false;
1934
+ this.foregroundHasVisibleActivity = true;
1936
1935
  }
1937
- if (routeToForeground) {
1938
- this.dispatchForegroundEvents(events);
1939
- return;
1940
- }
1941
- this.dispatchLiveEvents(events);
1936
+ this.dispatchEvents(events);
1942
1937
  }
1943
1938
  async handleMissingResumedConversation(message, query) {
1944
1939
  const staleResumeError = this.readMissingResumedConversationError(message);
@@ -1960,13 +1955,10 @@ class ClaudeAgentSession {
1960
1955
  this.persistence = null;
1961
1956
  this.persistedHistory = [];
1962
1957
  this.historyPending = false;
1963
- this.historyOffsetSessionId = null;
1964
- this.historyReadOffsetBytes = 0;
1965
- this.historyLineFragment = "";
1966
1958
  this.cachedRuntimeInfo = null;
1967
1959
  this.queryRestartNeeded = false;
1968
1960
  this.autonomousTurn = null;
1969
- this.activeForegroundTurn = null;
1961
+ this.activeForegroundTurnId = null;
1970
1962
  this.syncTurnState("missing resumed conversation");
1971
1963
  return true;
1972
1964
  }
@@ -2199,15 +2191,15 @@ class ClaudeAgentSession {
2199
2191
  this.currentMode = message.permissionMode;
2200
2192
  this.persistence = null;
2201
2193
  if (message.model) {
2202
- const normalizedModel = normalizeClaudeRuntimeModelId({
2203
- runtimeModelId: message.model,
2204
- supportedModelIds: this.selectableModelIds,
2205
- supportedModelFamilyAliases: this.selectableModelFamilyAliases,
2206
- configuredModelId: this.config.model ?? null,
2207
- currentModelId: this.lastOptionsModel,
2208
- });
2209
- this.logger.debug({ model: message.model, normalizedModel }, "Captured model from SDK init");
2210
- 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;
2211
2203
  this.cachedRuntimeInfo = null;
2212
2204
  }
2213
2205
  return threadStartedSessionId;
@@ -2273,12 +2265,19 @@ class ClaudeAgentSession {
2273
2265
  this.enqueueTimeline(item);
2274
2266
  }
2275
2267
  pushEvent(event) {
2276
- const foregroundTurn = this.activeForegroundTurn;
2277
- if (foregroundTurn) {
2278
- foregroundTurn.queue.push(event);
2279
- 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
+ }
2280
2280
  }
2281
- this.liveEventQueue.push(event);
2282
2281
  }
2283
2282
  normalizePermissionUpdates(updates) {
2284
2283
  if (!updates || updates.length === 0) {
@@ -2294,123 +2293,52 @@ class ClaudeAgentSession {
2294
2293
  this.pendingPermissions.delete(id);
2295
2294
  }
2296
2295
  }
2297
- loadPersistedHistory(sessionId, options) {
2296
+ loadPersistedHistory(sessionId) {
2298
2297
  try {
2299
2298
  const historyPath = this.resolveHistoryPath(sessionId);
2300
2299
  if (!historyPath || !fs.existsSync(historyPath)) {
2301
2300
  return;
2302
2301
  }
2303
- if (this.historyOffsetSessionId !== sessionId) {
2304
- this.historyOffsetSessionId = sessionId;
2305
- this.historyReadOffsetBytes = 0;
2306
- this.historyLineFragment = "";
2307
- }
2308
- const content = fs.readFileSync(historyPath);
2309
- if (content.byteLength < this.historyReadOffsetBytes) {
2310
- this.historyReadOffsetBytes = 0;
2311
- this.historyLineFragment = "";
2312
- }
2313
- if (content.byteLength === this.historyReadOffsetBytes) {
2314
- return;
2315
- }
2316
- const unreadChunk = content.subarray(this.historyReadOffsetBytes).toString("utf8");
2317
- this.historyReadOffsetBytes = content.byteLength;
2318
- this.ingestPersistedHistoryChunk(unreadChunk, {
2319
- dispatchLive: options?.dispatchLive ?? false,
2320
- });
2302
+ this.ingestPersistedHistory(fs.readFileSync(historyPath, "utf8"));
2321
2303
  }
2322
2304
  catch (error) {
2323
2305
  // ignore history load failures
2324
2306
  }
2325
2307
  }
2326
- ingestPersistedHistoryChunk(chunk, options) {
2327
- if (!chunk) {
2308
+ ingestPersistedHistory(content) {
2309
+ if (!content) {
2328
2310
  return;
2329
2311
  }
2330
- const combined = `${this.historyLineFragment}${chunk}`;
2331
- this.historyLineFragment = "";
2332
- const lines = combined.split(/\r?\n/);
2333
- const trailing = lines.pop() ?? "";
2334
2312
  const timeline = [];
2335
- for (const line of lines) {
2336
- this.ingestPersistedHistoryLine(line, {
2337
- dispatchLive: options.dispatchLive,
2338
- timeline,
2339
- });
2313
+ for (const line of content.split(/\r?\n/)) {
2314
+ this.ingestPersistedHistoryLine(line, timeline);
2340
2315
  }
2341
- if (trailing.trim().length > 0) {
2342
- const handled = this.ingestPersistedHistoryLine(trailing, {
2343
- dispatchLive: options.dispatchLive,
2344
- timeline,
2345
- });
2346
- if (!handled) {
2347
- this.historyLineFragment = trailing;
2348
- }
2349
- }
2350
- if (!options.dispatchLive && timeline.length > 0) {
2316
+ if (timeline.length > 0) {
2351
2317
  this.persistedHistory = [...this.persistedHistory, ...timeline];
2352
2318
  this.historyPending = true;
2353
2319
  }
2354
2320
  }
2355
- ingestPersistedHistoryLine(line, options) {
2321
+ ingestPersistedHistoryLine(line, timeline) {
2356
2322
  const trimmed = line.trim();
2357
2323
  if (!trimmed) {
2358
- return true;
2324
+ return;
2359
2325
  }
2360
2326
  let entry;
2361
2327
  try {
2362
2328
  entry = JSON.parse(trimmed);
2363
2329
  }
2364
2330
  catch {
2365
- return false;
2331
+ return;
2366
2332
  }
2367
2333
  if (entry.isSidechain) {
2368
- return true;
2334
+ return;
2369
2335
  }
2370
2336
  if (entry.type === "user" && typeof entry.uuid === "string") {
2371
2337
  this.rememberUserMessageId(entry.uuid);
2372
2338
  }
2373
- if (options.dispatchLive) {
2374
- this.dispatchPersistedHistoryEntry(entry);
2375
- return true;
2376
- }
2377
2339
  const items = this.convertHistoryEntry(entry);
2378
2340
  if (items.length > 0) {
2379
- options.timeline.push(...items);
2380
- }
2381
- return true;
2382
- }
2383
- dispatchPersistedHistoryEntry(entry) {
2384
- const liveMessage = this.normalizePersistedHistoryEntryToLiveMessage(entry);
2385
- if (liveMessage) {
2386
- this.routeSdkMessageFromPump(liveMessage);
2387
- return;
2388
- }
2389
- const items = this.convertHistoryEntry(entry);
2390
- for (const item of items) {
2391
- this.pushEvent({
2392
- type: "timeline",
2393
- item,
2394
- provider: "claude",
2395
- });
2396
- }
2397
- }
2398
- normalizePersistedHistoryEntryToLiveMessage(entry) {
2399
- const taskNotificationMessage = coerceTaskNotificationHistoryRecordToSystemMessage(entry);
2400
- if (taskNotificationMessage) {
2401
- return taskNotificationMessage;
2402
- }
2403
- const type = readTrimmedString(entry.type);
2404
- switch (type) {
2405
- case "assistant":
2406
- case "result":
2407
- case "stream_event":
2408
- case "system":
2409
- case "tool_progress":
2410
- case "user":
2411
- return entry;
2412
- default:
2413
- return null;
2341
+ timeline.push(...items);
2414
2342
  }
2415
2343
  }
2416
2344
  resolveHistoryPath(sessionId) {
@@ -3018,49 +2946,50 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
3018
2946
  }
3019
2947
  return timeline;
3020
2948
  }
3021
- class Pushable {
3022
- constructor() {
3023
- this.queue = [];
3024
- this.resolvers = [];
3025
- this.closed = false;
3026
- }
3027
- push(item) {
3028
- if (this.closed) {
3029
- return;
3030
- }
3031
- if (this.resolvers.length > 0) {
3032
- const resolve = this.resolvers.shift();
3033
- resolve({ value: item, done: false });
3034
- }
3035
- else {
3036
- this.queue.push(item);
3037
- }
3038
- }
3039
- end() {
3040
- this.closed = true;
3041
- while (this.resolvers.length > 0) {
3042
- const resolve = this.resolvers.shift();
3043
- resolve({ value: undefined, done: true });
3044
- }
3045
- }
3046
- [Symbol.asyncIterator]() {
3047
- return {
3048
- next: () => {
3049
- if (this.queue.length > 0) {
3050
- const value = this.queue.shift();
3051
- if (value !== undefined) {
3052
- return Promise.resolve({ value, done: false });
3053
- }
3054
- }
3055
- if (this.closed) {
3056
- return Promise.resolve({ value: undefined, done: true });
3057
- }
3058
- return new Promise((resolve) => {
3059
- this.resolvers.push(resolve);
3060
- });
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
+ };
3061
2990
  },
3062
- };
3063
- }
2991
+ },
2992
+ };
3064
2993
  }
3065
2994
  async function pathExists(target) {
3066
2995
  try {