@gajae-code/coding-agent 0.11.9 → 0.11.11

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 (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
@@ -172,6 +172,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a
172
172
  import { reset as resetCapabilities } from "../capability";
173
173
  import type { Rule } from "../capability/rule";
174
174
  import type { CasReceipt } from "../config/atomic-yaml-patch";
175
+ import { activateModelProfile } from "../config/model-profile-activation";
175
176
  import {
176
177
  GJC_MODEL_ASSIGNMENT_TARGETS,
177
178
  isAuthenticated,
@@ -233,7 +234,7 @@ import type {
233
234
  TurnEndEvent,
234
235
  TurnStartEvent,
235
236
  } from "../extensibility/extensions";
236
- import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
237
+ import type { CompactOptions, ContextUsage, ExtensionTranscriptEntry } from "../extensibility/extensions/types";
237
238
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
238
239
  import {
239
240
  type LoadedSubskillActivation,
@@ -243,7 +244,13 @@ import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injec
243
244
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
244
245
  import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
245
246
  import type { HookCommandContext } from "../extensibility/hooks/types";
246
- import { buildSkillPromptMessage, type Skill, type SkillWarning } from "../extensibility/skills";
247
+ import {
248
+ buildSkillPromptMessage,
249
+ getSkillSlashCommandName,
250
+ parseSkillInvocations,
251
+ type Skill,
252
+ type SkillWarning,
253
+ } from "../extensibility/skills";
247
254
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
248
255
  import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
249
256
  import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
@@ -341,6 +348,7 @@ import {
341
348
  import {
342
349
  type ConfiguredFallbackChain,
343
350
  cappedExponentialWithFullJitter,
351
+ compactionRetryDelay,
344
352
  effectiveFallbackDelay,
345
353
  FallbackChainController,
346
354
  } from "./fallback-chain-controller";
@@ -379,6 +387,7 @@ import type {
379
387
  CompactionEntry,
380
388
  DefaultModelSelectionStage,
381
389
  NewSessionOptions,
390
+ PreparedNewSession,
382
391
  RecoveryHydrationContext,
383
392
  RecoveryHydrationPromotionFence,
384
393
  SessionContext,
@@ -798,8 +807,14 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
798
807
 
799
808
  const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
800
809
  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",
810
+ "openai-responses": new Set([
811
+ "Provider stream timed out while waiting for the first event",
812
+ "OpenAI responses stream timed out while waiting for the first event",
813
+ ]),
814
+ "openai-completions": new Set([
815
+ "Provider stream timed out while waiting for the first event",
816
+ "OpenAI completions stream timed out while waiting for the first event",
817
+ ]),
803
818
  } as const;
804
819
 
805
820
  function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
@@ -1043,17 +1058,37 @@ function createHandoffContext(document: string): string {
1043
1058
  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.`;
1044
1059
  }
1045
1060
 
1061
+ async function discardPreparedNewSessionAfterFailure(
1062
+ sessionManager: SessionManager,
1063
+ prepared: PreparedNewSession,
1064
+ transitionError: unknown,
1065
+ ): Promise<unknown> {
1066
+ try {
1067
+ await sessionManager.discardPreparedNewSession(prepared);
1068
+ } catch (cleanupError) {
1069
+ throw new AggregateError(
1070
+ [transitionError, cleanupError],
1071
+ "Session transition and staged successor cleanup both failed.",
1072
+ );
1073
+ }
1074
+ return transitionError;
1075
+ }
1076
+
1046
1077
  // ============================================================================
1047
1078
  // ACP Permission Gate
1048
1079
  // ============================================================================
1049
1080
 
1050
1081
  /** Tools that require user permission before execution when an ACP client is connected. */
1051
- const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "edit", "delete", "move"]);
1082
+ const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move"]);
1052
1083
 
1053
1084
  function isShellExecutionPermissionTool(toolName: string): boolean {
1054
1085
  return toolName === "bash" || toolName === "monitor";
1055
1086
  }
1056
1087
 
1088
+ function isExecutionPermissionTool(toolName: string): boolean {
1089
+ return isShellExecutionPermissionTool(toolName) || toolName === "eval";
1090
+ }
1091
+
1057
1092
  /** Permission options presented to the client on each gated tool call. */
1058
1093
  const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
1059
1094
  { optionId: "allow_once", name: "Allow once", kind: "allow_once" },
@@ -1121,6 +1156,18 @@ function getPermissionIntent(
1121
1156
  const cmd = getStringProperty(a, "command")?.slice(0, 80);
1122
1157
  return { toolName, title: cmd || toolName, cacheKey: toolName };
1123
1158
  }
1159
+ if (toolName === "eval") {
1160
+ const cells = Array.isArray(a.cells) ? a.cells : [];
1161
+ const firstCell = cells.find(cell => cell && typeof cell === "object" && !Array.isArray(cell));
1162
+ const cell = firstCell as Record<string, unknown> | undefined;
1163
+ const title = cell ? getStringProperty(cell, "title") : undefined;
1164
+ const language = cell ? getStringProperty(cell, "language") : undefined;
1165
+ return {
1166
+ toolName,
1167
+ title: title ?? (language ? `Eval ${language}` : "eval"),
1168
+ cacheKey: toolName,
1169
+ };
1170
+ }
1124
1171
  if (toolName === "delete") {
1125
1172
  const p = getStringProperty(a, "path");
1126
1173
  return { toolName, title: p ? `Delete ${p}` : toolName, paths: p ? [p] : undefined, cacheKey: toolName };
@@ -3992,8 +4039,21 @@ export class AgentSession {
3992
4039
  return;
3993
4040
  }
3994
4041
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4042
+ const startsQueuedSuccessor =
4043
+ this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
3995
4044
  try {
3996
- await this.agent.continue(this.#managedFallbackPromptOptions());
4045
+ await this.agent.continue({
4046
+ ...this.#managedFallbackPromptOptions(),
4047
+ // Reset only after continue() has claimed the queued turn. Skipped or stale
4048
+ // continuations retain predecessor accounting, and resetAttemptBudget keeps
4049
+ // the sticky fallback cursor unchanged.
4050
+ onRunAccepted: startsQueuedSuccessor
4051
+ ? () => {
4052
+ this.#defaultFallbackChain().resetAttemptBudget();
4053
+ this.#overflowMaintenanceAttempts = 0;
4054
+ }
4055
+ : undefined,
4056
+ });
3997
4057
  } catch (error) {
3998
4058
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
3999
4059
  throw error;
@@ -4596,13 +4656,13 @@ export class AgentSession {
4596
4656
  return resolveToCwd(normalized, this.sessionManager.getCwd());
4597
4657
  }
4598
4658
 
4599
- #localProtocolOptions(): LocalProtocolOptions {
4659
+ #localProtocolOptions(prepared?: PreparedNewSession): LocalProtocolOptions {
4600
4660
  return {
4601
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
4661
+ getArtifactsDir: () => prepared?.artifactsDir ?? this.sessionManager.getArtifactsDir(),
4602
4662
  isManagedDestination: () => this.sessionManager.isManagedDestination(),
4603
- // Required for verified legacy local:// migration on managed sessions (#2797 / #2925).
4604
- getManagedLegacyLocalMigrationSource: () => this.sessionManager.getManagedLegacyLocalMigrationSource(),
4605
- getSessionId: () => this.sessionManager.getSessionId(),
4663
+ getManagedLegacyLocalMigrationSource: () =>
4664
+ prepared?.managedLegacyLocalMigrationSource ?? this.sessionManager.getManagedLegacyLocalMigrationSource(),
4665
+ getSessionId: () => prepared?.sessionId ?? this.sessionManager.getSessionId(),
4606
4666
  };
4607
4667
  }
4608
4668
 
@@ -5502,6 +5562,12 @@ export class AgentSession {
5502
5562
  return undefined;
5503
5563
  }
5504
5564
 
5565
+ /** Get a registered tool with the same guards used for model-facing execution. */
5566
+ getToolForExecution(name: string): AgentTool | undefined {
5567
+ const tool = this.getToolByName(name);
5568
+ return tool ? this.#prepareToolForExecution(tool) : undefined;
5569
+ }
5570
+
5505
5571
  /**
5506
5572
  * Register a UI/control-plane request handler for a currently foregrounded
5507
5573
  * managed bash execution. This is intentionally narrower than generic
@@ -5766,6 +5832,7 @@ export class AgentSession {
5766
5832
  return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
5767
5833
  }
5768
5834
  const isShellExecutionTool = isShellExecutionPermissionTool(target.name);
5835
+ const isExecutionTool = isExecutionPermissionTool(target.name);
5769
5836
  const command =
5770
5837
  isShellExecutionTool && args && typeof args === "object" && !Array.isArray(args)
5771
5838
  ? getStringProperty(args as Record<string, unknown>, "command")
@@ -5808,7 +5875,7 @@ export class AgentSession {
5808
5875
  toolCallId,
5809
5876
  toolName: target.name,
5810
5877
  title: permissionIntent.title,
5811
- ...(isShellExecutionTool ? { kind: "execute" } : {}),
5878
+ ...(isExecutionTool ? { kind: "execute" } : {}),
5812
5879
  status: "pending",
5813
5880
  rawInput: args,
5814
5881
  ...(commandContent ? { content: commandContent } : {}),
@@ -6456,6 +6523,15 @@ export class AgentSession {
6456
6523
  return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6457
6524
  }
6458
6525
 
6526
+ /** Build display context from an unpublished successor without changing active session state. @internal */
6527
+ buildPreparedDisplaySessionContext(prepared: PreparedNewSession): SessionContext {
6528
+ const context = deobfuscateSessionContext(
6529
+ this.sessionManager.buildPreparedNewSessionContext(prepared),
6530
+ this.#obfuscator,
6531
+ );
6532
+ return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6533
+ }
6534
+
6459
6535
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
6460
6536
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
6461
6537
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -6563,9 +6639,11 @@ export class AgentSession {
6563
6639
  /** Live SDK configuration values exposed through the session query surface. */
6564
6640
  getSdkConfigItems(): Record<string, string> {
6565
6641
  const model = this.model;
6642
+ const modelPreset = this.getActiveModelProfile() ?? this.settings.get("modelProfile.default");
6566
6643
  return {
6567
6644
  mode: this.#planModeState?.enabled ? "plan" : "default",
6568
6645
  ...(model ? { model: `${model.provider}/${model.id}` } : {}),
6646
+ ...(modelPreset ? { modelPreset } : {}),
6569
6647
  thinking: this.#thinkingLevel ?? "off",
6570
6648
  steeringMode: this.steeringMode,
6571
6649
  followUpMode: this.followUpMode,
@@ -6584,6 +6662,7 @@ export class AgentSession {
6584
6662
  async invokeSkill(
6585
6663
  name: string,
6586
6664
  args = "",
6665
+ options?: Pick<PromptOptions, "onPreflightAccepted">,
6587
6666
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6588
6667
  const skillName = name.trim();
6589
6668
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6616,7 +6695,7 @@ export class AgentSession {
6616
6695
  attribution: "user" as const,
6617
6696
  };
6618
6697
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
6619
- await this.promptCustomMessage(skillPromptMessage);
6698
+ await this.promptCustomMessage(skillPromptMessage, options);
6620
6699
  return {
6621
6700
  name: skill.name,
6622
6701
  path: skill.filePath,
@@ -6662,22 +6741,49 @@ export class AgentSession {
6662
6741
  }
6663
6742
  }
6664
6743
 
6665
- getTranscript(): Array<{ id: string; role: string; textSummary: string; ts: string; body: string }> {
6744
+ getTranscript(): ExtensionTranscriptEntry[] {
6666
6745
  return getEntriesForInternalRead(this.sessionManager).flatMap(entry => {
6667
6746
  if (entry.type !== "message") return [];
6668
- const message = entry.message as unknown as { role?: unknown; content?: unknown };
6747
+ const message = entry.message as unknown as {
6748
+ role?: unknown;
6749
+ content?: unknown;
6750
+ toolCallId?: unknown;
6751
+ toolName?: unknown;
6752
+ isError?: unknown;
6753
+ };
6754
+ const content: NonNullable<ExtensionTranscriptEntry["content"]> | undefined = Array.isArray(message.content)
6755
+ ? message.content.reduce<NonNullable<ExtensionTranscriptEntry["content"]>>((blocks, part) => {
6756
+ if (typeof part !== "object" || part === null || !("type" in part)) return blocks;
6757
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
6758
+ blocks.push({ type: "text", text: part.text });
6759
+ return blocks;
6760
+ }
6761
+ if (part.type === "thinking" && "thinking" in part && typeof part.thinking === "string") {
6762
+ blocks.push({ type: "thinking", thinking: part.thinking });
6763
+ return blocks;
6764
+ }
6765
+ if (
6766
+ part.type === "toolCall" &&
6767
+ "id" in part &&
6768
+ typeof part.id === "string" &&
6769
+ "name" in part &&
6770
+ typeof part.name === "string"
6771
+ ) {
6772
+ blocks.push({
6773
+ type: "toolCall",
6774
+ id: part.id,
6775
+ name: part.name,
6776
+ arguments: "arguments" in part ? part.arguments : {},
6777
+ });
6778
+ }
6779
+ return blocks;
6780
+ }, [])
6781
+ : undefined;
6669
6782
  const body =
6670
6783
  typeof message.content === "string"
6671
6784
  ? message.content
6672
- : Array.isArray(message.content)
6673
- ? message.content
6674
- .map(part =>
6675
- typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
6676
- ? part.text
6677
- : "",
6678
- )
6679
- .filter(Boolean)
6680
- .join("\n")
6785
+ : content
6786
+ ? content.flatMap(part => (part.type === "text" ? [part.text] : [])).join("\n")
6681
6787
  : "";
6682
6788
  return [
6683
6789
  {
@@ -6686,6 +6792,10 @@ export class AgentSession {
6686
6792
  textSummary: body.slice(0, 500),
6687
6793
  ts: entry.timestamp,
6688
6794
  body,
6795
+ ...(content ? { content } : {}),
6796
+ ...(typeof message.toolCallId === "string" ? { toolCallId: message.toolCallId } : {}),
6797
+ ...(typeof message.toolName === "string" ? { toolName: message.toolName } : {}),
6798
+ ...(typeof message.isError === "boolean" ? { isError: message.isError } : {}),
6689
6799
  },
6690
6800
  ];
6691
6801
  });
@@ -6723,8 +6833,7 @@ export class AgentSession {
6723
6833
  return getAskAnswerSourceFromRegistry(this.sessionId);
6724
6834
  }
6725
6835
 
6726
- #constructWorkflowGateEmitter(): WorkflowGateEmitter {
6727
- const sessionId = this.sessionManager.getSessionId();
6836
+ #constructWorkflowGateEmitter(sessionId = this.sessionManager.getSessionId()): WorkflowGateEmitter {
6728
6837
  assertNonEmptyGjcSessionId(sessionId, "AgentSession workflow-gate session");
6729
6838
  const gateStore = this.sessionManager.isPersisted()
6730
6839
  ? new FileGateStore(path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"))
@@ -7243,6 +7352,22 @@ export class AgentSession {
7243
7352
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7244
7353
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7245
7354
 
7355
+ if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
7356
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
7357
+ const invocations = parseSkillInvocations(text, skillCommands);
7358
+ if (invocations.length === 1) {
7359
+ const invocation = invocations[0];
7360
+ if (invocation) {
7361
+ await this.invokeSkill(
7362
+ invocation.skill.name,
7363
+ invocation.args,
7364
+ options?.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : undefined,
7365
+ );
7366
+ return;
7367
+ }
7368
+ }
7369
+ }
7370
+
7246
7371
  // Handle extension commands first (execute immediately, even during streaming)
7247
7372
  if (expandPromptTemplates && text.startsWith("/")) {
7248
7373
  const handled = await this.#tryExecuteExtensionCommand(text);
@@ -7422,7 +7547,7 @@ export class AgentSession {
7422
7547
 
7423
7548
  async promptCustomMessage<T = unknown>(
7424
7549
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7425
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
7550
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted">,
7426
7551
  ): Promise<void> {
7427
7552
  const textContent =
7428
7553
  typeof message.content === "string"
@@ -7530,7 +7655,11 @@ export class AgentSession {
7530
7655
  }
7531
7656
 
7532
7657
  // Validate API key
7533
- const apiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
7658
+ const apiKey = await this.#awaitPromptPreflight(
7659
+ generation,
7660
+ preflightSignal,
7661
+ this.#modelRegistry.getApiKey(this.model, this.sessionId, { signal: preflightSignal }),
7662
+ );
7534
7663
  if (!apiKey) {
7535
7664
  throw new Error(formatNoCredentialOnboardingError(this.model.provider));
7536
7665
  }
@@ -7817,6 +7946,7 @@ export class AgentSession {
7817
7946
  return tool ? { safeSummary: tool.safeSummary, safeSummaryFields: tool.safeSummaryFields } : undefined;
7818
7947
  },
7819
7948
  cycleModel: () => this.cycleModel(),
7949
+ setModelProfile: name => this.activateModelProfileForControl(name),
7820
7950
  cycleThinkingLevel: () => this.cycleThinkingLevel(),
7821
7951
  setQueueMode: (kind, mode) => {
7822
7952
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -7844,6 +7974,7 @@ export class AgentSession {
7844
7974
  getJobs: () => undefined,
7845
7975
  sdkBindings: () => [
7846
7976
  "cycleModel",
7977
+ "setModelProfile",
7847
7978
  "cycleThinkingLevel",
7848
7979
  "setQueueMode",
7849
7980
  "getSkillState",
@@ -9012,7 +9143,14 @@ export class AgentSession {
9012
9143
  this.#rebindProviderSessionState(new Map());
9013
9144
  this.agent.reset();
9014
9145
  if (!options?.drop) await this.sessionManager.flush();
9015
- await this.sessionManager.newSession(options);
9146
+ const prepared = await this.sessionManager.prepareNewSession(options);
9147
+ try {
9148
+ // Last fallible gate while public getters still show the predecessor (#3138).
9149
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9150
+ this.sessionManager.commitPreparedNewSession(prepared);
9151
+ } catch (error) {
9152
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9153
+ }
9016
9154
  this.setTodoPhases([]);
9017
9155
  this.#syncAgentSessionId();
9018
9156
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9074,12 +9212,18 @@ export class AgentSession {
9074
9212
  if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) {
9075
9213
  throw new Error("Owned async jobs did not settle before session replacement.");
9076
9214
  }
9077
-
9215
+ const prepared = await this.sessionManager.prepareNewSession(options);
9216
+ try {
9217
+ // Last fallible gate while public getters still show the predecessor (#3138).
9218
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9219
+ this.sessionManager.commitPreparedNewSession(prepared);
9220
+ } catch (error) {
9221
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9222
+ }
9078
9223
  this.#disconnectFromAgent();
9079
9224
  this.#closeAllProviderSessions("new session");
9080
9225
  this.#rebindProviderSessionState(new Map());
9081
9226
  this.agent.reset();
9082
- await this.sessionManager.newSession(options);
9083
9227
  this.setTodoPhases([]);
9084
9228
  this.#syncAgentSessionId();
9085
9229
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9122,10 +9266,17 @@ export class AgentSession {
9122
9266
  }
9123
9267
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
9124
9268
  if (nextDiscoverySessionToolNames) {
9125
- await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9126
- persistMCPSelection: false,
9127
- nextSelectedDiscoveredBuiltinToolNames: [],
9128
- });
9269
+ // Avoid a full system-prompt rebuild when the active set is unchanged; rebuild
9270
+ // can block on shared workspace-tree work across repeated /new transitions.
9271
+ const current = this.getActiveToolNames().slice().sort().join("\0");
9272
+ const next = [...new Set(nextDiscoverySessionToolNames.map(n => n.toLowerCase()))].sort().join("\0");
9273
+ if (current !== next) {
9274
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9275
+ persistMCPSelection: false,
9276
+ nextSelectedDiscoveredBuiltinToolNames: [],
9277
+ });
9278
+ } else {
9279
+ }
9129
9280
  }
9130
9281
  this.#todoReminderCount = 0;
9131
9282
  this.#planReferenceSent = false;
@@ -9138,6 +9289,7 @@ export class AgentSession {
9138
9289
  reason: "new",
9139
9290
  previousSessionFile,
9140
9291
  });
9292
+ } else {
9141
9293
  }
9142
9294
  }
9143
9295
 
@@ -9217,13 +9369,18 @@ export class AgentSession {
9217
9369
  // Flush current session to ensure all entries are written
9218
9370
  await this.sessionManager.flush();
9219
9371
 
9220
- // Fork the session (creates new session file with same entries)
9221
- const forkResult = await this.sessionManager.fork();
9222
- if (!forkResult) {
9372
+ // Prepare the copied successor and complete local:// readiness while all
9373
+ // public manager getters remain bound to the predecessor.
9374
+ const prepared = await this.sessionManager.prepareFork();
9375
+ if (!prepared) {
9223
9376
  return false;
9224
9377
  }
9225
-
9226
- // Update agent session ID
9378
+ try {
9379
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9380
+ this.sessionManager.commitPreparedNewSession(prepared);
9381
+ } catch (error) {
9382
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9383
+ }
9227
9384
  this.#syncAgentSessionId();
9228
9385
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
9229
9386
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -9312,6 +9469,17 @@ export class AgentSession {
9312
9469
  return this.#activeModelProfile;
9313
9470
  }
9314
9471
 
9472
+ /** Activate a complete model profile through a nonvisual session control. */
9473
+ async activateModelProfileForControl(profileName: string): Promise<boolean> {
9474
+ await activateModelProfile({
9475
+ session: this,
9476
+ modelRegistry: this.#modelRegistry,
9477
+ settings: this.settings,
9478
+ profileName,
9479
+ });
9480
+ return this.getActiveModelProfile() === profileName;
9481
+ }
9482
+
9315
9483
  /** Return the persisted configured fallback selectors for a model role. */
9316
9484
  getConfiguredModelChain(role: string): readonly string[] | undefined {
9317
9485
  return getSessionContextForInternalRead(this.sessionManager).configuredModelChains[role]?.entries;
@@ -10614,22 +10782,36 @@ export class AgentSession {
10614
10782
  // user work that agent.reset() would otherwise clear.
10615
10783
  const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
10616
10784
  const rollbackAgentFollowUpQueue = this.agent.snapshotFollowUp();
10617
- let successorSessionFile: string | undefined;
10618
10785
  let savedPath: string | undefined;
10619
10786
  let committed = false;
10620
- // Suspend (do not destroy) the workflow-gate emitter so it can be fenced on
10621
- // commit or resumed on rollback. Suspension runs inside the transaction so a
10622
- // listener fault during suspension is rolled back like any other prepare step.
10623
- let suspendedWorkflowGateEmitter: WorkflowGateEmitter | undefined;
10787
+ let prepared: PreparedNewSession | undefined;
10624
10788
  try {
10625
- suspendedWorkflowGateEmitter = this.#suspendWorkflowGateEmitter(rollbackSessionState.sessionId);
10626
- // --- Prepare (reversible): build and persist the successor session
10627
- // without touching predecessor authority. No irreversible predecessor
10628
- // teardown or identity publication happens until the commit boundary.
10629
- await this.sessionManager.newSession(
10789
+ // Prepare successor entries, persistence, display state, and gate
10790
+ // construction without publishing manager identity. Managed local-root
10791
+ // readiness is the last fallible step before the sole commit (#3138).
10792
+ prepared = await this.sessionManager.prepareNewSession(
10630
10793
  previousSessionFile ? { parentSession: previousSessionFile } : undefined,
10631
10794
  );
10632
- successorSessionFile = this.sessionFile;
10795
+ if (model) this.sessionManager.appendPreparedModelChange(prepared, `${model.provider}/${model.id}`);
10796
+ this.sessionManager.appendPreparedThinkingLevelChange(prepared, this.thinkingLevel);
10797
+ this.sessionManager.appendPreparedServiceTierChange(prepared, this.serviceTier ?? null);
10798
+ this.sessionManager.appendPreparedCustomMessageEntry(
10799
+ prepared,
10800
+ "handoff",
10801
+ createHandoffContext(handoffText),
10802
+ true,
10803
+ undefined,
10804
+ "agent",
10805
+ );
10806
+ await this.sessionManager.ensurePreparedNewSessionOnDisk(prepared);
10807
+ const sessionContext = this.buildPreparedDisplaySessionContext(prepared);
10808
+ const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
10809
+ // Last fallible action: verified local:// readiness from immutable staged options.
10810
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
10811
+
10812
+ // --- Commit boundary: synchronous adoption is the sole identity publication.
10813
+ this.sessionManager.commitPreparedNewSession(prepared);
10814
+ committed = true;
10633
10815
  this.agent.reset();
10634
10816
  this.#syncAgentSessionId();
10635
10817
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -10638,16 +10820,8 @@ export class AgentSession {
10638
10820
  this.#pendingNextTurnMessages = [];
10639
10821
  this.#scheduledHiddenNextTurnGeneration = undefined;
10640
10822
  this.#todoReminderCount = 0;
10641
- if (model) {
10642
- this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
10643
- }
10644
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
10645
- this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
10646
-
10647
- // Inject the handoff document as a custom message
10648
- const handoffContent = createHandoffContext(handoffText);
10649
- this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
10650
- await this.sessionManager.ensureOnDisk();
10823
+ this.agent.replaceMessages(sessionContext.messages);
10824
+ this.#syncTodoPhasesFromBranch();
10651
10825
  if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
10652
10826
  try {
10653
10827
  const artifactId = await this.sessionManager.saveArtifact(`${handoffText}\n`, "handoff");
@@ -10659,30 +10833,9 @@ export class AgentSession {
10659
10833
  }
10660
10834
  }
10661
10835
 
10662
- // Rebuild agent messages from session
10663
- const sessionContext = this.buildDisplaySessionContext();
10664
- this.agent.replaceMessages(sessionContext.messages);
10665
- this.#syncTodoPhasesFromBranch();
10666
-
10667
- // Construct the successor workflow-gate emitter in the reversible window:
10668
- // FileGateStore load / broker init can throw, and a failure here must roll
10669
- // back rather than corrupt an already-committed switch. Publication is
10670
- // deferred to the (no-throw) commit step below.
10671
- const successorGateEmitter = this.#constructWorkflowGateEmitter();
10672
-
10673
- // --- Commit boundary: the successor is durably persisted and every
10674
- // reversible, fallible step above has succeeded. Cross the irreversible
10675
- // commit BEFORE any identity rotation. Fencing the predecessor gate
10676
- // emitter and rotating provider sessions cannot be undone, so once we
10677
- // begin them a failure must RETAIN the committed successor rather than
10678
- // attempt a (now-impossible) rollback that would resume a fenced emitter.
10679
- committed = true;
10836
+ // The forward-only suffix starts only after the committed successor is live.
10680
10837
  this.#resetInjectedContextSignatures();
10681
- this.#publishWorkflowGateEmitter(
10682
- successorGateEmitter,
10683
- rollbackSessionState.sessionId,
10684
- suspendedWorkflowGateEmitter,
10685
- );
10838
+ this.#publishWorkflowGateEmitter(successorGateEmitter, rollbackSessionState.sessionId);
10686
10839
  this.#closeAllProviderSessions("session handoff");
10687
10840
  this.#rebindProviderSessionState(new Map());
10688
10841
  this.#resetHindsightConversationTrackingIfHindsight();
@@ -10718,12 +10871,12 @@ export class AgentSession {
10718
10871
  return { document: handoffText, savedPath };
10719
10872
  } catch (switchError) {
10720
10873
  if (committed) {
10721
- // The switch already committed; a post-commit step failed. Do not
10722
- // tear down the new session — surface success (the handoff exists).
10723
- logger.warn("Handoff post-commit step failed after the session switch committed", {
10724
- error: switchError instanceof Error ? switchError.message : String(switchError),
10874
+ throw Object.assign(new Error("Handoff committed but successor initialization was degraded."), {
10875
+ code: "handoff_committed_degraded",
10876
+ cause: switchError,
10877
+ handoffDocument: handoffText,
10878
+ savedPath,
10725
10879
  });
10726
- return { document: handoffText, savedPath };
10727
10880
  }
10728
10881
  // Reversible window: roll back to the pre-handoff session so the
10729
10882
  // failure is non-destructive. Predecessor gate emitter, provider
@@ -10731,7 +10884,6 @@ export class AgentSession {
10731
10884
  // were never mutated before commit, so they survive intact.
10732
10885
  this.sessionManager.restoreState(rollbackSessionState);
10733
10886
  this.#syncAgentSessionId(rollbackSessionState.sessionId);
10734
- this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
10735
10887
  this.#rekeyHindsightMemoryForCurrentSessionId();
10736
10888
  this.agent.replaceMessages(rollbackAgentMessages);
10737
10889
  this.agent.clearAllQueues();
@@ -10743,25 +10895,17 @@ export class AgentSession {
10743
10895
  this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
10744
10896
  this.#todoReminderCount = rollbackTodoReminderCount;
10745
10897
  this.#syncTodoPhasesFromBranch();
10746
- // Remove the orphaned successor transcript/artifacts written before the
10747
- // failure so a rolled-back handoff leaves nothing behind.
10748
- if (successorSessionFile && successorSessionFile !== previousSessionFile) {
10749
- try {
10750
- await this.sessionManager.discardUncommittedSession(successorSessionFile);
10751
- } catch (cleanupError) {
10752
- logger.warn("Failed to remove orphaned handoff session after rollback", {
10753
- path: successorSessionFile,
10754
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
10755
- });
10756
- }
10757
- }
10898
+ // Exact-discard only the staged successor; predecessor state was never adopted.
10899
+ const rollbackError = prepared
10900
+ ? await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, switchError)
10901
+ : switchError;
10758
10902
  // Map to cancellation only for a genuine handoff-signal abort; a
10759
10903
  // downstream error keeps its cause and the generated document.
10760
10904
  if (handoffSignal.aborted) {
10761
10905
  throw new Error("Handoff cancelled");
10762
10906
  }
10763
10907
  // Preserve the generated handoff document for copy/retry.
10764
- throw Object.assign(switchError instanceof Error ? switchError : new Error(String(switchError)), {
10908
+ throw Object.assign(rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError)), {
10765
10909
  handoffDocument: handoffText,
10766
10910
  });
10767
10911
  }
@@ -10769,7 +10913,7 @@ export class AgentSession {
10769
10913
  // Genuine handoff-signal cancellation maps to a cancellation error.
10770
10914
  // Errors surfaced by the inner transaction (which already rolled back
10771
10915
  // and, for non-aborts, attached the generated document) pass through.
10772
- if (handoffSignal.aborted) {
10916
+ if (handoffSignal.aborted && !(error instanceof AggregateError)) {
10773
10917
  throw new Error("Handoff cancelled");
10774
10918
  }
10775
10919
  throw error;
@@ -12459,8 +12603,14 @@ export class AgentSession {
12459
12603
  break;
12460
12604
  }
12461
12605
 
12462
- const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
12463
- const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
12606
+ // Legacy parsed Retry-After is capped at retry.maxDelayMs (see
12607
+ // compactionRetryDelay); only managed fallback is uncapped.
12608
+ const delayMs = compactionRetryDelay(
12609
+ retrySettings.baseDelayMs,
12610
+ retrySettings.maxDelayMs,
12611
+ attempt,
12612
+ retryAfterMs,
12613
+ );
12464
12614
 
12465
12615
  // If retry delay is too long (>30s), try next candidate instead of waiting
12466
12616
  const maxAcceptableDelayMs = 30_000;
@@ -12653,7 +12803,7 @@ export class AgentSession {
12653
12803
  );
12654
12804
  }
12655
12805
 
12656
- #alibabaTokenPlanCanonicalTimeoutForApi(api: string): string | undefined {
12806
+ #alibabaTokenPlanTimeoutMessagesForApi(api: string): ReadonlySet<string> | undefined {
12657
12807
  if (api === "openai-responses" || api === "openai-completions") {
12658
12808
  return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
12659
12809
  }
@@ -12662,18 +12812,23 @@ export class AgentSession {
12662
12812
 
12663
12813
  #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
12664
12814
  if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12665
- const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(message.api);
12666
- return canonicalMessage !== undefined && message.errorMessage === canonicalMessage;
12815
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
12816
+ return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
12667
12817
  }
12668
12818
 
12669
12819
  #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12670
12820
  if (candidate.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
12671
- const canonicalMessage = this.#alibabaTokenPlanCanonicalTimeoutForApi(candidate.api);
12672
- if (canonicalMessage === undefined) return false;
12673
- return (
12674
- errorMessage === `Summarization failed: ${canonicalMessage}` ||
12675
- errorMessage === `Turn prefix summarization failed: ${canonicalMessage}`
12676
- );
12821
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(candidate.api);
12822
+ if (!timeoutMessages) return false;
12823
+ for (const timeoutMessage of timeoutMessages) {
12824
+ if (
12825
+ errorMessage === `Summarization failed: ${timeoutMessage}` ||
12826
+ errorMessage === `Turn prefix summarization failed: ${timeoutMessage}`
12827
+ ) {
12828
+ return true;
12829
+ }
12830
+ }
12831
+ return false;
12677
12832
  }
12678
12833
 
12679
12834
  #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
@@ -13025,7 +13180,7 @@ export class AgentSession {
13025
13180
  }
13026
13181
  if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
13027
13182
  // The managed transport discarded this attempt before session policy saw it.
13028
- // Remove its provisional controller charge and surface the original message.
13183
+ // Remove its provisional charge without changing sticky fallback selection.
13029
13184
  this.#defaultFallbackChain().discardStartedAttempt();
13030
13185
  return {
13031
13186
  type: "terminal",
@@ -14557,6 +14712,12 @@ export class AgentSession {
14557
14712
 
14558
14713
  try {
14559
14714
  await this.sessionManager.setSessionFile(sessionPath);
14715
+ // The successor identity is already rotated in the manager but not yet
14716
+ // published; gate its local:// root before publication so the agent,
14717
+ // workflow-gate emitter, and hooks cannot resolve against an ungated
14718
+ // root. The manager-rotation window itself is tracked in #3138
14719
+ // (#2797 / #2925).
14720
+ if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14560
14721
  this.#syncAgentSessionId();
14561
14722
  this.#rekeyHindsightMemoryForCurrentSessionId();
14562
14723
 
@@ -14633,13 +14794,8 @@ export class AgentSession {
14633
14794
  await this.sessionManager.ensureOnDisk();
14634
14795
 
14635
14796
  if (switchingToDifferentSession) {
14636
- // Interactive /resume (and any other switchSession identity change) must
14637
- // await verified legacy local:// migration for the *new* session before
14638
- // post-commit session_switch / local path resolution. createAgentSession
14639
- // already does this at cold start (#2797); switchSession previously omitted
14640
- // it, so resolving local:// after /resume could fail-closed with
14641
- // "legacy migration must complete before path resolution" (#2925).
14642
- await initializeLocalRoot(this.#localProtocolOptions());
14797
+ // The local:// migration gate for this successor already ran above,
14798
+ // before the identity was published (#2797 / #2925).
14643
14799
  this.#resetHindsightConversationTrackingIfHindsight();
14644
14800
  this.#resetIrcRosterDeliveryState();
14645
14801
  }
@@ -14758,19 +14914,21 @@ export class AgentSession {
14758
14914
  skipConversationRestore = result?.skipConversationRestore ?? false;
14759
14915
  }
14760
14916
 
14761
- // Clear pending messages (bound to old session state)
14917
+ // Flush pending writes before preparing the successor.
14918
+ await this.sessionManager.flush();
14919
+ const prepared = selectedEntry.parentId
14920
+ ? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
14921
+ : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
14922
+ try {
14923
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
14924
+ this.sessionManager.commitPreparedNewSession(prepared);
14925
+ } catch (error) {
14926
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
14927
+ }
14762
14928
  this.#pendingNextTurnMessages = [];
14763
14929
  this.#scheduledHiddenNextTurnGeneration = undefined;
14764
-
14765
- // Flush pending writes before branching
14766
- await this.sessionManager.flush();
14767
14930
  this.#cancelOwnAsyncJobs();
14768
14931
 
14769
- if (!selectedEntry.parentId) {
14770
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
14771
- } else {
14772
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
14773
- }
14774
14932
  this.#syncTodoPhasesFromBranch();
14775
14933
  this.#syncAgentSessionId();
14776
14934
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);