@gajae-code/coding-agent 0.11.8 → 0.11.10

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 (101) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/types/cli.d.ts +1 -0
  3. package/dist/types/config/model-profiles.d.ts +2 -3
  4. package/dist/types/config/model-registry.d.ts +2 -0
  5. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  6. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  7. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  8. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  9. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  10. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  11. package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
  12. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  13. package/dist/types/modes/components/welcome.d.ts +2 -0
  14. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  15. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  16. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
  17. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  18. package/dist/types/sdk/bus/index.d.ts +8 -1
  19. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  20. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  21. package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
  22. package/dist/types/sdk/session.d.ts +7 -0
  23. package/dist/types/session/agent-session.d.ts +2 -0
  24. package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
  25. package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
  26. package/dist/types/session/session-manager.d.ts +5 -1
  27. package/dist/types/session/session-storage.d.ts +13 -0
  28. package/dist/types/skill-state/active-state.d.ts +4 -0
  29. package/dist/types/task/provider-retry-status.d.ts +13 -0
  30. package/dist/types/task/types.d.ts +3 -0
  31. package/dist/types/tools/index.d.ts +3 -1
  32. package/package.json +8 -8
  33. package/scripts/generate-sdk-operation-inventory.ts +1 -0
  34. package/src/cli/mcp-cli.ts +4 -21
  35. package/src/cli/notify-cli.ts +1 -0
  36. package/src/cli.ts +32 -0
  37. package/src/commands/deep-interview.ts +9 -4
  38. package/src/commit/agentic/index.ts +3 -3
  39. package/src/commit/map-reduce/index.ts +2 -2
  40. package/src/config/model-profiles.ts +9 -10
  41. package/src/config/model-registry.ts +4 -2
  42. package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
  43. package/src/eval/js/tool-bridge.ts +1 -1
  44. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  45. package/src/gjc-runtime/deep-interview-draft.ts +843 -0
  46. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  47. package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
  48. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  49. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  50. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  51. package/src/gjc-runtime/ralplan-runtime.ts +3 -1
  52. package/src/gjc-runtime/state-writer.ts +166 -2
  53. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  54. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  55. package/src/hooks/skill-state.ts +23 -1
  56. package/src/internal-urls/docs-index.generated.ts +6 -5
  57. package/src/modes/components/welcome.ts +10 -2
  58. package/src/modes/controllers/selector-controller.ts +38 -8
  59. package/src/modes/interactive-mode.ts +29 -9
  60. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  61. package/src/prompts/agents/architect.md +8 -1
  62. package/src/prompts/agents/critic.md +8 -1
  63. package/src/prompts/agents/planner.md +8 -1
  64. package/src/prompts/system/system-prompt.md +1 -0
  65. package/src/runtime-mcp/json-rpc.ts +7 -4
  66. package/src/runtime-mcp/redaction.ts +30 -0
  67. package/src/sdk/broker/identity.ts +22 -12
  68. package/src/sdk/broker/session-index.ts +11 -1
  69. package/src/sdk/bus/chat-daemon-control.ts +5 -3
  70. package/src/sdk/bus/daemon-paths.ts +2 -0
  71. package/src/sdk/bus/index.ts +60 -5
  72. package/src/sdk/bus/notification-service.ts +19 -5
  73. package/src/sdk/bus/operator-runtime.ts +31 -5
  74. package/src/sdk/bus/telegram-cli.ts +6 -13
  75. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  76. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  77. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  78. package/src/sdk/bus/telegram-daemon.ts +685 -120
  79. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  80. package/src/sdk/session.ts +12 -0
  81. package/src/session/agent-session.ts +140 -10
  82. package/src/session/internal/managed-session-scope.ts +339 -228
  83. package/src/session/internal/managed-session-storage.ts +17 -4
  84. package/src/session/session-manager.ts +96 -33
  85. package/src/session/session-storage.ts +23 -0
  86. package/src/skill-state/active-state.ts +10 -0
  87. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  88. package/src/task/executor.ts +154 -44
  89. package/src/task/provider-retry-status.ts +88 -0
  90. package/src/task/render.ts +45 -26
  91. package/src/task/types.ts +3 -0
  92. package/src/tools/ask.ts +1 -0
  93. package/src/tools/bash-allowed-prefixes.ts +45 -1
  94. package/src/tools/index.ts +3 -1
  95. package/src/tools/subagent-render.ts +74 -16
  96. package/src/tools/subagent.ts +2 -0
  97. package/vendor/insane-search/MANIFEST.json +4 -1
  98. package/vendor/insane-search/engine/bias_check.py +5 -0
  99. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  100. package/vendor/insane-search/engine/transport.py +13 -2
  101. package/vendor/insane-search/engine/validators.py +4 -1
@@ -2659,6 +2659,17 @@
2659
2659
  "testIds": "not_applicable"
2660
2660
  }
2661
2661
  },
2662
+ {
2663
+ "sourceId": "agent_session:getToolForExecution",
2664
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2665
+ "sourceKind": "agent_session",
2666
+ "decision": "exclude",
2667
+ "rationale": "internal accessor/plumbing, not a user-facing control seam",
2668
+ "exclusionMetadata": {
2669
+ "adapterMappings": "not_applicable",
2670
+ "testIds": "not_applicable"
2671
+ }
2672
+ },
2662
2673
  {
2663
2674
  "sourceId": "agent_session:registerForegroundBashBackgroundRequestHandler",
2664
2675
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -428,6 +428,13 @@ export interface CreateAgentSessionOptions {
428
428
  goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
429
429
  /** Optional per-session allowlist for tools exposed through search_tool_bm25. */
430
430
  discoverableToolAllowedNames?: readonly string[];
431
+ /**
432
+ * Discoverable built-in tools that must stay in the initial active set even when
433
+ * `tools.discoveryMode === "all"` would otherwise hide them behind `search_tool_bm25`.
434
+ * Used for coordination tools (e.g. `irc`) that a subagent must be able to use
435
+ * immediately without first spending a discovery round-trip to find them.
436
+ */
437
+ alwaysActiveToolNames?: readonly string[];
431
438
  /** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
432
439
  agentRegistry?: AgentRegistry;
433
440
  /** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
@@ -1542,6 +1549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1542
1549
  goalToolAllowedOps: options.goalToolAllowedOps,
1543
1550
  discoverableToolAllowedNames: options.discoverableToolAllowedNames,
1544
1551
  getToolByName: name => session?.getToolByName(name),
1552
+ getToolForExecution: name => session?.getToolForExecution(name),
1545
1553
  agentRegistry,
1546
1554
  getSessionSpawns: () => options.spawns ?? "*",
1547
1555
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
@@ -2329,6 +2337,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2329
2337
  // from the initial set unless they were explicitly requested or restored from persistence.
2330
2338
  // The model finds them via search_tool_bm25 and activates them on demand.
2331
2339
  if (effectiveDiscoveryMode === "all") {
2340
+ const alwaysActiveDiscoveredBuiltinNames = new Set(
2341
+ (options.alwaysActiveToolNames ?? []).map(name => name.toLowerCase()),
2342
+ );
2332
2343
  const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
2333
2344
  const allowedDiscoveredBuiltinNames = options.discoverableToolAllowedNames
2334
2345
  ? new Set(options.discoverableToolAllowedNames.map(name => name.toLowerCase()))
@@ -2337,6 +2348,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2337
2348
  const tool = toolRegistry.get(name);
2338
2349
  if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
2339
2350
  if (tool.loadMode === "essential") return true;
2351
+ if (alwaysActiveDiscoveredBuiltinNames.has(name)) return true;
2340
2352
  return essentialBuiltinNames.has(name);
2341
2353
  });
2342
2354
  explicitlyRequestedDiscoveredBuiltinToolNames = selectRestorableDiscoveredBuiltinToolNames(
@@ -785,6 +785,22 @@ type RetryErrorClassification =
785
785
 
786
786
  const BARE_DEFAULT_WATCHDOG_ERROR =
787
787
  /^(?:[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)$/;
788
+ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
789
+ "anthropic-messages": new Set([
790
+ "Provider stream timed out while waiting for the first event",
791
+ "Anthropic stream timed out while waiting for the first event",
792
+ ]),
793
+ "openai-completions": new Set([
794
+ "Provider stream timed out while waiting for the first event",
795
+ "OpenAI completions stream timed out while waiting for the first event",
796
+ ]),
797
+ } as const;
798
+
799
+ const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
800
+ const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
801
+ "openai-responses": "OpenAI responses stream timed out while waiting for the first event",
802
+ "openai-completions": "OpenAI completions stream timed out while waiting for the first event",
803
+ } as const;
788
804
 
789
805
  function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
790
806
  if (message.errorKind !== undefined || message.errorStatus !== undefined) return true;
@@ -1032,12 +1048,16 @@ function createHandoffContext(document: string): string {
1032
1048
  // ============================================================================
1033
1049
 
1034
1050
  /** Tools that require user permission before execution when an ACP client is connected. */
1035
- const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "edit", "delete", "move"]);
1051
+ const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move"]);
1036
1052
 
1037
1053
  function isShellExecutionPermissionTool(toolName: string): boolean {
1038
1054
  return toolName === "bash" || toolName === "monitor";
1039
1055
  }
1040
1056
 
1057
+ function isExecutionPermissionTool(toolName: string): boolean {
1058
+ return isShellExecutionPermissionTool(toolName) || toolName === "eval";
1059
+ }
1060
+
1041
1061
  /** Permission options presented to the client on each gated tool call. */
1042
1062
  const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
1043
1063
  { optionId: "allow_once", name: "Allow once", kind: "allow_once" },
@@ -1105,6 +1125,18 @@ function getPermissionIntent(
1105
1125
  const cmd = getStringProperty(a, "command")?.slice(0, 80);
1106
1126
  return { toolName, title: cmd || toolName, cacheKey: toolName };
1107
1127
  }
1128
+ if (toolName === "eval") {
1129
+ const cells = Array.isArray(a.cells) ? a.cells : [];
1130
+ const firstCell = cells.find(cell => cell && typeof cell === "object" && !Array.isArray(cell));
1131
+ const cell = firstCell as Record<string, unknown> | undefined;
1132
+ const title = cell ? getStringProperty(cell, "title") : undefined;
1133
+ const language = cell ? getStringProperty(cell, "language") : undefined;
1134
+ return {
1135
+ toolName,
1136
+ title: title ?? (language ? `Eval ${language}` : "eval"),
1137
+ cacheKey: toolName,
1138
+ };
1139
+ }
1108
1140
  if (toolName === "delete") {
1109
1141
  const p = getStringProperty(a, "path");
1110
1142
  return { toolName, title: p ? `Delete ${p}` : toolName, paths: p ? [p] : undefined, cacheKey: toolName };
@@ -5486,6 +5518,12 @@ export class AgentSession {
5486
5518
  return undefined;
5487
5519
  }
5488
5520
 
5521
+ /** Get a registered tool with the same guards used for model-facing execution. */
5522
+ getToolForExecution(name: string): AgentTool | undefined {
5523
+ const tool = this.getToolByName(name);
5524
+ return tool ? this.#prepareToolForExecution(tool) : undefined;
5525
+ }
5526
+
5489
5527
  /**
5490
5528
  * Register a UI/control-plane request handler for a currently foregrounded
5491
5529
  * managed bash execution. This is intentionally narrower than generic
@@ -5750,6 +5788,7 @@ export class AgentSession {
5750
5788
  return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
5751
5789
  }
5752
5790
  const isShellExecutionTool = isShellExecutionPermissionTool(target.name);
5791
+ const isExecutionTool = isExecutionPermissionTool(target.name);
5753
5792
  const command =
5754
5793
  isShellExecutionTool && args && typeof args === "object" && !Array.isArray(args)
5755
5794
  ? getStringProperty(args as Record<string, unknown>, "command")
@@ -5792,7 +5831,7 @@ export class AgentSession {
5792
5831
  toolCallId,
5793
5832
  toolName: target.name,
5794
5833
  title: permissionIntent.title,
5795
- ...(isShellExecutionTool ? { kind: "execute" } : {}),
5834
+ ...(isExecutionTool ? { kind: "execute" } : {}),
5796
5835
  status: "pending",
5797
5836
  rawInput: args,
5798
5837
  ...(commandContent ? { content: commandContent } : {}),
@@ -7514,7 +7553,11 @@ export class AgentSession {
7514
7553
  }
7515
7554
 
7516
7555
  // Validate API key
7517
- const apiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
7556
+ const apiKey = await this.#awaitPromptPreflight(
7557
+ generation,
7558
+ preflightSignal,
7559
+ this.#modelRegistry.getApiKey(this.model, this.sessionId, { signal: preflightSignal }),
7560
+ );
7518
7561
  if (!apiKey) {
7519
7562
  throw new Error(formatNoCredentialOnboardingError(this.model.provider));
7520
7563
  }
@@ -8997,6 +9040,10 @@ export class AgentSession {
8997
9040
  this.agent.reset();
8998
9041
  if (!options?.drop) await this.sessionManager.flush();
8999
9042
  await this.sessionManager.newSession(options);
9043
+ // Gate the successor local:// root before the successor identity is
9044
+ // published to the agent, the workflow-gate emitter, hooks, or any
9045
+ // synchronous resolver (#2797 / #2925).
9046
+ await initializeLocalRoot(this.#localProtocolOptions());
9000
9047
  this.setTodoPhases([]);
9001
9048
  this.#syncAgentSessionId();
9002
9049
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9064,6 +9111,10 @@ export class AgentSession {
9064
9111
  this.#rebindProviderSessionState(new Map());
9065
9112
  this.agent.reset();
9066
9113
  await this.sessionManager.newSession(options);
9114
+ // Gate the successor local:// root before the successor identity is
9115
+ // published to the agent, the workflow-gate emitter, hooks, or any
9116
+ // synchronous resolver (#2797 / #2925).
9117
+ await initializeLocalRoot(this.#localProtocolOptions());
9067
9118
  this.setTodoPhases([]);
9068
9119
  this.#syncAgentSessionId();
9069
9120
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9207,6 +9258,9 @@ export class AgentSession {
9207
9258
  return false;
9208
9259
  }
9209
9260
 
9261
+ // Fork rotates the session identity (and copies artifacts); gate the
9262
+ // successor local:// root before that identity is published (#2797 / #2925).
9263
+ await initializeLocalRoot(this.#localProtocolOptions());
9210
9264
  // Update agent session ID
9211
9265
  this.#syncAgentSessionId();
9212
9266
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -10614,6 +10668,10 @@ export class AgentSession {
10614
10668
  previousSessionFile ? { parentSession: previousSessionFile } : undefined,
10615
10669
  );
10616
10670
  successorSessionFile = this.sessionFile;
10671
+ // Gate the successor local:// root inside the reversible prepare
10672
+ // window, before the successor identity is published; a gate failure
10673
+ // here rolls back (#2797 / #2925).
10674
+ await initializeLocalRoot(this.#localProtocolOptions());
10617
10675
  this.agent.reset();
10618
10676
  this.#syncAgentSessionId();
10619
10677
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -12432,6 +12490,7 @@ export class AgentSession {
12432
12490
  }
12433
12491
  const retryAfterMs = this.#parseRetryAfterMsFromError(message);
12434
12492
  const shouldRetry =
12493
+ !this.#isTerminalProviderCompactionTimeout(candidate, message) &&
12435
12494
  retrySettings.enabled &&
12436
12495
  attempt < retrySettings.maxRetries &&
12437
12496
  (retryAfterMs !== undefined ||
@@ -12617,7 +12676,61 @@ export class AgentSession {
12617
12676
  * unknown/no-code errors are retryable.
12618
12677
  */
12619
12678
 
12679
+ #isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
12680
+ if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
12681
+ const errorMessage = message.errorMessage ?? "";
12682
+ if (message.api === "anthropic-messages") {
12683
+ return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
12684
+ }
12685
+ if (message.api === "openai-completions") {
12686
+ return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["openai-completions"].has(errorMessage);
12687
+ }
12688
+ return false;
12689
+ }
12690
+
12691
+ #isKimiCodeCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12692
+ if (candidate.provider !== "kimi-code") return false;
12693
+ return /^(?:Summarization failed|Turn prefix summarization failed): (?:Provider|Anthropic|OpenAI completions) stream timed out while waiting for the first event$/.test(
12694
+ errorMessage,
12695
+ );
12696
+ }
12697
+
12698
+ #alibabaTokenPlanCanonicalTimeoutForApi(api: string): string | undefined {
12699
+ if (api === "openai-responses" || api === "openai-completions") {
12700
+ return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
12701
+ }
12702
+ return undefined;
12703
+ }
12704
+
12705
+ #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
12706
+ if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12707
+ const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(message.api);
12708
+ return canonicalMessage !== undefined && message.errorMessage === canonicalMessage;
12709
+ }
12710
+
12711
+ #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12712
+ if (candidate.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12713
+ const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(candidate.api);
12714
+ if (canonicalMessage === undefined) return false;
12715
+ return (
12716
+ errorMessage === `Summarization failed: ${canonicalMessage}` ||
12717
+ errorMessage === `Turn prefix summarization failed: ${canonicalMessage}`
12718
+ );
12719
+ }
12720
+
12721
+ #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
12722
+ return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
12723
+ }
12724
+
12725
+ #isTerminalProviderCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12726
+ return (
12727
+ this.#isKimiCodeCompactionTimeout(candidate, errorMessage) ||
12728
+ this.#isAlibabaTokenPlanCompactionTimeout(candidate, errorMessage)
12729
+ );
12730
+ }
12731
+
12620
12732
  #isRetryableError(message: AssistantMessage): boolean {
12733
+ if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
12621
12734
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
12622
12735
  const transportFailure = message.transportFailure;
12623
12736
  const contextWindow = this.model?.contextWindow ?? 0;
@@ -12767,6 +12880,9 @@ export class AgentSession {
12767
12880
  if (isTerminalHttp4xx && (explicitStatus !== undefined || !/rate.?limit|too many requests/i.test(err))) {
12768
12881
  return "terminal";
12769
12882
  }
12883
+ if (this.#isTerminalProviderFirstEventTimeout(message)) {
12884
+ return "terminal";
12885
+ }
12770
12886
  // A first-event timeout on ollama-cloud (the ollama-chat API) must not
12771
12887
  // join the unbounded transient class: each continuation retry re-issues
12772
12888
  // the full request to a remote, billable backend, so an unbounded loop
@@ -12949,6 +13065,18 @@ export class AgentSession {
12949
13065
  },
12950
13066
  };
12951
13067
  }
13068
+ if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
13069
+ // The managed transport discarded this attempt before session policy saw it.
13070
+ // Remove its provisional controller charge and surface the original message.
13071
+ this.#defaultFallbackChain().discardStartedAttempt();
13072
+ return {
13073
+ type: "terminal",
13074
+ terminal: {
13075
+ stopReason: "error",
13076
+ messages: [outcome.failure.message],
13077
+ },
13078
+ };
13079
+ }
12952
13080
  return this.#handleRetryableError(
12953
13081
  outcome.failure.message,
12954
13082
  true,
@@ -14471,6 +14599,10 @@ export class AgentSession {
14471
14599
 
14472
14600
  try {
14473
14601
  await this.sessionManager.setSessionFile(sessionPath);
14602
+ // The successor identity is already rotated in the manager but not yet
14603
+ // published; gate its local:// root before publication so no observer
14604
+ // can resolve against an ungated root (#2797 / #2925).
14605
+ if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14474
14606
  this.#syncAgentSessionId();
14475
14607
  this.#rekeyHindsightMemoryForCurrentSessionId();
14476
14608
 
@@ -14547,13 +14679,8 @@ export class AgentSession {
14547
14679
  await this.sessionManager.ensureOnDisk();
14548
14680
 
14549
14681
  if (switchingToDifferentSession) {
14550
- // Interactive /resume (and any other switchSession identity change) must
14551
- // await verified legacy local:// migration for the *new* session before
14552
- // post-commit session_switch / local path resolution. createAgentSession
14553
- // already does this at cold start (#2797); switchSession previously omitted
14554
- // it, so resolving local:// after /resume could fail-closed with
14555
- // "legacy migration must complete before path resolution" (#2925).
14556
- await initializeLocalRoot(this.#localProtocolOptions());
14682
+ // The local:// migration gate for this successor already ran above,
14683
+ // before the identity was published (#2797 / #2925).
14557
14684
  this.#resetHindsightConversationTrackingIfHindsight();
14558
14685
  this.#resetIrcRosterDeliveryState();
14559
14686
  }
@@ -14685,6 +14812,9 @@ export class AgentSession {
14685
14812
  } else {
14686
14813
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
14687
14814
  }
14815
+ // Branch/tree-jump establishes a new session identity; gate the successor
14816
+ // local:// root before that identity is published (#2797 / #2925).
14817
+ await initializeLocalRoot(this.#localProtocolOptions());
14688
14818
  this.#syncTodoPhasesFromBranch();
14689
14819
  this.#syncAgentSessionId();
14690
14820
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);