@gajae-code/coding-agent 0.11.4 → 0.11.5

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 (146) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +20 -0
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +88 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/task/executor.d.ts +9 -0
  50. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  51. package/dist/types/tools/ask.d.ts +48 -0
  52. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  53. package/dist/types/tools/index.d.ts +4 -0
  54. package/package.json +9 -9
  55. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  56. package/src/cli/notify-cli.ts +40 -18
  57. package/src/commands/sdk.ts +7 -2
  58. package/src/config/settings-schema.ts +17 -0
  59. package/src/daemon/runtime.ts +2 -2
  60. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  61. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  62. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  64. package/src/eval/py/index.ts +2 -2
  65. package/src/export/html/template.generated.ts +1 -1
  66. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  67. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  68. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  69. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  70. package/src/gjc-runtime/launch-tmux.ts +61 -6
  71. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  72. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  73. package/src/gjc-runtime/state-runtime.ts +38 -12
  74. package/src/gjc-runtime/team-runtime.ts +1 -1
  75. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  76. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  77. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  78. package/src/harness-control-plane/owner.ts +59 -3
  79. package/src/hindsight/client.ts +65 -2
  80. package/src/internal-urls/agent-protocol.ts +76 -5
  81. package/src/internal-urls/docs-index.generated.ts +7 -6
  82. package/src/internal-urls/local-protocol.ts +416 -18
  83. package/src/lsp/index.ts +4 -88
  84. package/src/main.ts +37 -13
  85. package/src/modes/acp/acp-agent.ts +8 -0
  86. package/src/modes/components/ansi-display-validator.ts +54 -0
  87. package/src/modes/components/notifications-settings-editor.ts +26 -2
  88. package/src/modes/components/session-observer-overlay.ts +40 -24
  89. package/src/modes/components/settings-selector.ts +3 -1
  90. package/src/modes/components/tool-transcript-format.ts +321 -0
  91. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  92. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  93. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  94. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  95. package/src/modes/controllers/selector-controller.ts +70 -8
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  98. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  99. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  100. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  101. package/src/plan-mode/approved-plan.ts +3 -1
  102. package/src/prompts/agents/architect.md +1 -0
  103. package/src/prompts/agents/critic.md +1 -0
  104. package/src/prompts/system/custom-system-prompt.md +1 -1
  105. package/src/prompts/system/system-prompt.md +2 -1
  106. package/src/prompts/tools/lsp.md +3 -3
  107. package/src/runtime-mcp/manager.ts +16 -1
  108. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  109. package/src/sdk/broker/session-index.ts +122 -64
  110. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  111. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  112. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  113. package/src/sdk/bus/config-commands.ts +22 -0
  114. package/src/sdk/bus/config.ts +37 -0
  115. package/src/sdk/bus/index.ts +384 -92
  116. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  117. package/src/sdk/bus/notification-service.ts +284 -62
  118. package/src/sdk/bus/recent-activity.ts +14 -5
  119. package/src/sdk/bus/session-control.ts +150 -44
  120. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  121. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  122. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  123. package/src/sdk/bus/telegram-daemon.ts +1788 -267
  124. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  125. package/src/sdk/session.ts +155 -27
  126. package/src/secrets/index.ts +53 -10
  127. package/src/secrets/obfuscator.ts +21 -18
  128. package/src/secrets/regex.ts +2 -0
  129. package/src/session/agent-session.ts +495 -253
  130. package/src/session/artifacts.ts +188 -23
  131. package/src/session/context-estimation.ts +7 -2
  132. package/src/session/internal/managed-session-scope.ts +200 -38
  133. package/src/session/internal/managed-session-storage.ts +1152 -46
  134. package/src/session/session-manager.ts +1055 -148
  135. package/src/session/session-storage.ts +109 -11
  136. package/src/session/streaming-output.ts +6 -0
  137. package/src/task/executor.ts +71 -26
  138. package/src/task/index.ts +63 -20
  139. package/src/tool-discovery/tool-index.ts +18 -0
  140. package/src/tools/ask.ts +129 -32
  141. package/src/tools/bash-skill-urls.ts +14 -3
  142. package/src/tools/bash.ts +2 -2
  143. package/src/tools/index.ts +4 -0
  144. package/src/tools/plan-mode-guard.ts +1 -0
  145. package/src/tools/sqlite-reader.ts +3 -2
  146. package/src/tools/subagent.ts +2 -0
@@ -245,7 +245,6 @@ import {
245
245
  persistCoordinatorRuntimeStateFromEvent,
246
246
  registerCoordinatorRuntimeStateFinalizer,
247
247
  } from "../gjc-runtime/session-state-sidecar";
248
- import { writeArtifact } from "../gjc-runtime/state-writer";
249
248
  import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
250
249
  import { GoalRuntime } from "../goals/runtime";
251
250
  import type { Goal, GoalModeState } from "../goals/state";
@@ -295,8 +294,10 @@ import {
295
294
  isMCPBridgeTool,
296
295
  isMCPToolName,
297
296
  selectDiscoverableToolNamesByServer,
297
+ selectRestorableDiscoveredBuiltinToolNames,
298
298
  } from "../tool-discovery/tool-index";
299
299
  import type { AskAnswerSource, ToolSession } from "../tools";
300
+ import { computeEssentialBuiltinNames } from "../tools";
300
301
  import { AskTool } from "../tools/ask";
301
302
  import {
302
303
  getAskAnswerSource as getAskAnswerSourceFromRegistry,
@@ -366,6 +367,8 @@ import type {
366
367
  CompactionEntry,
367
368
  DefaultModelSelectionStage,
368
369
  NewSessionOptions,
370
+ RecoveryHydrationContext,
371
+ RecoveryHydrationPromotionFence,
369
372
  SessionContext,
370
373
  SessionEntry,
371
374
  SessionManager,
@@ -432,11 +435,6 @@ export type AgentSessionEvent =
432
435
  | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
433
436
  | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
434
437
 
435
- function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
436
- const relative = path.relative(path.join(path.resolve(cwd), ".gjc"), path.resolve(targetPath));
437
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
438
- }
439
-
440
438
  /** Listener function for agent session events */
441
439
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
442
440
  export type AsyncJobSnapshotItem = Pick<
@@ -529,12 +527,31 @@ export interface AgentSessionConfig {
529
527
  discoveryMode?: "off" | "mcp-only" | "all";
530
528
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
531
529
  initialSelectedMCPToolNames?: string[];
532
- /** Whether constructor-provided MCP defaults should be persisted immediately. */
530
+ /** Built-in discoverable tool names restored for the current all-discovery session. */
531
+ initialSelectedDiscoveredBuiltinToolNames?: string[];
532
+ /** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
533
+ initialBaselineDiscoveredBuiltinToolNames?: string[];
534
+ /** Whether an MCP selection was explicitly supplied to the constructor, including an empty selection. */
535
+ initialMCPToolSelectionIsExplicit?: boolean;
536
+ /** Whether a discoverable built-in selection was explicitly supplied to the constructor, including an empty selection. */
537
+ initialDiscoveredBuiltinToolSelectionIsExplicit?: boolean;
538
+
539
+ /** Whether constructor-provided MCP selections should be persisted immediately. */
533
540
  persistInitialMCPToolSelection?: boolean;
541
+ /** Whether constructor-provided discovered-built-in selections should be persisted immediately. */
542
+ persistInitialDiscoveredBuiltinToolSelection?: boolean;
543
+ /** Explicit MCP authority to write for a new session; distinct from active fallback tools. */
544
+ initialPersistedMCPToolNames?: string[];
545
+ /** Explicit discovered built-in authority to write for a new session; distinct from active fallback tools. */
546
+ initialPersistedDiscoveredBuiltinToolNames?: string[];
547
+ /** Immutable predecessor authority while a recovery host is read-only. */
548
+ recoveryHydrationContext?: RecoveryHydrationContext;
534
549
  /** MCP server names whose tools should seed discovery-mode sessions whenever those servers are connected. */
535
550
  defaultSelectedMCPServerNames?: string[];
536
551
  /** MCP tool names that should seed brand-new sessions created from this AgentSession. */
537
552
  defaultSelectedMCPToolNames?: string[];
553
+ /** MCP capabilities that are always active and never part of persisted user selection. */
554
+ mandatoryMCPToolNames?: string[];
538
555
  /** TTSR manager for time-traveling stream rules */
539
556
  ttsrManager?: TtsrManager;
540
557
  /** Secret obfuscator for deobfuscating streaming edit content */
@@ -754,6 +771,30 @@ type RetryErrorClassification =
754
771
  | "local_unavailable"
755
772
  | "unknown";
756
773
 
774
+ const BARE_DEFAULT_WATCHDOG_ERROR =
775
+ /^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
776
+
777
+ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
778
+ if (message.errorKind !== undefined || message.errorStatus !== undefined) return true;
779
+ const facts = message.transportFailure;
780
+ if (!facts) return false;
781
+ if (classifyFallbackTrigger(facts).class !== "other") return true;
782
+ return (
783
+ facts.status !== undefined ||
784
+ facts.providerCode !== undefined ||
785
+ facts.anthropicErrorType !== undefined ||
786
+ facts.openaiErrorCode !== undefined ||
787
+ (facts.headers !== undefined && Object.keys(facts.headers).length > 0)
788
+ );
789
+ }
790
+
791
+ function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
792
+ return message.content.some(content => {
793
+ if (content.type === "text") return content.text.length > 0;
794
+ return content.type === "thinking" || content.type === "redactedThinking" || content.type === "toolCall";
795
+ });
796
+ }
797
+
757
798
  function isLocalModelEndpoint(model: Model | undefined): boolean {
758
799
  if (!model) return false;
759
800
  if (model.provider === "ollama" || model.provider === "lm-studio" || model.provider === "llama.cpp") {
@@ -949,11 +990,6 @@ function createHandoffContext(document: string): string {
949
990
  return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
950
991
  }
951
992
 
952
- function createHandoffFileName(date = new Date()): string {
953
- const fileTimestamp = date.toISOString().replace(/[:.]/g, "-");
954
- return `handoff-${fileTimestamp}.md`;
955
- }
956
-
957
993
  // ============================================================================
958
994
  // ACP Permission Gate
959
995
  // ============================================================================
@@ -1474,6 +1510,30 @@ export class AgentSession {
1474
1510
  #lastMidRunMaintenanceAnchorSignature: string | undefined = undefined;
1475
1511
  #resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
1476
1512
  #retainedMemorySampler: (() => RetainedMemorySample) | undefined;
1513
+
1514
+ /** Replay safety for the currently admitted top-level prompt/custom-message run. */
1515
+ #retryReplayEpoch = 0;
1516
+ #retryReplayUnsafeEpoch: number | undefined;
1517
+
1518
+ #resetRetryReplaySafety(): void {
1519
+ this.#retryReplayEpoch++;
1520
+ this.#retryReplayUnsafeEpoch = undefined;
1521
+ if (
1522
+ this.#extensionRunner?.hasHandlers("context") ||
1523
+ this.#extensionRunner?.hasHandlers("before_provider_request") ||
1524
+ this.#extensionRunner?.hasHandlers("after_provider_response")
1525
+ ) {
1526
+ this.#markRetryReplayUnsafe();
1527
+ }
1528
+ }
1529
+
1530
+ #markRetryReplayUnsafe(): void {
1531
+ if (this.#retryReplayEpoch > 0) this.#retryReplayUnsafeEpoch = this.#retryReplayEpoch;
1532
+ }
1533
+
1534
+ get #hasCleanRetryReplaySafety(): boolean {
1535
+ return this.#retryReplayEpoch > 0 && this.#retryReplayUnsafeEpoch !== this.#retryReplayEpoch;
1536
+ }
1477
1537
  #prePromptContextCheckPromise: Promise<void> | undefined = undefined;
1478
1538
  /** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
1479
1539
  #contextUsageCache: { key: string; value: ContextUsage } | undefined;
@@ -1612,12 +1672,17 @@ export class AgentSession {
1612
1672
  // Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
1613
1673
  #discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
1614
1674
  #selectedDiscoveredToolNames = new Set<string>();
1675
+ #baselineDiscoveredBuiltinToolNames = new Set<string>();
1615
1676
  #discoverableToolAllowedNames: ReadonlySet<string> | undefined;
1616
1677
  #gjcSubskillToolNames = new Set<string>();
1617
1678
  #gjcSubskillToolSignature: string | undefined;
1618
1679
  #defaultSelectedMCPServerNames = new Set<string>();
1619
1680
  #defaultSelectedMCPToolNames = new Set<string>();
1620
- #sessionDefaultSelectedMCPToolNames = new Map<string, string[]>();
1681
+ #mandatoryMCPToolNames = new Set<string>();
1682
+ /** Constructor authority applies only while this AgentSession instance remains alive. */
1683
+ #constructorMCPToolSelection: string[] | undefined;
1684
+ #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
1685
+ #recoveryHydrationContext: RecoveryHydrationContext | undefined;
1621
1686
 
1622
1687
  // TTSR manager for time-traveling stream rules
1623
1688
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -2021,6 +2086,7 @@ export class AgentSession {
2021
2086
  });
2022
2087
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
2023
2088
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
2089
+ this.#recoveryHydrationContext = config.recoveryHydrationContext;
2024
2090
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
2025
2091
  this.#retainedMemorySampler = config.retainedMemorySampler;
2026
2092
  this.#ownedMcpManager = config.ownedMcpManager;
@@ -2112,24 +2178,53 @@ export class AgentSession {
2112
2178
  : undefined;
2113
2179
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
2114
2180
  this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
2181
+ this.#selectedDiscoveredToolNames = new Set(
2182
+ this.#selectRestorableDiscoveredBuiltinToolNames(config.initialSelectedDiscoveredBuiltinToolNames ?? []),
2183
+ );
2184
+ this.#baselineDiscoveredBuiltinToolNames = new Set(
2185
+ selectRestorableDiscoveredBuiltinToolNames(
2186
+ config.initialBaselineDiscoveredBuiltinToolNames ?? [],
2187
+ this.#toolRegistry,
2188
+ this.#discoverableToolAllowedNames,
2189
+ ),
2190
+ );
2115
2191
  this.#defaultSelectedMCPServerNames = new Set(config.defaultSelectedMCPServerNames ?? []);
2116
2192
  this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
2193
+ this.#mandatoryMCPToolNames = new Set(
2194
+ (config.mandatoryMCPToolNames ?? [])
2195
+ .map(name => name.toLowerCase())
2196
+ .filter(name => this.#toolRegistry.has(name)),
2197
+ );
2198
+ this.#constructorMCPToolSelection =
2199
+ config.initialMCPToolSelectionIsExplicit === true
2200
+ ? this.#filterSelectableMCPToolNames(config.initialPersistedMCPToolNames ?? [])
2201
+ : undefined;
2202
+ this.#constructorDiscoveredBuiltinToolSelection =
2203
+ config.initialDiscoveredBuiltinToolSelectionIsExplicit === true
2204
+ ? this.#selectRestorableDiscoveredBuiltinToolNames(config.initialPersistedDiscoveredBuiltinToolNames ?? [])
2205
+ : undefined;
2117
2206
  this.#pruneSelectedMCPToolNames();
2118
- const persistedSelectedMCPToolNames = this.buildDisplaySessionContext().selectedMCPToolNames;
2119
- const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
2120
2207
  const persistInitialMCPToolSelection =
2121
- config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
2208
+ config.persistInitialMCPToolSelection === true && config.initialMCPToolSelectionIsExplicit !== false;
2209
+ const persistInitialDiscoveredBuiltinToolSelection =
2210
+ config.persistInitialDiscoveredBuiltinToolSelection === true &&
2211
+ config.initialDiscoveredBuiltinToolSelectionIsExplicit !== false;
2122
2212
  if (
2123
- this.#mcpDiscoveryEnabled &&
2124
- persistInitialMCPToolSelection &&
2125
- !this.#selectedMCPToolNamesMatch(persistedSelectedMCPToolNames, currentSelectedMCPToolNames)
2213
+ !this.#recoveryHydrationContext &&
2214
+ (this.#mcpDiscoveryEnabled || this.#resolveEffectiveDiscoveryMode() === "all") &&
2215
+ persistInitialMCPToolSelection
2126
2216
  ) {
2127
- this.sessionManager.appendMCPToolSelection(currentSelectedMCPToolNames);
2217
+ this.sessionManager.appendMCPToolSelection(config.initialPersistedMCPToolNames ?? []);
2218
+ }
2219
+ if (
2220
+ !this.#recoveryHydrationContext &&
2221
+ this.#resolveEffectiveDiscoveryMode() === "all" &&
2222
+ persistInitialDiscoveredBuiltinToolSelection
2223
+ ) {
2224
+ this.sessionManager.appendDiscoveredBuiltinToolSelection(
2225
+ config.initialPersistedDiscoveredBuiltinToolNames ?? [],
2226
+ );
2128
2227
  }
2129
- this.#rememberSessionDefaultSelectedMCPToolNames(
2130
- this.sessionManager.getSessionFile(),
2131
- this.#getConfiguredDefaultSelectedMCPToolNames(),
2132
- );
2133
2228
  this.#ttsrManager = config.ttsrManager;
2134
2229
  this.#obfuscator = config.obfuscator;
2135
2230
  this.#agentId = config.agentId;
@@ -2187,11 +2282,37 @@ export class AgentSession {
2187
2282
  // Re-evaluate append-only context mode when the setting changes at runtime.
2188
2283
  this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
2189
2284
  // SDK ToolSession callbacks capture the just-constructed session. Defer the
2190
- // initial ask-tool attachment until that capture has been assigned by the
2285
+ // initial ask-tool registration until that capture has been assigned by the
2191
2286
  // session factory, while retaining the durable emitter created above.
2192
- queueMicrotask(() => {
2193
- if (!this.#isDisposed) this.#ensureWorkflowGateAskTool();
2287
+ this.#workflowGateToolRestoration = new Promise<void>((resolve, reject) => {
2288
+ queueMicrotask(() => {
2289
+ if (this.#isDisposed) {
2290
+ resolve();
2291
+ return;
2292
+ }
2293
+ try {
2294
+ this.#registerWorkflowGateAskTool();
2295
+ this.#attachAskToolIfWorkflowActive().then(resolve, reject);
2296
+ } catch (error) {
2297
+ reject(error instanceof Error ? error : new Error(String(error)));
2298
+ }
2299
+ });
2194
2300
  });
2301
+ // Non-SDK embedders may never observe the getter; a swallowed handler
2302
+ // prevents an unhandled rejection while later awaits still reject.
2303
+ this.#workflowGateToolRestoration.catch(() => {});
2304
+ }
2305
+
2306
+ #workflowGateToolRestoration: Promise<void> = Promise.resolve();
2307
+
2308
+ /**
2309
+ * Resolves when constructor-time workflow-gate tool restoration (ask
2310
+ * registration plus durable active-workflow attachment) has settled. The
2311
+ * SDK factory awaits this so a resumed canonical workflow session is
2312
+ * returned with `ask` already resident.
2313
+ */
2314
+ get workflowGateToolRestoration(): Promise<void> {
2315
+ return this.#workflowGateToolRestoration;
2195
2316
  }
2196
2317
 
2197
2318
  /** Model registry for API key resolution and model discovery */
@@ -2900,17 +3021,16 @@ export class AgentSession {
2900
3021
  if (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens) return;
2901
3022
 
2902
3023
  try {
2903
- const artifact = await this.sessionManager.allocateArtifactPath("tool-result");
2904
- if (!artifact.id || !artifact.path) return;
2905
- await Bun.write(artifact.path, fullText);
3024
+ const artifactId = await this.sessionManager.saveArtifact(fullText, "tool-result");
3025
+ if (!artifactId) return;
2906
3026
  const digest = crypto.createHash("sha256").update(fullText).digest("hex");
2907
- const preview = createPreAdmissionArtifactSpillPreview(fullText, artifact.id, digest);
3027
+ const preview = createPreAdmissionArtifactSpillPreview(fullText, artifactId, digest);
2908
3028
  const spillMeta = outputMeta()
2909
3029
  .truncationFromText(preview, {
2910
3030
  direction: "middle",
2911
3031
  totalLines: fullText.split("\n").length,
2912
3032
  totalBytes: Buffer.byteLength(fullText, "utf-8"),
2913
- artifactId: artifact.id,
3033
+ artifactId,
2914
3034
  })
2915
3035
  .get();
2916
3036
  const existingDetails = message.details;
@@ -2950,6 +3070,38 @@ export class AgentSession {
2950
3070
 
2951
3071
  /** Internal handler for agent events - shared by subscribe and reconnect */
2952
3072
  #handleAgentEvent = async (event: AgentEvent): Promise<void> => {
3073
+ if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
3074
+ if (
3075
+ event.type === "tool_execution_start" ||
3076
+ event.type === "tool_execution_update" ||
3077
+ event.type === "tool_execution_end"
3078
+ ) {
3079
+ this.#markRetryReplayUnsafe();
3080
+ } else if (event.type === "message_end") {
3081
+ if (
3082
+ event.message.role === "toolResult" ||
3083
+ (event.message.role === "assistant" && assistantMessageHasVisibleOrToolContent(event.message))
3084
+ ) {
3085
+ this.#markRetryReplayUnsafe();
3086
+ }
3087
+ } else if (event.type === "message_update") {
3088
+ const update = event.assistantMessageEvent;
3089
+ if (
3090
+ update.type === "toolcall_start" ||
3091
+ update.type === "toolcall_delta" ||
3092
+ update.type === "toolcall_end" ||
3093
+ ((update.type === "text_delta" ||
3094
+ update.type === "thinking_delta" ||
3095
+ update.type === "reasoning_summary_delta") &&
3096
+ update.delta.length > 0) ||
3097
+ ((update.type === "text_end" ||
3098
+ update.type === "thinking_end" ||
3099
+ update.type === "reasoning_summary_end") &&
3100
+ update.content.length > 0)
3101
+ ) {
3102
+ this.#markRetryReplayUnsafe();
3103
+ }
3104
+ }
2953
3105
  // Record a successful final yield before any asynchronous extension work so a
2954
3106
  // concurrently delivered agent_end cannot start post-turn maintenance first.
2955
3107
  if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
@@ -3096,7 +3248,7 @@ export class AgentSession {
3096
3248
  // Deobfuscate assistant message content for display emission — the LLM echoes back
3097
3249
  // obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
3098
3250
  // values. The original event.message stays obfuscated so the canonical persistence path above
3099
- // writes `#HASH#` tokens to the session file; convertToLlm re-obfuscates outbound
3251
+ // writes authenticated placeholder tokens to the session file; convertToLlm re-obfuscates outbound
3100
3252
  // traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
3101
3253
  let displayEvent: AgentEvent = event;
3102
3254
  const obfuscator = this.#obfuscator;
@@ -3621,6 +3773,7 @@ export class AgentSession {
3621
3773
  suppressPredecessorAgentEnd?: boolean;
3622
3774
  shouldContinue?: () => boolean;
3623
3775
  onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
3776
+ allowDuringCancelAndSubmit?: boolean;
3624
3777
  onError?: (error: unknown) => void;
3625
3778
  }): Promise<void> {
3626
3779
  const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
@@ -3655,7 +3808,7 @@ export class AgentSession {
3655
3808
  skip("generation_changed");
3656
3809
  return false;
3657
3810
  }
3658
- if (this.#cancelAndSubmitInProgress) {
3811
+ if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
3659
3812
  skip("queue_drained");
3660
3813
  return false;
3661
3814
  }
@@ -4291,6 +4444,7 @@ export class AgentSession {
4291
4444
  #localProtocolOptions(): LocalProtocolOptions {
4292
4445
  return {
4293
4446
  getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
4447
+ isManagedDestination: () => this.sessionManager.isManagedDestination(),
4294
4448
  getSessionId: () => this.sessionManager.getSessionId(),
4295
4449
  };
4296
4450
  }
@@ -4583,6 +4737,7 @@ export class AgentSession {
4583
4737
  message: event.message,
4584
4738
  contentIndex: event.assistantMessageEvent.contentIndex,
4585
4739
  };
4740
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
4586
4741
  await this.#extensionRunner.emit(reasoningEvent, continueWhile);
4587
4742
  } else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
4588
4743
  const reasoningEvent: ReasoningSummaryDeltaEvent = {
@@ -4591,6 +4746,7 @@ export class AgentSession {
4591
4746
  contentIndex: event.assistantMessageEvent.contentIndex,
4592
4747
  delta: event.assistantMessageEvent.delta,
4593
4748
  };
4749
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
4594
4750
  await this.#extensionRunner.emit(reasoningEvent, continueWhile);
4595
4751
  } else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
4596
4752
  const reasoningEvent: ReasoningSummaryEndEvent = {
@@ -4599,6 +4755,7 @@ export class AgentSession {
4599
4755
  contentIndex: event.assistantMessageEvent.contentIndex,
4600
4756
  content: event.assistantMessageEvent.content,
4601
4757
  };
4758
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
4602
4759
  await this.#extensionRunner.emit(reasoningEvent, continueWhile);
4603
4760
  }
4604
4761
  } else if (event.type === "message_end") {
@@ -4652,6 +4809,7 @@ export class AgentSession {
4652
4809
  continuationSkipReason: event.continuationSkipReason,
4653
4810
  });
4654
4811
  } else if (event.type === "auto_retry_start") {
4812
+ if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
4655
4813
  await this.#extensionRunner.emit({
4656
4814
  type: "auto_retry_start",
4657
4815
  attempt: event.attempt,
@@ -5090,29 +5248,66 @@ export class AgentSession {
5090
5248
  return left.length === right.length && left.every((name, index) => name === right[index]);
5091
5249
  }
5092
5250
 
5093
- #rememberSessionDefaultSelectedMCPToolNames(
5094
- sessionFile: string | null | undefined,
5095
- toolNames: Iterable<string>,
5096
- ): void {
5097
- if (!sessionFile) return;
5098
- this.#sessionDefaultSelectedMCPToolNames.set(
5099
- path.resolve(sessionFile),
5100
- this.#filterSelectableMCPToolNames(toolNames),
5251
+ #resolveConstructorMCPToolSelection(): string[] | undefined {
5252
+ return this.#constructorMCPToolSelection
5253
+ ? this.#filterSelectableMCPToolNames(this.#constructorMCPToolSelection)
5254
+ : undefined;
5255
+ }
5256
+
5257
+ #selectRestorableDiscoveredBuiltinToolNames(toolNames: Iterable<string>): string[] {
5258
+ return selectRestorableDiscoveredBuiltinToolNames(
5259
+ toolNames,
5260
+ this.#toolRegistry,
5261
+ this.#discoverableToolAllowedNames,
5262
+ new Set(computeEssentialBuiltinNames(this.settings)),
5101
5263
  );
5102
5264
  }
5103
5265
 
5104
- #getSessionDefaultSelectedMCPToolNames(sessionFile: string | null | undefined): string[] {
5105
- if (!sessionFile) return [];
5106
- return this.#sessionDefaultSelectedMCPToolNames.get(path.resolve(sessionFile)) ?? [];
5266
+ #resolveConstructorDiscoveredBuiltinToolSelection(): string[] | undefined {
5267
+ return this.#constructorDiscoveredBuiltinToolSelection
5268
+ ? this.#selectRestorableDiscoveredBuiltinToolNames(this.#constructorDiscoveredBuiltinToolSelection)
5269
+ : undefined;
5270
+ }
5271
+
5272
+ #clearConstructorToolSelectionAuthority(): void {
5273
+ this.#constructorMCPToolSelection = undefined;
5274
+ this.#constructorDiscoveredBuiltinToolSelection = undefined;
5107
5275
  }
5108
5276
 
5109
- #persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames: string[]): void {
5110
- if (!this.#mcpDiscoveryEnabled) return;
5277
+ #getSelectedDiscoveredBuiltinToolNames(): string[] {
5278
+ return this.#selectRestorableDiscoveredBuiltinToolNames(this.#selectedDiscoveredToolNames).filter(name =>
5279
+ this.getActiveToolNames().includes(name),
5280
+ );
5281
+ }
5282
+
5283
+ #persistSelectedMCPToolNamesIfChanged(
5284
+ previousSelectedMCPToolNames: string[],
5285
+ previousSelectedDiscoveredBuiltinToolNames: string[],
5286
+ ): void {
5287
+ if (!this.#mcpDiscoveryEnabled && this.#resolveEffectiveDiscoveryMode() !== "all") return;
5111
5288
  const nextSelectedMCPToolNames = this.getSelectedMCPToolNames();
5112
- if (this.#selectedMCPToolNamesMatch(previousSelectedMCPToolNames, nextSelectedMCPToolNames)) {
5113
- return;
5289
+ const nextSelectedDiscoveredBuiltinToolNames = this.#getSelectedDiscoveredBuiltinToolNames();
5290
+ const mcpSelectionChanged = !this.#selectedMCPToolNamesMatch(
5291
+ previousSelectedMCPToolNames,
5292
+ nextSelectedMCPToolNames,
5293
+ );
5294
+ const discoveredBuiltinSelectionChanged =
5295
+ this.#resolveEffectiveDiscoveryMode() === "all" &&
5296
+ !this.#selectedMCPToolNamesMatch(
5297
+ previousSelectedDiscoveredBuiltinToolNames,
5298
+ nextSelectedDiscoveredBuiltinToolNames,
5299
+ );
5300
+ const mutationCorrelationId =
5301
+ mcpSelectionChanged && discoveredBuiltinSelectionChanged ? crypto.randomUUID() : undefined;
5302
+ if (mcpSelectionChanged) {
5303
+ this.sessionManager.appendMCPToolSelection(nextSelectedMCPToolNames, mutationCorrelationId);
5304
+ }
5305
+ if (discoveredBuiltinSelectionChanged) {
5306
+ this.sessionManager.appendDiscoveredBuiltinToolSelection(
5307
+ nextSelectedDiscoveredBuiltinToolNames,
5308
+ mutationCorrelationId,
5309
+ );
5114
5310
  }
5115
- this.sessionManager.appendMCPToolSelection(nextSelectedMCPToolNames);
5116
5311
  }
5117
5312
 
5118
5313
  #getActiveNonMCPToolNames(): string[] {
@@ -5291,30 +5486,13 @@ export class AgentSession {
5291
5486
 
5292
5487
  getSelectedMCPToolNames(): string[] {
5293
5488
  if (!this.#mcpDiscoveryEnabled) {
5294
- return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
5295
- }
5296
- return this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames);
5297
- }
5298
-
5299
- async #activateDiscoveredMCPTools(toolNames: string[]): Promise<string[]> {
5300
- const nextSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
5301
- const activated: string[] = [];
5302
- for (const name of toolNames) {
5303
- if (!this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
5304
- continue;
5305
- }
5306
- nextSelectedMCPToolNames.add(name);
5307
- activated.push(name);
5308
- }
5309
- if (activated.length === 0) {
5310
- return [];
5489
+ return this.getActiveToolNames().filter(
5490
+ name => isMCPToolName(name) && this.#toolRegistry.has(name) && !this.#mandatoryMCPToolNames.has(name),
5491
+ );
5311
5492
  }
5312
- const nextActive = [
5313
- ...this.#getActiveNonMCPToolNames(),
5314
- ...this.#filterSelectableMCPToolNames(nextSelectedMCPToolNames),
5315
- ];
5316
- await this.setActiveToolsByName(nextActive);
5317
- return [...new Set(activated)];
5493
+ return this.#filterSelectableMCPToolNames(this.#selectedMCPToolNames).filter(
5494
+ name => !this.#mandatoryMCPToolNames.has(name),
5495
+ );
5318
5496
  }
5319
5497
 
5320
5498
  // ── Generic tool discovery (covers built-in + MCP + extension) ────────────
@@ -5362,54 +5540,44 @@ export class AgentSession {
5362
5540
  return this.#discoverableToolSearchIndex;
5363
5541
  }
5364
5542
 
5365
- /** Invalidate the generic search index cache (call after tool set changes).
5366
- * Delegates to {@link #invalidateDiscoveryCaches} so all discovery-related caches stay in sync. */
5367
- #invalidateDiscoverableToolSearchIndex(): void {
5368
- this.#invalidateDiscoveryCaches();
5369
- }
5370
-
5371
5543
  getSelectedDiscoveredToolNames(): string[] {
5372
- // Union of MCP-selected and generic non-MCP selected. Non-MCP selections are only
5373
- // selected while they are still active; otherwise BM25 must be able to rediscover them.
5374
- const activeNames = new Set(this.getActiveToolNames());
5375
5544
  const mcpSelected = this.getSelectedMCPToolNames();
5376
- const nonMcpSelected = Array.from(this.#selectedDiscoveredToolNames).filter(
5377
- name => activeNames.has(name) && this.#toolRegistry.has(name) && !isMCPToolName(name),
5378
- );
5379
- return [...new Set([...mcpSelected, ...nonMcpSelected])];
5545
+ return [...new Set([...mcpSelected, ...this.#getSelectedDiscoveredBuiltinToolNames()])];
5380
5546
  }
5381
5547
 
5382
5548
  async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
5383
- const mcpNames = toolNames.filter(name => this.#discoverableMCPTools.has(name));
5384
- const nonMcpNames = toolNames.filter(name => !this.#discoverableMCPTools.has(name));
5549
+ const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
5550
+ const previousSelectedDiscoveredBuiltinToolNames = this.#getSelectedDiscoveredBuiltinToolNames();
5551
+ const nextActiveToolNames = this.getActiveToolNames();
5552
+ const nextActiveNameSet = new Set(nextActiveToolNames);
5553
+ const nextSelectedDiscoveredBuiltinToolNames = new Set(this.#selectedDiscoveredToolNames);
5385
5554
  const activated: string[] = [];
5386
-
5387
- // Activate MCP tools via existing path
5388
- if (mcpNames.length > 0) {
5389
- const activatedMcp = await this.#activateDiscoveredMCPTools(mcpNames);
5390
- activated.push(...activatedMcp);
5391
- }
5392
-
5393
- // Activate non-MCP tools (built-ins that are in the registry but not currently active)
5394
- if (nonMcpNames.length > 0) {
5395
- const currentActiveNames = new Set(this.getActiveToolNames());
5396
- const newlyAdded: string[] = [];
5397
- for (const name of nonMcpNames) {
5398
- if (this.#discoverableToolAllowedNames && !this.#discoverableToolAllowedNames.has(name)) continue;
5399
- if (this.#toolRegistry.has(name) && !currentActiveNames.has(name)) {
5400
- newlyAdded.push(name);
5401
- this.#selectedDiscoveredToolNames.add(name);
5555
+ for (const name of new Set(toolNames)) {
5556
+ if (this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name)) {
5557
+ if (!nextActiveNameSet.has(name)) {
5558
+ nextActiveToolNames.push(name);
5559
+ nextActiveNameSet.add(name);
5402
5560
  activated.push(name);
5403
5561
  }
5562
+ continue;
5404
5563
  }
5405
- if (newlyAdded.length > 0) {
5406
- const nextActive = [...this.getActiveToolNames(), ...newlyAdded];
5407
- await this.setActiveToolsByName(nextActive);
5408
- this.#invalidateDiscoverableToolSearchIndex();
5409
- }
5564
+ if (this.#discoverableToolAllowedNames && !this.#discoverableToolAllowedNames.has(name)) continue;
5565
+ const tool = this.#toolRegistry.get(name);
5566
+ if (tool?.loadMode === "discoverable" && !nextActiveNameSet.has(name)) {
5567
+ nextActiveToolNames.push(name);
5568
+ nextActiveNameSet.add(name);
5569
+ nextSelectedDiscoveredBuiltinToolNames.add(name);
5570
+ activated.push(name);
5571
+ }
5572
+ }
5573
+ if (activated.length > 0) {
5574
+ await this.#applyActiveToolsByName(nextActiveToolNames, {
5575
+ previousSelectedMCPToolNames,
5576
+ previousSelectedDiscoveredBuiltinToolNames,
5577
+ nextSelectedDiscoveredBuiltinToolNames: [...nextSelectedDiscoveredBuiltinToolNames],
5578
+ });
5410
5579
  }
5411
-
5412
- return [...new Set(activated)];
5580
+ return activated;
5413
5581
  }
5414
5582
 
5415
5583
  /** Wrap guarded tools so SDK permission modes remain fail-closed without a reverse provider. */
@@ -5597,10 +5765,17 @@ export class AgentSession {
5597
5765
 
5598
5766
  async #applyActiveToolsByName(
5599
5767
  toolNames: string[],
5600
- options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
5768
+ options?: {
5769
+ persistMCPSelection?: boolean;
5770
+ previousSelectedMCPToolNames?: string[];
5771
+ previousSelectedDiscoveredBuiltinToolNames?: string[];
5772
+ nextSelectedDiscoveredBuiltinToolNames?: string[];
5773
+ },
5601
5774
  ): Promise<void> {
5602
- toolNames = [...new Set(toolNames.map(name => name.toLowerCase()))];
5775
+ toolNames = [...new Set([...toolNames.map(name => name.toLowerCase()), ...this.#mandatoryMCPToolNames])];
5603
5776
  const previousSelectedMCPToolNames = options?.previousSelectedMCPToolNames ?? this.getSelectedMCPToolNames();
5777
+ const previousSelectedDiscoveredBuiltinToolNames =
5778
+ options?.previousSelectedDiscoveredBuiltinToolNames ?? this.#getSelectedDiscoveredBuiltinToolNames();
5604
5779
  const tools: AgentTool[] = [];
5605
5780
  const validToolNames: string[] = [];
5606
5781
  for (const name of toolNames) {
@@ -5610,37 +5785,29 @@ export class AgentSession {
5610
5785
  validToolNames.push(name);
5611
5786
  }
5612
5787
  }
5613
- if (this.#mcpDiscoveryEnabled) {
5614
- this.#selectedMCPToolNames = new Set(
5615
- validToolNames.filter(
5616
- name => isMCPToolName(name) && this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
5617
- ),
5618
- );
5619
- }
5788
+ const nextSelectedMCPToolNames = this.#mcpDiscoveryEnabled
5789
+ ? new Set(
5790
+ validToolNames.filter(
5791
+ name =>
5792
+ isMCPToolName(name) &&
5793
+ !this.#mandatoryMCPToolNames.has(name) &&
5794
+ this.#discoverableMCPTools.has(name) &&
5795
+ this.#toolRegistry.has(name),
5796
+ ),
5797
+ )
5798
+ : this.#selectedMCPToolNames;
5620
5799
  const activeNameSet = new Set(validToolNames);
5621
- for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
5800
+ const nextSelectedDiscoveredBuiltinToolNames = new Set(
5801
+ options?.nextSelectedDiscoveredBuiltinToolNames ?? this.#selectedDiscoveredToolNames,
5802
+ );
5803
+ for (const name of nextSelectedDiscoveredBuiltinToolNames) {
5622
5804
  if (!activeNameSet.has(name) || this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
5623
- this.#selectedDiscoveredToolNames.delete(name);
5805
+ nextSelectedDiscoveredBuiltinToolNames.delete(name);
5624
5806
  }
5625
5807
  }
5626
5808
  const signature = this.#computeAppliedToolSignature(validToolNames, tools);
5627
5809
  const promptRelevantToolsChanged =
5628
5810
  signature !== (this.#pendingAppliedToolSignature ?? this.#lastAppliedToolSignature);
5629
- if (promptRelevantToolsChanged) {
5630
- this.#defaultModelSelectionMutationRevision++;
5631
- this.#pendingAppliedToolSignature = signature;
5632
- }
5633
- this.#setGuardedAgentTools(tools);
5634
-
5635
- // Active tool set changed → discoverable tool list (which excludes already-active tools)
5636
- // is now stale. Invalidate before any prompt-template hook reads the discovery list.
5637
- this.#invalidateDiscoveryCaches();
5638
-
5639
- // Rebuild base system prompt with new tool set, but only when the tool set
5640
- // actually changed. MCP servers can reconnect at arbitrary times and call
5641
- // `refreshMCPTools` -> `#applyActiveToolsByName` even though the resulting
5642
- // tool list is byte-identical. Skipping the rebuild keeps the system prompt
5643
- // stable, which is required for Anthropic prompt caching to keep hitting.
5644
5811
  if (promptRelevantToolsChanged && this.#rebuildSystemPrompt) {
5645
5812
  const generation = this.#reserveBaseSystemPromptGeneration();
5646
5813
  try {
@@ -5663,8 +5830,16 @@ export class AgentSession {
5663
5830
  this.#lastAppliedToolSignature = signature;
5664
5831
  this.#pendingAppliedToolSignature = undefined;
5665
5832
  }
5833
+ if (promptRelevantToolsChanged) this.#defaultModelSelectionMutationRevision++;
5834
+ this.#selectedMCPToolNames = nextSelectedMCPToolNames;
5835
+ this.#selectedDiscoveredToolNames = nextSelectedDiscoveredBuiltinToolNames;
5836
+ this.#setGuardedAgentTools(tools);
5837
+ this.#invalidateDiscoveryCaches();
5666
5838
  if (options?.persistMCPSelection !== false) {
5667
- this.#persistSelectedMCPToolNamesIfChanged(previousSelectedMCPToolNames);
5839
+ this.#persistSelectedMCPToolNamesIfChanged(
5840
+ previousSelectedMCPToolNames,
5841
+ previousSelectedDiscoveredBuiltinToolNames,
5842
+ );
5668
5843
  }
5669
5844
  }
5670
5845
 
@@ -5717,24 +5892,33 @@ export class AgentSession {
5717
5892
  await this.#applyActiveToolsByName(toolNames);
5718
5893
  }
5719
5894
 
5720
- async #restoreMCPSelectionsForSessionContext(
5721
- sessionContext: SessionContext,
5722
- options?: { fallbackSelectedMCPToolNames?: Iterable<string> },
5723
- ): Promise<void> {
5724
- if (!this.#mcpDiscoveryEnabled) return;
5725
- const nextActiveNonMCPToolNames = this.#getActiveNonMCPToolNames();
5726
- const fallbackSelectedMCPToolNames =
5727
- options?.fallbackSelectedMCPToolNames ?? this.#getConfiguredDefaultSelectedMCPToolNames();
5895
+ async #restoreMCPSelectionsForSessionContext(sessionContext: SessionContext): Promise<void> {
5896
+ if (!this.#mcpDiscoveryEnabled && this.#resolveEffectiveDiscoveryMode() !== "all") {
5897
+ await this.#attachAskToolIfWorkflowActive();
5898
+ return;
5899
+ }
5900
+ const selectionOnlyDiscoveredBuiltinToolNames = new Set(
5901
+ this.#getSelectedDiscoveredBuiltinToolNames().filter(
5902
+ name => !this.#baselineDiscoveredBuiltinToolNames.has(name),
5903
+ ),
5904
+ );
5905
+ const nextActiveNonMCPToolNames = this.#getActiveNonMCPToolNames().filter(
5906
+ name => name !== "ask" && !selectionOnlyDiscoveredBuiltinToolNames.has(name),
5907
+ );
5908
+ const constructorMCPToolNames = this.#resolveConstructorMCPToolSelection();
5728
5909
  const restoredMCPToolNames = sessionContext.hasPersistedMCPToolSelection
5729
5910
  ? this.#filterSelectableMCPToolNames(sessionContext.selectedMCPToolNames)
5730
- : this.#filterSelectableMCPToolNames(fallbackSelectedMCPToolNames);
5731
- this.#rememberSessionDefaultSelectedMCPToolNames(
5732
- this.sessionFile,
5733
- this.#getConfiguredDefaultSelectedMCPToolNames(),
5911
+ : (constructorMCPToolNames ?? this.#getConfiguredDefaultSelectedMCPToolNames());
5912
+ const constructorDiscoveredBuiltinToolNames = this.#resolveConstructorDiscoveredBuiltinToolSelection();
5913
+ const restoredDiscoveredBuiltinToolNames = sessionContext.hasPersistedDiscoveredBuiltinToolSelection
5914
+ ? this.#selectRestorableDiscoveredBuiltinToolNames(sessionContext.selectedDiscoveredBuiltinToolNames ?? [])
5915
+ : (constructorDiscoveredBuiltinToolNames ?? []);
5916
+ this.#selectedDiscoveredToolNames = new Set(restoredDiscoveredBuiltinToolNames);
5917
+ await this.#applyActiveToolsByName(
5918
+ [...nextActiveNonMCPToolNames, ...restoredMCPToolNames, ...restoredDiscoveredBuiltinToolNames],
5919
+ { persistMCPSelection: false },
5734
5920
  );
5735
- await this.#applyActiveToolsByName([...nextActiveNonMCPToolNames, ...restoredMCPToolNames], {
5736
- persistMCPSelection: false,
5737
- });
5921
+ await this.#attachAskToolIfWorkflowActive();
5738
5922
  }
5739
5923
  /** Rebuild the base system prompt using the current active tool set. */
5740
5924
  async refreshBaseSystemPrompt(): Promise<void> {
@@ -5878,19 +6062,17 @@ export class AgentSession {
5878
6062
 
5879
6063
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
5880
6064
  this.#pruneSelectedMCPToolNames();
5881
- if (!this.buildDisplaySessionContext().hasPersistedMCPToolSelection) {
5882
- this.#selectedMCPToolNames = new Set([
5883
- ...this.#selectedMCPToolNames,
5884
- ...this.#getConfiguredDefaultSelectedMCPToolNames(),
5885
- ]);
6065
+ const hasPersistedMCPToolSelection = this.buildDisplaySessionContext().hasPersistedMCPToolSelection;
6066
+ if (!hasPersistedMCPToolSelection) {
6067
+ this.#selectedMCPToolNames = new Set(
6068
+ this.#resolveConstructorMCPToolSelection() ?? this.#getConfiguredDefaultSelectedMCPToolNames(),
6069
+ );
5886
6070
  }
5887
- this.#rememberSessionDefaultSelectedMCPToolNames(
5888
- this.sessionFile,
5889
- this.#getConfiguredDefaultSelectedMCPToolNames(),
5890
- );
5891
-
5892
6071
  const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
5893
- await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames });
6072
+ await this.#applyActiveToolsByName(nextActive, {
6073
+ previousSelectedMCPToolNames,
6074
+ persistMCPSelection: hasPersistedMCPToolSelection,
6075
+ });
5894
6076
  }
5895
6077
 
5896
6078
  async #hasActiveGjcSubskillTools(parent: string, sessionId: string | undefined): Promise<boolean> {
@@ -6035,8 +6217,29 @@ export class AgentSession {
6035
6217
  return this.#promptGeneration;
6036
6218
  }
6037
6219
 
6220
+ /** The immutable recovery authority, present only before external ownership promotion. */
6221
+ get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
6222
+ return this.#recoveryHydrationContext;
6223
+ }
6224
+
6225
+ /** Enables normal session mutations after the owner has published its durable fence and writer lease. */
6226
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: RecoveryHydrationPromotionFence): Promise<void> {
6227
+ const context = this.#recoveryHydrationContext;
6228
+ if (!context) throw new Error("Agent session is not awaiting recovery hydration promotion.");
6229
+ await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
6230
+ this.#recoveryHydrationContext = undefined;
6231
+ }
6232
+
6233
+ /** Recovery hydration must not start a continuation before ownership promotion. */
6234
+ #assertRecoveryHydrationPromoted(): void {
6235
+ if (this.#recoveryHydrationContext) {
6236
+ throw new Error("Recovery hydration has not been promoted to a writer-owning session.");
6237
+ }
6238
+ }
6239
+
6038
6240
  /** Main startup calls this exactly once, after a strict open returned `kind: "opened"`. */
6039
6241
  async continuePersistedHistory(): Promise<void> {
6242
+ this.#assertRecoveryHydrationPromoted();
6040
6243
  this.#removeEphemeralCustomMessages();
6041
6244
 
6042
6245
  if (!canContinuePersistedHistory(this.agent.state.messages)) {
@@ -6392,33 +6595,70 @@ export class AgentSession {
6392
6595
  this.#workflowGateEmitter = emitter;
6393
6596
  notifyWorkflowGateEmitterChanged(this.sessionId, emitter);
6394
6597
  if (emitter) {
6395
- this.#ensureWorkflowGateAskTool();
6598
+ this.#registerWorkflowGateAskTool();
6396
6599
  }
6397
6600
  }
6398
6601
 
6399
- #ensureWorkflowGateAskTool(): void {
6400
- if (this.#toolRegistry.has("ask")) return;
6602
+ #registerWorkflowGateAskTool(): void {
6401
6603
  if (!this.#workflowGateToolSession) return;
6402
6604
 
6403
- const askTool = AskTool.createIf(this.#workflowGateToolSession);
6404
- if (!askTool) return;
6605
+ let askTool = this.#toolRegistry.get("ask");
6606
+ if (!askTool) {
6607
+ const createdAskTool = AskTool.createIf(this.#workflowGateToolSession);
6608
+ if (!createdAskTool) return;
6609
+ const wrappedTool = wrapToolWithMetaNotice(createdAskTool as unknown as AgentTool);
6610
+ askTool = this.#extensionRunner ? new ExtensionToolWrapper(wrappedTool, this.#extensionRunner) : wrappedTool;
6611
+ this.#toolRegistry.set(askTool.name, askTool);
6612
+ }
6405
6613
 
6406
- const wrappedTool = wrapToolWithMetaNotice(askTool as unknown as AgentTool);
6407
- const finalTool: AgentTool = this.#extensionRunner
6408
- ? new ExtensionToolWrapper(wrappedTool, this.#extensionRunner)
6409
- : wrappedTool;
6410
- this.#toolRegistry.set(finalTool.name, finalTool);
6614
+ try {
6615
+ if ((this.#workflowGateEmitter?.listPendingGates?.().length ?? 0) > 0) {
6616
+ this.#attachAskTool();
6617
+ }
6618
+ } catch (error) {
6619
+ logger.warn("Failed to inspect pending workflow gates; activating ask tool conservatively", {
6620
+ error: error instanceof Error ? error.message : String(error),
6621
+ });
6622
+ this.#attachAskTool();
6623
+ }
6624
+ }
6411
6625
 
6412
- if (!this.getActiveToolNames().includes(finalTool.name)) {
6413
- const activeTools = [...this.agent.state.tools, finalTool];
6414
- this.#setGuardedAgentTools(activeTools);
6415
- this.#invalidateDiscoveryCaches();
6416
- void this.refreshBaseSystemPrompt().catch(error => {
6417
- logger.warn("Failed to refresh system prompt after workflow gate ask tool registration", {
6626
+ async #attachAskToolIfWorkflowActive(): Promise<void> {
6627
+ const sessionId = this.sessionManager.getSessionId();
6628
+ const inMemoryActiveSkill =
6629
+ this.#activeSkillState && (!this.#activeSkillState.sessionId || this.#activeSkillState.sessionId === sessionId)
6630
+ ? this.#activeSkillState.skill
6631
+ : undefined;
6632
+ let activeSkill = inMemoryActiveSkill;
6633
+ if (!activeSkill) {
6634
+ try {
6635
+ const activeState = await readVisibleSkillActiveState(this.sessionManager.getCwd(), sessionId);
6636
+ activeSkill =
6637
+ activeState?.skill ?? activeState?.active_skills?.find(entry => entry.active !== false)?.skill;
6638
+ } catch (error) {
6639
+ logger.warn("Failed to read durable workflow skill state while restoring ask tool", {
6418
6640
  error: error instanceof Error ? error.message : String(error),
6419
6641
  });
6420
- });
6642
+ return;
6643
+ }
6644
+ // Identity fence: the durable read is async — the session may have been
6645
+ // disposed or switched to a different identity meanwhile. Never attach
6646
+ // a predecessor session's workflow state to the current identity.
6647
+ if (this.#isDisposed || this.sessionManager.getSessionId() !== sessionId) return;
6421
6648
  }
6649
+ if (activeSkill && isCanonicalGjcWorkflowSkill(activeSkill.trim())) this.#attachAskTool();
6650
+ }
6651
+
6652
+ #attachAskTool(): void {
6653
+ const askTool = this.#toolRegistry.get("ask");
6654
+ if (!askTool || this.getActiveToolNames().includes(askTool.name)) return;
6655
+ this.#setGuardedAgentTools([...this.agent.state.tools, askTool]);
6656
+ this.#invalidateDiscoveryCaches();
6657
+ void this.refreshBaseSystemPrompt().catch(error => {
6658
+ logger.warn("Failed to refresh system prompt after workflow gate ask tool activation", {
6659
+ error: error instanceof Error ? error.message : String(error),
6660
+ });
6661
+ });
6422
6662
  }
6423
6663
 
6424
6664
  get goalRuntime(): GoalRuntime {
@@ -6894,6 +7134,7 @@ export class AgentSession {
6894
7134
  ...options,
6895
7135
  prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
6896
7136
  admissionLease: admission,
7137
+ resetRetryReplaySafety: true,
6897
7138
  });
6898
7139
  } finally {
6899
7140
  // Clean up residual eager-todo directive if the prompt never consumed it
@@ -6916,6 +7157,11 @@ export class AgentSession {
6916
7157
  const name = (details as { name?: unknown }).name;
6917
7158
  if (typeof name !== "string" || !name.trim()) return;
6918
7159
  const skill = name.trim();
7160
+ // Functional tool availability must not depend on the best-effort
7161
+ // observational state-sync below (whose failures are swallowed by
7162
+ // #syncSkillPromptActiveStateSafely): attach ask first so canonical
7163
+ // workflow skills can always call it.
7164
+ if (active && isCanonicalGjcWorkflowSkill(skill)) this.#attachAskTool();
6919
7165
  const sessionId = this.sessionManager.getSessionId();
6920
7166
  // Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
6921
7167
  // own their `.gjc/state/skill-active-state.json` row through the
@@ -7021,7 +7267,11 @@ export class AgentSession {
7021
7267
 
7022
7268
  await this.#syncSkillPromptActiveStateSafely(customMessage, true);
7023
7269
  try {
7024
- await this.#promptWithMessage(customMessage, textContent, { ...options, admissionLease: admission });
7270
+ await this.#promptWithMessage(customMessage, textContent, {
7271
+ ...options,
7272
+ admissionLease: admission,
7273
+ resetRetryReplaySafety: true,
7274
+ });
7025
7275
  } finally {
7026
7276
  await this.#syncSkillPromptActiveStateSafely(customMessage, false);
7027
7277
  }
@@ -7037,6 +7287,7 @@ export class AgentSession {
7037
7287
  predecessorAgentEndHold?: symbol;
7038
7288
  admissionLease?: SessionAdmissionLease;
7039
7289
  onRunAccepted?: () => void;
7290
+ resetRetryReplaySafety?: boolean;
7040
7291
  },
7041
7292
  ): Promise<void> {
7042
7293
  await this.#agentEndPublicationPromise;
@@ -7049,6 +7300,7 @@ export class AgentSession {
7049
7300
  let hindsightRecall: string | undefined;
7050
7301
  try {
7051
7302
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
7303
+ if (options?.resetRetryReplaySafety) this.#resetRetryReplaySafety();
7052
7304
  if (message.role === "user") {
7053
7305
  await this.#resetDefaultFallbackForNewTurn();
7054
7306
  await this.#ensureDefaultFallbackResolution();
@@ -7188,6 +7440,8 @@ export class AgentSession {
7188
7440
 
7189
7441
  // Emit before_agent_start extension event. Race hook completion with prompt
7190
7442
  // cancellation so a wedged hook cannot retain SDK prompt authority.
7443
+ if (this.#extensionRunner?.hasHandlers("before_agent_start")) this.#markRetryReplayUnsafe();
7444
+
7191
7445
  if (this.#extensionRunner) {
7192
7446
  const result = await this.#awaitPromptPreflight(
7193
7447
  generation,
@@ -7458,6 +7712,8 @@ export class AgentSession {
7458
7712
  } as unknown as HookCommandContext;
7459
7713
 
7460
7714
  try {
7715
+ this.#markRetryReplayUnsafe();
7716
+
7461
7717
  const args = parseCommandArgs(argsString);
7462
7718
  const result = await loaded.command.execute(args, ctx);
7463
7719
  // If result is a string, it's a prompt to send to LLM
@@ -8188,6 +8444,7 @@ export class AgentSession {
8188
8444
  : undefined;
8189
8445
  this.#abortActiveMidRunBarriers();
8190
8446
  this.#silentAbortPending = options?.silent === true;
8447
+ this.#markRetryReplayUnsafe();
8191
8448
  this.abortRetry();
8192
8449
  this.#promptGeneration++;
8193
8450
  this.#promptPreflightAbortController.abort();
@@ -8426,6 +8683,7 @@ export class AgentSession {
8426
8683
  try {
8427
8684
  await this.#promptWithMessage(message, messageText, {
8428
8685
  admissionLease: admission,
8686
+ resetRetryReplaySafety: true,
8429
8687
  onRunAccepted: () => {
8430
8688
  runAccepted = true;
8431
8689
  if (selected) {
@@ -8481,10 +8739,17 @@ export class AgentSession {
8481
8739
  async #runNewSessionTransition(options?: NewSessionOptions): Promise<boolean> {
8482
8740
  const previousSessionFile = this.sessionFile;
8483
8741
  const previousWorkflowGateSessionId = this.sessionId;
8742
+ const selectionOnlyDiscoveredBuiltinToolNames = new Set(
8743
+ this.#getSelectedDiscoveredBuiltinToolNames().filter(
8744
+ name => !this.#baselineDiscoveredBuiltinToolNames.has(name),
8745
+ ),
8746
+ );
8484
8747
  const nextDiscoverySessionToolNames = this.#mcpDiscoveryEnabled
8485
8748
  ? [
8486
- ...this.#getActiveNonMCPToolNames(),
8487
- ...this.#filterSelectableMCPToolNames(this.#defaultSelectedMCPToolNames),
8749
+ ...this.#getActiveNonMCPToolNames().filter(
8750
+ name => name !== "ask" && !selectionOnlyDiscoveredBuiltinToolNames.has(name),
8751
+ ),
8752
+ ...this.#getConfiguredDefaultSelectedMCPToolNames(),
8488
8753
  ]
8489
8754
  : undefined;
8490
8755
 
@@ -8626,6 +8891,7 @@ export class AgentSession {
8626
8891
  nextDiscoverySessionToolNames: string[] | undefined,
8627
8892
  previousSessionFile: string | undefined,
8628
8893
  ): Promise<void> {
8894
+ this.#clearConstructorToolSelectionAuthority();
8629
8895
  const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
8630
8896
  this.#thinkingLevel = inheritedThinkingLevel;
8631
8897
  this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
@@ -8635,15 +8901,11 @@ export class AgentSession {
8635
8901
  }
8636
8902
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
8637
8903
  if (nextDiscoverySessionToolNames) {
8638
- await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
8639
- if (this.getSelectedMCPToolNames().length > 0) {
8640
- this.sessionManager.appendMCPToolSelection(this.getSelectedMCPToolNames());
8641
- }
8904
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
8905
+ persistMCPSelection: false,
8906
+ nextSelectedDiscoveredBuiltinToolNames: [],
8907
+ });
8642
8908
  }
8643
- this.#rememberSessionDefaultSelectedMCPToolNames(
8644
- this.sessionFile,
8645
- this.#getConfiguredDefaultSelectedMCPToolNames(),
8646
- );
8647
8909
  this.#todoReminderCount = 0;
8648
8910
  this.#planReferenceSent = false;
8649
8911
  this.#planReferencePath = "local://PLAN.md";
@@ -8731,25 +8993,6 @@ export class AgentSession {
8731
8993
  return false;
8732
8994
  }
8733
8995
 
8734
- // Copy artifacts directory if it exists
8735
- const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
8736
- const newArtifactDir = forkResult.newSessionFile.slice(0, -6);
8737
-
8738
- try {
8739
- const oldDirStat = await fs.promises.stat(oldArtifactDir);
8740
- if (oldDirStat.isDirectory()) {
8741
- await fs.promises.cp(oldArtifactDir, newArtifactDir, { recursive: true });
8742
- }
8743
- } catch (err) {
8744
- if (!isEnoent(err)) {
8745
- logger.warn("Failed to copy artifacts during fork", {
8746
- oldArtifactDir,
8747
- newArtifactDir,
8748
- error: err instanceof Error ? err.message : String(err),
8749
- });
8750
- }
8751
- }
8752
-
8753
8996
  // Update agent session ID
8754
8997
  this.#syncAgentSessionId();
8755
8998
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -10092,27 +10335,13 @@ export class AgentSession {
10092
10335
  await this.sessionManager.ensureOnDisk();
10093
10336
  let savedPath: string | undefined;
10094
10337
  if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
10095
- const artifactsDir = this.sessionManager.getArtifactsDir();
10096
- if (artifactsDir) {
10097
- const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
10098
- try {
10099
- if (isUnderProjectGjc(this.sessionManager.getCwd(), handoffFilePath)) {
10100
- await writeArtifact(handoffFilePath, `${handoffText}\n`, {
10101
- cwd: this.sessionManager.getCwd(),
10102
- audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
10103
- });
10104
- } else {
10105
- await Bun.write(handoffFilePath, `${handoffText}\n`);
10106
- }
10107
- savedPath = handoffFilePath;
10108
- } catch (error) {
10109
- logger.warn("Failed to save handoff document to disk", {
10110
- path: handoffFilePath,
10111
- error: error instanceof Error ? error.message : String(error),
10112
- });
10113
- }
10114
- } else {
10115
- logger.debug("Skipping handoff document save because session is not persisted");
10338
+ try {
10339
+ const artifactId = await this.sessionManager.saveArtifact(`${handoffText}\n`, "handoff");
10340
+ savedPath = `artifact://${artifactId}`;
10341
+ } catch (error) {
10342
+ logger.warn("Failed to save handoff document", {
10343
+ error: error instanceof Error ? error.message : String(error),
10344
+ });
10116
10345
  }
10117
10346
  }
10118
10347
 
@@ -10638,6 +10867,8 @@ export class AgentSession {
10638
10867
  return;
10639
10868
  }
10640
10869
 
10870
+ this.#attachAskTool();
10871
+
10641
10872
  const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
10642
10873
  askToolName: "ask",
10643
10874
  });
@@ -12489,23 +12720,33 @@ export class AgentSession {
12489
12720
  ): Promise<boolean | ManagedAttemptDecision> {
12490
12721
  const controller = this.#defaultFallbackChain();
12491
12722
  const managedFallback = controller.chain.entries.length > 1;
12492
- const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
12493
- if (!trigger) {
12494
- return managedOutcome
12495
- ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
12496
- : false;
12497
- }
12498
12723
  const retrySettings = this.settings.getGroup("retry");
12499
12724
  const legacyRetryConfigured =
12500
12725
  this.settings.has("retry.enabled") ||
12501
12726
  this.settings.has("retry.maxRetries") ||
12502
12727
  this.settings.has("retry.baseDelayMs") ||
12503
12728
  this.settings.has("retry.maxDelayMs");
12504
- // A bare default retry configuration must preserve the historical
12505
- // fail-closed behavior for generic provider errors. Explicit retry
12506
- // settings opt into the resilient legacy retry path.
12507
- if (!managedFallback && (!retrySettings.enabled || !legacyRetryConfigured)) return false;
12729
+ // retry.enabled=false always surfaces immediately, matching the explicit
12730
+ // user opt-out.
12731
+ if (!managedFallback && !retrySettings.enabled) return false;
12508
12732
  const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
12733
+ // Bare defaults admit only clean, side-effect-free canonical stream watchdog failures.
12734
+ if (!managedFallback && !legacyRetryConfigured) {
12735
+ if (
12736
+ hasBareDefaultRetryDisqualifyingFacts(message) ||
12737
+ (classification !== "transient" && classification !== "first_event_timeout") ||
12738
+ !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? "") ||
12739
+ !this.#hasCleanRetryReplaySafety
12740
+ ) {
12741
+ return false;
12742
+ }
12743
+ }
12744
+ const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
12745
+ if (!trigger) {
12746
+ return managedOutcome
12747
+ ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
12748
+ : false;
12749
+ }
12509
12750
  const legacyUnbounded = classification === "transient";
12510
12751
  const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
12511
12752
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
@@ -12678,6 +12919,7 @@ export class AgentSession {
12678
12919
  this.#scheduleAgentContinue({
12679
12920
  delayMs: 1,
12680
12921
  generation,
12922
+ allowDuringCancelAndSubmit: true,
12681
12923
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
12682
12924
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
12683
12925
  });
@@ -12860,6 +13102,8 @@ export class AgentSession {
12860
13102
  options?: { excludeFromContext?: boolean },
12861
13103
  ): Promise<BashResult> {
12862
13104
  const excludeFromContext = options?.excludeFromContext === true;
13105
+ this.#markRetryReplayUnsafe();
13106
+
12863
13107
  const cwd = this.sessionManager.getCwd();
12864
13108
 
12865
13109
  if (this.#extensionRunner?.hasHandlers("user_bash")) {
@@ -12889,7 +13133,7 @@ export class AgentSession {
12889
13133
  cwd,
12890
13134
  timeout: clampTimeout("bash") * 1000,
12891
13135
  env: buildGjcRuntimeSessionEnv({
12892
- sessionFile: this.sessionManager.getSessionFile(),
13136
+ sessionFile: null,
12893
13137
  sessionId: this.sessionId,
12894
13138
  cwd,
12895
13139
  }),
@@ -12991,6 +13235,7 @@ export class AgentSession {
12991
13235
  options?: { excludeFromContext?: boolean },
12992
13236
  ): Promise<PythonResult> {
12993
13237
  const excludeFromContext = options?.excludeFromContext === true;
13238
+ this.#markRetryReplayUnsafe();
12994
13239
  const cwd = this.sessionManager.getCwd();
12995
13240
  this.assertEvalExecutionAllowed();
12996
13241
 
@@ -13755,9 +14000,6 @@ export class AgentSession {
13755
14000
  const previousTools = [...this.agent.state.tools];
13756
14001
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
13757
14002
  const previousSystemPrompt = this.agent.state.systemPrompt;
13758
- const previousFallbackSelectedMCPToolNames = previousSessionFile
13759
- ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
13760
- : undefined;
13761
14003
  const previousAgentSteeringQueue = this.agent.snapshotSteering();
13762
14004
  const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
13763
14005
 
@@ -13779,8 +14021,7 @@ export class AgentSession {
13779
14021
  const didReloadConversationChange =
13780
14022
  !switchingToDifferentSession &&
13781
14023
  this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
13782
- const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
13783
- await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
14024
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
13784
14025
 
13785
14026
  // The target session is loaded and MCP selections are restored: discard
13786
14027
  // pre-switch delivery queues before completing the restored agent state.
@@ -13852,6 +14093,11 @@ export class AgentSession {
13852
14093
  }
13853
14094
 
13854
14095
  this.#reconnectToAgent();
14096
+ // Fence predecessor continuations before session_switch starts SDK runtime
14097
+ // teardown. The previous runtime waits for those continuations to settle;
14098
+ // waiting to transfer authority until after hooks creates a circular wait.
14099
+ if (suspendedWorkflowGateEmitter)
14100
+ this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
13855
14101
  // session_switch is the post-commit identity signal. SDK authority and
13856
14102
  // other identity-bound integrations must not observe the successor until
13857
14103
  // messages, model state, MCP selections, and the agent subscription are live.
@@ -13862,8 +14108,6 @@ export class AgentSession {
13862
14108
  previousSessionFile,
13863
14109
  });
13864
14110
  }
13865
- if (suspendedWorkflowGateEmitter)
13866
- this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
13867
14111
  return true;
13868
14112
  } catch (error) {
13869
14113
  this.sessionManager.restoreState(previousSessionState);
@@ -13873,9 +14117,7 @@ export class AgentSession {
13873
14117
  this.#rekeyHindsightMemoryForCurrentSessionId();
13874
14118
  let restoreMcpError: unknown;
13875
14119
  try {
13876
- await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
13877
- fallbackSelectedMCPToolNames: previousFallbackSelectedMCPToolNames,
13878
- });
14120
+ await this.#restoreMCPSelectionsForSessionContext(previousSessionContext);
13879
14121
  } catch (mcpError) {
13880
14122
  restoreMcpError = mcpError;
13881
14123
  logger.warn("Failed to restore MCP selections after switch error", {