@gajae-code/coding-agent 0.11.10 → 0.12.0

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 (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -18,6 +18,7 @@ import * as crypto from "node:crypto";
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import { scheduler } from "node:timers/promises";
21
+ import * as util from "node:util";
21
22
  import {
22
23
  type AfterToolCallContext,
23
24
  type AfterToolCallResult,
@@ -172,6 +173,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a
172
173
  import { reset as resetCapabilities } from "../capability";
173
174
  import type { Rule } from "../capability/rule";
174
175
  import type { CasReceipt } from "../config/atomic-yaml-patch";
176
+ import { activateModelProfile } from "../config/model-profile-activation";
175
177
  import {
176
178
  GJC_MODEL_ASSIGNMENT_TARGETS,
177
179
  isAuthenticated,
@@ -233,7 +235,7 @@ import type {
233
235
  TurnEndEvent,
234
236
  TurnStartEvent,
235
237
  } from "../extensibility/extensions";
236
- import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
238
+ import type { CompactOptions, ContextUsage, ExtensionTranscriptEntry } from "../extensibility/extensions/types";
237
239
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
238
240
  import {
239
241
  type LoadedSubskillActivation,
@@ -243,10 +245,22 @@ import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injec
243
245
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
244
246
  import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
245
247
  import type { HookCommandContext } from "../extensibility/hooks/types";
246
- import { buildSkillPromptMessage, type Skill, type SkillWarning } from "../extensibility/skills";
248
+ import type { SessionSwitchEvent } from "../extensibility/shared-events";
249
+ import {
250
+ buildSkillPromptMessage,
251
+ getSkillSlashCommandName,
252
+ parseSkillInvocations,
253
+ type Skill,
254
+ type SkillWarning,
255
+ } from "../extensibility/skills";
247
256
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
248
257
  import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
249
258
  import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
259
+ import {
260
+ isMemoryGuardClaimsLease,
261
+ isMemoryGuardClaimsLeaseForStateDir,
262
+ type MemoryGuardClaimsLease,
263
+ } from "../gjc-runtime/memory-guard-owner-claims";
250
264
  import {
251
265
  assertNonEmptyGjcSessionId,
252
266
  modeStatePath as sessionModeStatePath,
@@ -341,6 +355,7 @@ import {
341
355
  import {
342
356
  type ConfiguredFallbackChain,
343
357
  cappedExponentialWithFullJitter,
358
+ compactionRetryDelay,
344
359
  effectiveFallbackDelay,
345
360
  FallbackChainController,
346
361
  } from "./fallback-chain-controller";
@@ -360,6 +375,7 @@ import {
360
375
  prepareContributionPrep,
361
376
  } from "./contribution-prep";
362
377
  import { pruneStaleFileMentions } from "./file-mention-pruning";
378
+ import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
363
379
  import {
364
380
  type BashExecutionMessage,
365
381
  type CompactionSummaryMessage,
@@ -379,6 +395,7 @@ import type {
379
395
  CompactionEntry,
380
396
  DefaultModelSelectionStage,
381
397
  NewSessionOptions,
398
+ PreparedNewSession,
382
399
  RecoveryHydrationContext,
383
400
  RecoveryHydrationPromotionFence,
384
401
  SessionContext,
@@ -604,6 +621,24 @@ export interface AgentSessionConfig {
604
621
  providerCacheSessionId?: string;
605
622
  }
606
623
 
624
+ export interface AgentSessionMemoryGuardRestoreInput
625
+ extends Omit<AgentSessionConfig, "sessionManager" | "recoveryHydrationContext"> {
626
+ staged: Extract<MemoryGuardRestoreResult, { kind: "staged" }>;
627
+ claimsLease: MemoryGuardClaimsLease;
628
+ claimsStateDir: string;
629
+ }
630
+
631
+ export interface AgentMemoryGuardPromotionFence extends RecoveryHydrationPromotionFence {
632
+ readonly claimsLease: MemoryGuardClaimsLease;
633
+ }
634
+
635
+ export type AgentMemoryGuardRestoreResult =
636
+ | { kind: "staged"; session: AgentSession; promotionFence: AgentMemoryGuardPromotionFence }
637
+ | {
638
+ kind: "blocked";
639
+ reason: "transcript-mismatch" | "hydration-context-mismatch" | "claim-mismatch" | "agent-messages-mismatch";
640
+ };
641
+
607
642
  type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
608
643
 
609
644
  type AutoCompactionTerminalStatus =
@@ -633,8 +668,16 @@ export interface PromptOptions {
633
668
  /**
634
669
  * Invoked after all prompt preflight checks pass and immediately before agent execution begins.
635
670
  * Cancellation before this callback rejects the prompt.
671
+ * Prefer `onPreflightAcceptCommit` for async durable acceptance (#3031/#3032).
636
672
  */
637
673
  onPreflightAccepted?: () => void;
674
+ /**
675
+ * Awaitable durable-accept fence. Called after preflight and before queue mutation
676
+ * or `#promptAgentWithIdleRetry`. SDK bus installs a closure that fsyncs acceptance.
677
+ */
678
+ onPreflightAcceptCommit?: () => void | Promise<void>;
679
+ /** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
680
+ onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
638
681
  }
639
682
 
640
683
  function promptPreflightCancelledError(): Error {
@@ -798,8 +841,14 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
798
841
 
799
842
  const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
800
843
  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",
844
+ "openai-responses": new Set([
845
+ "Provider stream timed out while waiting for the first event",
846
+ "OpenAI responses stream timed out while waiting for the first event",
847
+ ]),
848
+ "openai-completions": new Set([
849
+ "Provider stream timed out while waiting for the first event",
850
+ "OpenAI completions stream timed out while waiting for the first event",
851
+ ]),
803
852
  } as const;
804
853
 
805
854
  function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
@@ -1043,6 +1092,22 @@ function createHandoffContext(document: string): string {
1043
1092
  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
1093
  }
1045
1094
 
1095
+ async function discardPreparedNewSessionAfterFailure(
1096
+ sessionManager: SessionManager,
1097
+ prepared: PreparedNewSession,
1098
+ transitionError: unknown,
1099
+ ): Promise<unknown> {
1100
+ try {
1101
+ await sessionManager.discardPreparedNewSession(prepared);
1102
+ } catch (cleanupError) {
1103
+ throw new AggregateError(
1104
+ [transitionError, cleanupError],
1105
+ "Session transition and staged successor cleanup both failed.",
1106
+ );
1107
+ }
1108
+ return transitionError;
1109
+ }
1110
+
1046
1111
  // ============================================================================
1047
1112
  // ACP Permission Gate
1048
1113
  // ============================================================================
@@ -1507,6 +1572,47 @@ export class AgentSession {
1507
1572
  #sessionAdmissionClosed = false;
1508
1573
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
1509
1574
  #defaultModelSelectionMutationRevision = 0;
1575
+ #thinkingLevelMutationRevision = 0;
1576
+ #thinkingVisibilityMutationRevision = 0;
1577
+ #thinkingLevelLiveMutationRevision = 0;
1578
+ #thinkingVisibilityLiveMutationRevision = 0;
1579
+ #reasoningControlContextGeneration = 0;
1580
+ #pendingThinkingLevelControlSuccess:
1581
+ | {
1582
+ level: ThinkingLevel;
1583
+ mutationRevision: number;
1584
+ sessionId: string;
1585
+ model: Model | undefined;
1586
+ contextGeneration: number;
1587
+ }
1588
+ | undefined;
1589
+ #pendingThinkingVisibilityControlSuccess:
1590
+ | {
1591
+ visibility: "visible" | "hidden";
1592
+ mutationRevision: number;
1593
+ sessionId: string;
1594
+ model: Model | undefined;
1595
+ contextGeneration: number;
1596
+ }
1597
+ | undefined;
1598
+ #pendingThinkingLevelControlFailure:
1599
+ | {
1600
+ mutationRevision: number;
1601
+ liveMutationRevision: number;
1602
+ sessionId: string;
1603
+ model: Model | undefined;
1604
+ contextGeneration: number;
1605
+ }
1606
+ | undefined;
1607
+ #pendingThinkingVisibilityControlFailure:
1608
+ | {
1609
+ mutationRevision: number;
1610
+ liveMutationRevision: number;
1611
+ sessionId: string;
1612
+ model: Model | undefined;
1613
+ contextGeneration: number;
1614
+ }
1615
+ | undefined;
1510
1616
  #promptTemplates: PromptTemplate[];
1511
1617
  #slashCommands: FileSlashCommand[];
1512
1618
 
@@ -1758,6 +1864,7 @@ export class AgentSession {
1758
1864
  #constructorMCPToolSelection: string[] | undefined;
1759
1865
  #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
1760
1866
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
1867
+ #memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
1761
1868
 
1762
1869
  // TTSR manager for time-traveling stream rules
1763
1870
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -2213,6 +2320,7 @@ export class AgentSession {
2213
2320
  this.#unregisterResourceGc = registerResourceGcSession({
2214
2321
  sessionId: resourceGcSessionId,
2215
2322
  settings: this.settings,
2323
+ cwd: () => this.sessionManager.getCwd(),
2216
2324
  });
2217
2325
  }
2218
2326
  this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
@@ -2640,9 +2748,15 @@ export class AgentSession {
2640
2748
  this.#defaultFallbackController = scope.fallbackController;
2641
2749
  const previousEditMode = this.#resolveActiveEditMode();
2642
2750
  if (scope.model) {
2643
- this.agent.setModel(scope.model);
2751
+ this.#setAgentModelWithReasoningContext(scope.model);
2644
2752
  this.#syncAppendOnlyContext(scope.model);
2645
2753
  }
2754
+ this.#thinkingLevelMutationRevision++;
2755
+ this.#thinkingLevelLiveMutationRevision++;
2756
+ this.#pendingThinkingLevelControlSuccess = undefined;
2757
+ this.#pendingThinkingLevelControlFailure = undefined;
2758
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
2759
+ this.#pendingThinkingVisibilityControlFailure = undefined;
2646
2760
  this.#thinkingLevel = scope.thinkingLevel;
2647
2761
  this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
2648
2762
  void this.#syncEditToolModeAfterModelChange(previousEditMode);
@@ -4008,8 +4122,21 @@ export class AgentSession {
4008
4122
  return;
4009
4123
  }
4010
4124
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4125
+ const startsQueuedSuccessor =
4126
+ this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4011
4127
  try {
4012
- await this.agent.continue(this.#managedFallbackPromptOptions());
4128
+ await this.agent.continue({
4129
+ ...this.#managedFallbackPromptOptions(),
4130
+ // Reset only after continue() has claimed the queued turn. Skipped or stale
4131
+ // continuations retain predecessor accounting, and resetAttemptBudget keeps
4132
+ // the sticky fallback cursor unchanged.
4133
+ onRunAccepted: startsQueuedSuccessor
4134
+ ? () => {
4135
+ this.#defaultFallbackChain().resetAttemptBudget();
4136
+ this.#overflowMaintenanceAttempts = 0;
4137
+ }
4138
+ : undefined,
4139
+ });
4013
4140
  } catch (error) {
4014
4141
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4015
4142
  throw error;
@@ -4612,13 +4739,13 @@ export class AgentSession {
4612
4739
  return resolveToCwd(normalized, this.sessionManager.getCwd());
4613
4740
  }
4614
4741
 
4615
- #localProtocolOptions(): LocalProtocolOptions {
4742
+ #localProtocolOptions(prepared?: PreparedNewSession): LocalProtocolOptions {
4616
4743
  return {
4617
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
4744
+ getArtifactsDir: () => prepared?.artifactsDir ?? this.sessionManager.getArtifactsDir(),
4618
4745
  isManagedDestination: () => this.sessionManager.isManagedDestination(),
4619
- // Required for verified legacy local:// migration on managed sessions (#2797 / #2925).
4620
- getManagedLegacyLocalMigrationSource: () => this.sessionManager.getManagedLegacyLocalMigrationSource(),
4621
- getSessionId: () => this.sessionManager.getSessionId(),
4746
+ getManagedLegacyLocalMigrationSource: () =>
4747
+ prepared?.managedLegacyLocalMigrationSource ?? this.sessionManager.getManagedLegacyLocalMigrationSource(),
4748
+ getSessionId: () => prepared?.sessionId ?? this.sessionManager.getSessionId(),
4622
4749
  };
4623
4750
  }
4624
4751
 
@@ -5073,6 +5200,7 @@ export class AgentSession {
5073
5200
  * such event.
5074
5201
  */
5075
5202
  #syncAgentSessionId(sessionId?: string): void {
5203
+ this.#reasoningControlContextGeneration++;
5076
5204
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
5077
5205
  this.agent.sessionId = sid;
5078
5206
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
@@ -6397,17 +6525,65 @@ export class AgentSession {
6397
6525
  return this.#promptGeneration;
6398
6526
  }
6399
6527
 
6528
+ static async restoreFromMemoryGuardCheckpoint(
6529
+ input: AgentSessionMemoryGuardRestoreInput,
6530
+ ): Promise<AgentMemoryGuardRestoreResult> {
6531
+ const { staged, claimsLease, claimsStateDir, ...config } = input;
6532
+ if (
6533
+ staged.manager.getSessionId() !== staged.transcriptIdentity.sessionId ||
6534
+ staged.hydrationContext.identity.sessionId !== staged.transcriptIdentity.sessionId
6535
+ ) {
6536
+ await staged.cleanup();
6537
+ return { kind: "blocked", reason: "transcript-mismatch" };
6538
+ }
6539
+ if (
6540
+ !isMemoryGuardClaimsLeaseForStateDir(claimsLease, claimsStateDir) ||
6541
+ claimsLease.owner.sessionId !== staged.transcriptIdentity.sessionId
6542
+ ) {
6543
+ await staged.cleanup();
6544
+ return { kind: "blocked", reason: "claim-mismatch" };
6545
+ }
6546
+ const checkpointMessages = staged.manager.buildSessionContext().messages;
6547
+ if (!util.isDeepStrictEqual(config.agent.state.messages, checkpointMessages)) {
6548
+ await staged.cleanup();
6549
+ return { kind: "blocked", reason: "agent-messages-mismatch" };
6550
+ }
6551
+ const session = new AgentSession({
6552
+ ...config,
6553
+ sessionManager: staged.manager,
6554
+ recoveryHydrationContext: staged.hydrationContext,
6555
+ });
6556
+ session.#memoryGuardClaimsLease = claimsLease;
6557
+ if (session.recoveryHydrationContext !== staged.hydrationContext) {
6558
+ await session.dispose();
6559
+ await staged.cleanup();
6560
+ return { kind: "blocked", reason: "hydration-context-mismatch" };
6561
+ }
6562
+ return {
6563
+ kind: "staged",
6564
+ session,
6565
+ promotionFence: Object.freeze({ ownershipReady: true, claimsLease }),
6566
+ };
6567
+ }
6568
+
6400
6569
  /** The immutable recovery authority, present only before external ownership promotion. */
6401
6570
  get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
6402
6571
  return this.#recoveryHydrationContext;
6403
6572
  }
6404
6573
 
6405
6574
  /** Enables normal session mutations after the owner has published its durable fence and writer lease. */
6406
- async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: RecoveryHydrationPromotionFence): Promise<void> {
6575
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: AgentMemoryGuardPromotionFence): Promise<void> {
6407
6576
  const context = this.#recoveryHydrationContext;
6408
6577
  if (!context) throw new Error("Agent session is not awaiting recovery hydration promotion.");
6578
+ if (!isMemoryGuardClaimsLease(fence.claimsLease)) {
6579
+ throw new Error("Recovery hydration promotion requires a live memory-guard claims lease.");
6580
+ }
6581
+ if (this.#memoryGuardClaimsLease !== fence.claimsLease) {
6582
+ throw new Error("Recovery hydration promotion requires the acquired memory-guard claims lease.");
6583
+ }
6409
6584
  await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
6410
6585
  this.#recoveryHydrationContext = undefined;
6586
+ this.#memoryGuardClaimsLease = undefined;
6411
6587
  }
6412
6588
 
6413
6589
  /** Recovery hydration must not start a continuation before ownership promotion. */
@@ -6479,6 +6655,15 @@ export class AgentSession {
6479
6655
  return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6480
6656
  }
6481
6657
 
6658
+ /** Build display context from an unpublished successor without changing active session state. @internal */
6659
+ buildPreparedDisplaySessionContext(prepared: PreparedNewSession): SessionContext {
6660
+ const context = deobfuscateSessionContext(
6661
+ this.sessionManager.buildPreparedNewSessionContext(prepared),
6662
+ this.#obfuscator,
6663
+ );
6664
+ return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6665
+ }
6666
+
6482
6667
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
6483
6668
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
6484
6669
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -6586,9 +6771,11 @@ export class AgentSession {
6586
6771
  /** Live SDK configuration values exposed through the session query surface. */
6587
6772
  getSdkConfigItems(): Record<string, string> {
6588
6773
  const model = this.model;
6774
+ const modelPreset = this.getActiveModelProfile() ?? this.settings.get("modelProfile.default");
6589
6775
  return {
6590
6776
  mode: this.#planModeState?.enabled ? "plan" : "default",
6591
6777
  ...(model ? { model: `${model.provider}/${model.id}` } : {}),
6778
+ ...(modelPreset ? { modelPreset } : {}),
6592
6779
  thinking: this.#thinkingLevel ?? "off",
6593
6780
  steeringMode: this.steeringMode,
6594
6781
  followUpMode: this.followUpMode,
@@ -6607,6 +6794,7 @@ export class AgentSession {
6607
6794
  async invokeSkill(
6608
6795
  name: string,
6609
6796
  args = "",
6797
+ options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared">,
6610
6798
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6611
6799
  const skillName = name.trim();
6612
6800
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6639,7 +6827,13 @@ export class AgentSession {
6639
6827
  attribution: "user" as const,
6640
6828
  };
6641
6829
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
6642
- await this.promptCustomMessage(skillPromptMessage);
6830
+ options?.onSkillPrepared?.({
6831
+ name: skill.name,
6832
+ path: skill.filePath,
6833
+ lineCount: built.details.lineCount,
6834
+ cleanedArgs: activation.cleanedArgs || undefined,
6835
+ });
6836
+ await this.promptCustomMessage(skillPromptMessage, options);
6643
6837
  return {
6644
6838
  name: skill.name,
6645
6839
  path: skill.filePath,
@@ -6685,22 +6879,49 @@ export class AgentSession {
6685
6879
  }
6686
6880
  }
6687
6881
 
6688
- getTranscript(): Array<{ id: string; role: string; textSummary: string; ts: string; body: string }> {
6882
+ getTranscript(): ExtensionTranscriptEntry[] {
6689
6883
  return getEntriesForInternalRead(this.sessionManager).flatMap(entry => {
6690
6884
  if (entry.type !== "message") return [];
6691
- const message = entry.message as unknown as { role?: unknown; content?: unknown };
6885
+ const message = entry.message as unknown as {
6886
+ role?: unknown;
6887
+ content?: unknown;
6888
+ toolCallId?: unknown;
6889
+ toolName?: unknown;
6890
+ isError?: unknown;
6891
+ };
6892
+ const content: NonNullable<ExtensionTranscriptEntry["content"]> | undefined = Array.isArray(message.content)
6893
+ ? message.content.reduce<NonNullable<ExtensionTranscriptEntry["content"]>>((blocks, part) => {
6894
+ if (typeof part !== "object" || part === null || !("type" in part)) return blocks;
6895
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
6896
+ blocks.push({ type: "text", text: part.text });
6897
+ return blocks;
6898
+ }
6899
+ if (part.type === "thinking" && "thinking" in part && typeof part.thinking === "string") {
6900
+ blocks.push({ type: "thinking", thinking: part.thinking });
6901
+ return blocks;
6902
+ }
6903
+ if (
6904
+ part.type === "toolCall" &&
6905
+ "id" in part &&
6906
+ typeof part.id === "string" &&
6907
+ "name" in part &&
6908
+ typeof part.name === "string"
6909
+ ) {
6910
+ blocks.push({
6911
+ type: "toolCall",
6912
+ id: part.id,
6913
+ name: part.name,
6914
+ arguments: "arguments" in part ? part.arguments : {},
6915
+ });
6916
+ }
6917
+ return blocks;
6918
+ }, [])
6919
+ : undefined;
6692
6920
  const body =
6693
6921
  typeof message.content === "string"
6694
6922
  ? message.content
6695
- : Array.isArray(message.content)
6696
- ? message.content
6697
- .map(part =>
6698
- typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
6699
- ? part.text
6700
- : "",
6701
- )
6702
- .filter(Boolean)
6703
- .join("\n")
6923
+ : content
6924
+ ? content.flatMap(part => (part.type === "text" ? [part.text] : [])).join("\n")
6704
6925
  : "";
6705
6926
  return [
6706
6927
  {
@@ -6709,6 +6930,10 @@ export class AgentSession {
6709
6930
  textSummary: body.slice(0, 500),
6710
6931
  ts: entry.timestamp,
6711
6932
  body,
6933
+ ...(content ? { content } : {}),
6934
+ ...(typeof message.toolCallId === "string" ? { toolCallId: message.toolCallId } : {}),
6935
+ ...(typeof message.toolName === "string" ? { toolName: message.toolName } : {}),
6936
+ ...(typeof message.isError === "boolean" ? { isError: message.isError } : {}),
6712
6937
  },
6713
6938
  ];
6714
6939
  });
@@ -6746,8 +6971,7 @@ export class AgentSession {
6746
6971
  return getAskAnswerSourceFromRegistry(this.sessionId);
6747
6972
  }
6748
6973
 
6749
- #constructWorkflowGateEmitter(): WorkflowGateEmitter {
6750
- const sessionId = this.sessionManager.getSessionId();
6974
+ #constructWorkflowGateEmitter(sessionId = this.sessionManager.getSessionId()): WorkflowGateEmitter {
6751
6975
  assertNonEmptyGjcSessionId(sessionId, "AgentSession workflow-gate session");
6752
6976
  const gateStore = this.sessionManager.isPersisted()
6753
6977
  ? new FileGateStore(path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"))
@@ -7264,8 +7488,32 @@ export class AgentSession {
7264
7488
  * @throws Error if no model selected or no API key available (when not streaming)
7265
7489
  */
7266
7490
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7491
+ this.#assertRecoveryHydrationPromoted();
7267
7492
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7268
7493
 
7494
+ if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
7495
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
7496
+ const invocations = parseSkillInvocations(text, skillCommands);
7497
+ if (invocations.length === 1) {
7498
+ const invocation = invocations[0];
7499
+ if (invocation) {
7500
+ await this.invokeSkill(
7501
+ invocation.skill.name,
7502
+ invocation.args,
7503
+ options?.onPreflightAccepted || options?.onPreflightAcceptCommit
7504
+ ? {
7505
+ ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
7506
+ ...(options.onPreflightAcceptCommit
7507
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
7508
+ : {}),
7509
+ }
7510
+ : undefined,
7511
+ );
7512
+ return;
7513
+ }
7514
+ }
7515
+ }
7516
+
7269
7517
  // Handle extension commands first (execute immediately, even during streaming)
7270
7518
  if (expandPromptTemplates && text.startsWith("/")) {
7271
7519
  const handled = await this.#tryExecuteExtensionCommand(text);
@@ -7313,7 +7561,8 @@ export class AgentSession {
7313
7561
  if (workflowIntentDiff) {
7314
7562
  this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
7315
7563
  }
7316
- options?.onPreflightAccepted?.();
7564
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
7565
+ else options?.onPreflightAccepted?.();
7317
7566
  return;
7318
7567
  }
7319
7568
 
@@ -7445,7 +7694,10 @@ export class AgentSession {
7445
7694
 
7446
7695
  async promptCustomMessage<T = unknown>(
7447
7696
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7448
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
7697
+ options?: Pick<
7698
+ PromptOptions,
7699
+ "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7700
+ >,
7449
7701
  ): Promise<void> {
7450
7702
  const textContent =
7451
7703
  typeof message.content === "string"
@@ -7507,7 +7759,10 @@ export class AgentSession {
7507
7759
  async #promptWithMessage(
7508
7760
  message: AgentMessage,
7509
7761
  expandedText: string,
7510
- options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted"> & {
7762
+ options?: Pick<
7763
+ PromptOptions,
7764
+ "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7765
+ > & {
7511
7766
  prependMessages?: AgentMessage[];
7512
7767
  skipPostPromptRecoveryWait?: boolean;
7513
7768
  predecessorAgentEndHold?: symbol;
@@ -7614,7 +7869,7 @@ export class AgentSession {
7614
7869
  // A newer abort/prompt cycle superseded this preflight. Callers awaiting
7615
7870
  // acceptance (onPreflightAccepted) must be told it never ran; direct
7616
7871
  // callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
7617
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7872
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7618
7873
  return;
7619
7874
  }
7620
7875
 
@@ -7728,7 +7983,7 @@ export class AgentSession {
7728
7983
  this.#resetInjectedContextSignatures();
7729
7984
  // Ack-waiting callers are told the preflight never ran; direct callers
7730
7985
  // (aborted after setup) resolve gracefully as before f24f46ff5.
7731
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7986
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7732
7987
  return;
7733
7988
  }
7734
7989
 
@@ -7741,7 +7996,8 @@ export class AgentSession {
7741
7996
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
7742
7997
  },
7743
7998
  };
7744
- options?.onPreflightAccepted?.();
7999
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8000
+ else options?.onPreflightAccepted?.();
7745
8001
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
7746
8002
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
7747
8003
  const terminalAssistant = this.#findLastAssistantMessage();
@@ -7760,7 +8016,12 @@ export class AgentSession {
7760
8016
  // Session identity changes historically cancel local setup silently. Only SDK
7761
8017
  // submissions provide an acceptance callback and require an explicit terminal
7762
8018
  // preflight failure for their remote request authority.
7763
- if (isPromptPreflightCancelledError(error) && !options?.onPreflightAccepted) return;
8019
+ if (
8020
+ isPromptPreflightCancelledError(error) &&
8021
+ !options?.onPreflightAccepted &&
8022
+ !options?.onPreflightAcceptCommit
8023
+ )
8024
+ return;
7764
8025
  throw error;
7765
8026
  } finally {
7766
8027
  this.#removeEphemeralCustomMessages();
@@ -7844,6 +8105,7 @@ export class AgentSession {
7844
8105
  return tool ? { safeSummary: tool.safeSummary, safeSummaryFields: tool.safeSummaryFields } : undefined;
7845
8106
  },
7846
8107
  cycleModel: () => this.cycleModel(),
8108
+ setModelProfile: name => this.activateModelProfileForControl(name),
7847
8109
  cycleThinkingLevel: () => this.cycleThinkingLevel(),
7848
8110
  setQueueMode: (kind, mode) => {
7849
8111
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -7860,7 +8122,7 @@ export class AgentSession {
7860
8122
  }
7861
8123
  return false;
7862
8124
  },
7863
- invokeSkill: (name, args) => this.invokeSkill(name, args),
8125
+ invokeSkill: (name, args, options) => this.invokeSkill(name, args, options),
7864
8126
  setPlanMode: on => this.setSdkPlanMode(on),
7865
8127
  operateGoal: (op, objective) => this.operateGoal(op, objective),
7866
8128
  getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -7871,6 +8133,7 @@ export class AgentSession {
7871
8133
  getJobs: () => undefined,
7872
8134
  sdkBindings: () => [
7873
8135
  "cycleModel",
8136
+ "setModelProfile",
7874
8137
  "cycleThinkingLevel",
7875
8138
  "setQueueMode",
7876
8139
  "getSkillState",
@@ -7974,6 +8237,7 @@ export class AgentSession {
7974
8237
  * Queue a steering message to interrupt the agent mid-run.
7975
8238
  */
7976
8239
  async steer(text: string, images?: ImageContent[]): Promise<void> {
8240
+ this.#assertRecoveryHydrationPromoted();
7977
8241
  if (text.startsWith("/")) {
7978
8242
  this.#throwIfExtensionCommand(text);
7979
8243
  }
@@ -7991,6 +8255,7 @@ export class AgentSession {
7991
8255
  images?: ImageContent[],
7992
8256
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
7993
8257
  ): Promise<void> {
8258
+ this.#assertRecoveryHydrationPromoted();
7994
8259
  if (text.startsWith("/")) {
7995
8260
  this.#throwIfExtensionCommand(text);
7996
8261
  }
@@ -8225,6 +8490,7 @@ export class AgentSession {
8225
8490
  followUpQueuePolicy?: "respect-mode" | "sequential";
8226
8491
  },
8227
8492
  ): Promise<void> {
8493
+ this.#assertRecoveryHydrationPromoted();
8228
8494
  const appMessage: CustomMessage<T> = {
8229
8495
  role: "custom",
8230
8496
  customType: message.customType,
@@ -8366,8 +8632,13 @@ export class AgentSession {
8366
8632
  */
8367
8633
  async sendUserMessage(
8368
8634
  content: string | (TextContent | ImageContent)[],
8369
- options?: { deliverAs?: "steer" | "followUp"; onPreflightAccepted?: () => void },
8635
+ options?: {
8636
+ deliverAs?: "steer" | "followUp";
8637
+ onPreflightAccepted?: () => void;
8638
+ onPreflightAcceptCommit?: () => void | Promise<void>;
8639
+ },
8370
8640
  ): Promise<void> {
8641
+ this.#assertRecoveryHydrationPromoted();
8371
8642
  // Normalize content to text string + optional images
8372
8643
  let text: string;
8373
8644
  let images: ImageContent[] | undefined;
@@ -8389,11 +8660,13 @@ export class AgentSession {
8389
8660
  }
8390
8661
 
8391
8662
  if (options?.deliverAs === "followUp") {
8663
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8392
8664
  await this.#queueFollowUp(text, images, { claimsGenuineUserIntent: true });
8393
8665
  options.onPreflightAccepted?.();
8394
8666
  return;
8395
8667
  }
8396
8668
  if (options?.deliverAs === "steer") {
8669
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8397
8670
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8398
8671
  options.onPreflightAccepted?.();
8399
8672
  return;
@@ -8405,6 +8678,7 @@ export class AgentSession {
8405
8678
  // in-flight compaction internally, and #queueSteer would otherwise park
8406
8679
  // the message in the steering queue with no turn to consume it.
8407
8680
  if (this.isStreaming) {
8681
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8408
8682
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8409
8683
  options?.onPreflightAccepted?.();
8410
8684
  return;
@@ -8415,6 +8689,7 @@ export class AgentSession {
8415
8689
  expandPromptTemplates: false,
8416
8690
  images,
8417
8691
  onPreflightAccepted: options?.onPreflightAccepted,
8692
+ onPreflightAcceptCommit: options?.onPreflightAcceptCommit,
8418
8693
  });
8419
8694
  }
8420
8695
 
@@ -9039,11 +9314,14 @@ export class AgentSession {
9039
9314
  this.#rebindProviderSessionState(new Map());
9040
9315
  this.agent.reset();
9041
9316
  if (!options?.drop) await this.sessionManager.flush();
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());
9317
+ const prepared = await this.sessionManager.prepareNewSession(options);
9318
+ try {
9319
+ // Last fallible gate while public getters still show the predecessor (#3138).
9320
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9321
+ this.sessionManager.commitPreparedNewSession(prepared);
9322
+ } catch (error) {
9323
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9324
+ }
9047
9325
  this.setTodoPhases([]);
9048
9326
  this.#syncAgentSessionId();
9049
9327
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9105,16 +9383,18 @@ export class AgentSession {
9105
9383
  if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) {
9106
9384
  throw new Error("Owned async jobs did not settle before session replacement.");
9107
9385
  }
9108
-
9386
+ const prepared = await this.sessionManager.prepareNewSession(options);
9387
+ try {
9388
+ // Last fallible gate while public getters still show the predecessor (#3138).
9389
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9390
+ this.sessionManager.commitPreparedNewSession(prepared);
9391
+ } catch (error) {
9392
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9393
+ }
9109
9394
  this.#disconnectFromAgent();
9110
9395
  this.#closeAllProviderSessions("new session");
9111
9396
  this.#rebindProviderSessionState(new Map());
9112
9397
  this.agent.reset();
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());
9118
9398
  this.setTodoPhases([]);
9119
9399
  this.#syncAgentSessionId();
9120
9400
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9149,6 +9429,12 @@ export class AgentSession {
9149
9429
  ): Promise<void> {
9150
9430
  this.#clearConstructorToolSelectionAuthority();
9151
9431
  const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
9432
+ this.#thinkingLevelMutationRevision++;
9433
+ this.#thinkingLevelLiveMutationRevision++;
9434
+ this.#pendingThinkingLevelControlSuccess = undefined;
9435
+ this.#pendingThinkingLevelControlFailure = undefined;
9436
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
9437
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9152
9438
  this.#thinkingLevel = inheritedThinkingLevel;
9153
9439
  this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
9154
9440
  this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
@@ -9157,10 +9443,17 @@ export class AgentSession {
9157
9443
  }
9158
9444
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
9159
9445
  if (nextDiscoverySessionToolNames) {
9160
- await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9161
- persistMCPSelection: false,
9162
- nextSelectedDiscoveredBuiltinToolNames: [],
9163
- });
9446
+ // Avoid a full system-prompt rebuild when the active set is unchanged; rebuild
9447
+ // can block on shared workspace-tree work across repeated /new transitions.
9448
+ const current = this.getActiveToolNames().slice().sort().join("\0");
9449
+ const next = [...new Set(nextDiscoverySessionToolNames.map(n => n.toLowerCase()))].sort().join("\0");
9450
+ if (current !== next) {
9451
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9452
+ persistMCPSelection: false,
9453
+ nextSelectedDiscoveredBuiltinToolNames: [],
9454
+ });
9455
+ } else {
9456
+ }
9164
9457
  }
9165
9458
  this.#todoReminderCount = 0;
9166
9459
  this.#planReferenceSent = false;
@@ -9173,6 +9466,7 @@ export class AgentSession {
9173
9466
  reason: "new",
9174
9467
  previousSessionFile,
9175
9468
  });
9469
+ } else {
9176
9470
  }
9177
9471
  }
9178
9472
 
@@ -9252,16 +9546,18 @@ export class AgentSession {
9252
9546
  // Flush current session to ensure all entries are written
9253
9547
  await this.sessionManager.flush();
9254
9548
 
9255
- // Fork the session (creates new session file with same entries)
9256
- const forkResult = await this.sessionManager.fork();
9257
- if (!forkResult) {
9549
+ // Prepare the copied successor and complete local:// readiness while all
9550
+ // public manager getters remain bound to the predecessor.
9551
+ const prepared = await this.sessionManager.prepareFork();
9552
+ if (!prepared) {
9258
9553
  return false;
9259
9554
  }
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());
9264
- // Update agent session ID
9555
+ try {
9556
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9557
+ this.sessionManager.commitPreparedNewSession(prepared);
9558
+ } catch (error) {
9559
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9560
+ }
9265
9561
  this.#syncAgentSessionId();
9266
9562
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
9267
9563
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -9350,6 +9646,17 @@ export class AgentSession {
9350
9646
  return this.#activeModelProfile;
9351
9647
  }
9352
9648
 
9649
+ /** Activate a complete model profile through a nonvisual session control. */
9650
+ async activateModelProfileForControl(profileName: string): Promise<boolean> {
9651
+ await activateModelProfile({
9652
+ session: this,
9653
+ modelRegistry: this.#modelRegistry,
9654
+ settings: this.settings,
9655
+ profileName,
9656
+ });
9657
+ return this.getActiveModelProfile() === profileName;
9658
+ }
9659
+
9353
9660
  /** Return the persisted configured fallback selectors for a model role. */
9354
9661
  getConfiguredModelChain(role: string): readonly string[] | undefined {
9355
9662
  return getSessionContextForInternalRead(this.sessionManager).configuredModelChains[role]?.entries;
@@ -9477,7 +9784,7 @@ export class AgentSession {
9477
9784
  const ownsScope = scope !== undefined && !suppliedScope;
9478
9785
  try {
9479
9786
  if (isTemporaryOperation) {
9480
- this.agent.setModel(model);
9787
+ this.#setAgentModelWithReasoningContext(model);
9481
9788
  this.#syncAppendOnlyContext(model);
9482
9789
  } else {
9483
9790
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
@@ -9562,6 +9869,12 @@ export class AgentSession {
9562
9869
  this.#clearActiveRetryFallback();
9563
9870
  this.#setModelWithProviderSessionReset(model);
9564
9871
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
9872
+ this.#thinkingLevelMutationRevision++;
9873
+ this.#thinkingLevelLiveMutationRevision++;
9874
+ this.#pendingThinkingLevelControlSuccess = undefined;
9875
+ this.#pendingThinkingLevelControlFailure = undefined;
9876
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
9877
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9565
9878
  this.#thinkingLevel = thinkingLevel;
9566
9879
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
9567
9880
  if (thinkingLevelChanged) {
@@ -9870,7 +10183,19 @@ export class AgentSession {
9870
10183
  * Set thinking level.
9871
10184
  * Saves the effective metadata-clamped level to the session, and to settings when requested.
9872
10185
  */
10186
+ #assertDurableSettingsWritable(): void {
10187
+ if (this.settings.canWriteDurableConfig()) return;
10188
+ throw new Error(
10189
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
10190
+ );
10191
+ }
10192
+
9873
10193
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
10194
+ if (persist) this.#assertDurableSettingsWritable();
10195
+ this.#thinkingLevelMutationRevision++;
10196
+ this.#thinkingLevelLiveMutationRevision++;
10197
+ this.#pendingThinkingLevelControlSuccess = undefined;
10198
+ this.#pendingThinkingLevelControlFailure = undefined;
9874
10199
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
9875
10200
  const isChanging = effectiveLevel !== this.#thinkingLevel;
9876
10201
 
@@ -9890,11 +10215,10 @@ export class AgentSession {
9890
10215
  }
9891
10216
 
9892
10217
  /**
9893
- * Set thinking level from a control surface. Global changes are durable or rolled back.
10218
+ * Set thinking level from a control surface. Global changes commit before affecting live state.
9894
10219
  */
9895
10220
  async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
9896
10221
  const previousThinkingLevel = this.thinkingLevel;
9897
- const previousScope = this.getThinkingScopeForControl();
9898
10222
  if (!persist) {
9899
10223
  this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
9900
10224
  if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
@@ -9903,26 +10227,99 @@ export class AgentSession {
9903
10227
  return;
9904
10228
  }
9905
10229
 
9906
- const previousDefaultThinkingLevel = this.settings.getGlobal("defaultThinkingLevel");
9907
- if (level === ThinkingLevel.Inherit) {
9908
- const defaultLevel = getDefault("defaultThinkingLevel");
9909
- this.settings.set("defaultThinkingLevel", defaultLevel);
9910
- this.setThinkingLevel(this.#getInheritedThinkingLevel());
9911
- } else {
9912
- this.setThinkingLevel(level, true);
9913
- }
10230
+ this.#assertDurableSettingsWritable();
10231
+ const effectiveLevel = resolveThinkingLevelForModel(
10232
+ this.model,
10233
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level,
10234
+ );
10235
+ const persistedLevel = level === ThinkingLevel.Inherit ? getDefault("defaultThinkingLevel") : effectiveLevel;
10236
+ const mutationRevision = ++this.#thinkingLevelMutationRevision;
10237
+ const expectedLiveMutationRevision = this.#thinkingLevelLiveMutationRevision;
10238
+ const expectedSessionId = this.sessionManager.getSessionId();
10239
+ const expectedModel = this.model;
10240
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
9914
10241
  try {
9915
- await this.settings.flushOrThrow();
9916
- this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10242
+ await this.settings.commitAtomicBatch([{ path: "defaultThinkingLevel", op: "set", value: persistedLevel }]);
9917
10243
  } catch {
9918
- this.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel ?? getDefault("defaultThinkingLevel"));
9919
- this.setThinkingLevel(previousThinkingLevel);
9920
- this.sessionManager.appendThinkingLevelChange(
9921
- previousScope === "global config" ? ThinkingLevel.Inherit : previousThinkingLevel,
9922
- );
9923
- await this.settings.flushOrThrow().catch(() => {});
10244
+ if (
10245
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10246
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10247
+ this.sessionManager.getSessionId() === expectedSessionId &&
10248
+ this.model === expectedModel
10249
+ ) {
10250
+ const pending = this.#pendingThinkingLevelControlSuccess;
10251
+ this.#pendingThinkingLevelControlSuccess = undefined;
10252
+ if (
10253
+ pending &&
10254
+ pending.contextGeneration === expectedContextGeneration &&
10255
+ this.sessionManager.getSessionId() === pending.sessionId &&
10256
+ this.model === pending.model
10257
+ ) {
10258
+ this.setThinkingLevel(
10259
+ pending.level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : pending.level,
10260
+ );
10261
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10262
+ } else {
10263
+ this.#pendingThinkingLevelControlFailure = {
10264
+ mutationRevision,
10265
+ liveMutationRevision: expectedLiveMutationRevision,
10266
+ sessionId: expectedSessionId,
10267
+ model: expectedModel,
10268
+ contextGeneration: expectedContextGeneration,
10269
+ };
10270
+ }
10271
+ }
9924
10272
  throw new Error("Unable to persist reasoning settings.");
9925
10273
  }
10274
+
10275
+ if (
10276
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10277
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision
10278
+ ) {
10279
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
10280
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10281
+ return;
10282
+ }
10283
+ if (
10284
+ mutationRevision !== this.#thinkingLevelMutationRevision ||
10285
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10286
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10287
+ this.model !== expectedModel
10288
+ ) {
10289
+ if (
10290
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision &&
10291
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10292
+ this.sessionManager.getSessionId() === expectedSessionId &&
10293
+ this.model === expectedModel &&
10294
+ (this.#pendingThinkingLevelControlSuccess?.mutationRevision ?? -1) < mutationRevision
10295
+ ) {
10296
+ this.#pendingThinkingLevelControlSuccess = {
10297
+ level,
10298
+ mutationRevision,
10299
+ sessionId: expectedSessionId,
10300
+ model: expectedModel,
10301
+ contextGeneration: expectedContextGeneration,
10302
+ };
10303
+ const failure = this.#pendingThinkingLevelControlFailure;
10304
+ if (
10305
+ failure &&
10306
+ failure.mutationRevision === this.#thinkingLevelMutationRevision &&
10307
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10308
+ failure.sessionId === expectedSessionId &&
10309
+ failure.model === expectedModel &&
10310
+ failure.contextGeneration === expectedContextGeneration
10311
+ ) {
10312
+ this.#pendingThinkingLevelControlFailure = undefined;
10313
+ this.setThinkingLevel(
10314
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel,
10315
+ );
10316
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10317
+ }
10318
+ }
10319
+ return;
10320
+ }
10321
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel);
10322
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
9926
10323
  }
9927
10324
 
9928
10325
  getThinkingScopeForControl(): "session" | "global config" {
@@ -9938,6 +10335,11 @@ export class AgentSession {
9938
10335
  }
9939
10336
 
9940
10337
  setThinkingVisibility(visibility: "visible" | "hidden", persist: boolean = false): void {
10338
+ if (persist) this.#assertDurableSettingsWritable();
10339
+ this.#thinkingVisibilityMutationRevision++;
10340
+ this.#thinkingVisibilityLiveMutationRevision++;
10341
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10342
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9941
10343
  this.agent.hideThinkingSummary = visibility === "hidden";
9942
10344
  if (persist) {
9943
10345
  this.settings.set("hideThinkingBlock", visibility === "hidden");
@@ -9945,7 +10347,7 @@ export class AgentSession {
9945
10347
  }
9946
10348
 
9947
10349
  /**
9948
- * Set thinking visibility from a control surface. Global changes are durable or rolled back.
10350
+ * Set thinking visibility from a control surface. Global changes commit before affecting live state.
9949
10351
  */
9950
10352
  async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
9951
10353
  if (!persist) {
@@ -9953,17 +10355,88 @@ export class AgentSession {
9953
10355
  return;
9954
10356
  }
9955
10357
 
9956
- const previousVisibility = this.getThinkingVisibility();
9957
- const previousHideThinkingBlock = this.settings.getGlobal("hideThinkingBlock");
9958
- this.setThinkingVisibility(visibility, true);
10358
+ this.#assertDurableSettingsWritable();
10359
+ const mutationRevision = ++this.#thinkingVisibilityMutationRevision;
10360
+ const expectedLiveMutationRevision = this.#thinkingVisibilityLiveMutationRevision;
10361
+ const expectedSessionId = this.sessionManager.getSessionId();
10362
+ const expectedModel = this.model;
10363
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
9959
10364
  try {
9960
- await this.settings.flushOrThrow();
10365
+ await this.settings.commitAtomicBatch([
10366
+ { path: "hideThinkingBlock", op: "set", value: visibility === "hidden" },
10367
+ ]);
9961
10368
  } catch {
9962
- this.settings.set("hideThinkingBlock", previousHideThinkingBlock ?? getDefault("hideThinkingBlock"));
9963
- this.setThinkingVisibility(previousVisibility);
9964
- await this.settings.flushOrThrow().catch(() => {});
10369
+ if (
10370
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10371
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10372
+ this.sessionManager.getSessionId() === expectedSessionId &&
10373
+ this.model === expectedModel
10374
+ ) {
10375
+ const pending = this.#pendingThinkingVisibilityControlSuccess;
10376
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10377
+ if (
10378
+ pending &&
10379
+ pending.contextGeneration === expectedContextGeneration &&
10380
+ this.sessionManager.getSessionId() === pending.sessionId &&
10381
+ this.model === pending.model
10382
+ ) {
10383
+ this.setThinkingVisibility(pending.visibility);
10384
+ } else {
10385
+ this.#pendingThinkingVisibilityControlFailure = {
10386
+ mutationRevision,
10387
+ liveMutationRevision: expectedLiveMutationRevision,
10388
+ sessionId: expectedSessionId,
10389
+ model: expectedModel,
10390
+ contextGeneration: expectedContextGeneration,
10391
+ };
10392
+ }
10393
+ }
9965
10394
  throw new Error("Unable to persist reasoning settings.");
9966
10395
  }
10396
+ if (
10397
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10398
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision
10399
+ ) {
10400
+ this.setThinkingVisibility(visibility);
10401
+ return;
10402
+ }
10403
+
10404
+ if (
10405
+ mutationRevision !== this.#thinkingVisibilityMutationRevision ||
10406
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10407
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10408
+ this.model !== expectedModel
10409
+ ) {
10410
+ if (
10411
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision &&
10412
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10413
+ this.sessionManager.getSessionId() === expectedSessionId &&
10414
+ this.model === expectedModel &&
10415
+ (this.#pendingThinkingVisibilityControlSuccess?.mutationRevision ?? -1) < mutationRevision
10416
+ ) {
10417
+ this.#pendingThinkingVisibilityControlSuccess = {
10418
+ visibility,
10419
+ mutationRevision,
10420
+ sessionId: expectedSessionId,
10421
+ model: expectedModel,
10422
+ contextGeneration: expectedContextGeneration,
10423
+ };
10424
+ const failure = this.#pendingThinkingVisibilityControlFailure;
10425
+ if (
10426
+ failure &&
10427
+ failure.mutationRevision === this.#thinkingVisibilityMutationRevision &&
10428
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10429
+ failure.sessionId === expectedSessionId &&
10430
+ failure.model === expectedModel &&
10431
+ failure.contextGeneration === expectedContextGeneration
10432
+ ) {
10433
+ this.#pendingThinkingVisibilityControlFailure = undefined;
10434
+ this.setThinkingVisibility(visibility);
10435
+ }
10436
+ }
10437
+ return;
10438
+ }
10439
+ this.setThinkingVisibility(visibility);
9967
10440
  }
9968
10441
 
9969
10442
  /**
@@ -10140,6 +10613,7 @@ export class AgentSession {
10140
10613
  * Saves to settings.
10141
10614
  */
10142
10615
  setSteeringMode(mode: "all" | "one-at-a-time"): void {
10616
+ this.#assertDurableSettingsWritable();
10143
10617
  this.agent.setSteeringMode(mode);
10144
10618
  this.settings.set("steeringMode", mode);
10145
10619
  }
@@ -10149,6 +10623,7 @@ export class AgentSession {
10149
10623
  * Saves to settings.
10150
10624
  */
10151
10625
  setFollowUpMode(mode: "all" | "one-at-a-time"): void {
10626
+ this.#assertDurableSettingsWritable();
10152
10627
  this.agent.setFollowUpMode(mode);
10153
10628
  this.settings.set("followUpMode", mode);
10154
10629
  }
@@ -10158,6 +10633,7 @@ export class AgentSession {
10158
10633
  * Saves to settings.
10159
10634
  */
10160
10635
  setInterruptMode(mode: "immediate" | "wait"): void {
10636
+ this.#assertDurableSettingsWritable();
10161
10637
  this.agent.setInterruptMode(mode);
10162
10638
  this.settings.set("interruptMode", mode);
10163
10639
  }
@@ -10652,26 +11128,36 @@ export class AgentSession {
10652
11128
  // user work that agent.reset() would otherwise clear.
10653
11129
  const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
10654
11130
  const rollbackAgentFollowUpQueue = this.agent.snapshotFollowUp();
10655
- let successorSessionFile: string | undefined;
10656
11131
  let savedPath: string | undefined;
10657
11132
  let committed = false;
10658
- // Suspend (do not destroy) the workflow-gate emitter so it can be fenced on
10659
- // commit or resumed on rollback. Suspension runs inside the transaction so a
10660
- // listener fault during suspension is rolled back like any other prepare step.
10661
- let suspendedWorkflowGateEmitter: WorkflowGateEmitter | undefined;
11133
+ let prepared: PreparedNewSession | undefined;
10662
11134
  try {
10663
- suspendedWorkflowGateEmitter = this.#suspendWorkflowGateEmitter(rollbackSessionState.sessionId);
10664
- // --- Prepare (reversible): build and persist the successor session
10665
- // without touching predecessor authority. No irreversible predecessor
10666
- // teardown or identity publication happens until the commit boundary.
10667
- await this.sessionManager.newSession(
11135
+ // Prepare successor entries, persistence, display state, and gate
11136
+ // construction without publishing manager identity. Managed local-root
11137
+ // readiness is the last fallible step before the sole commit (#3138).
11138
+ prepared = await this.sessionManager.prepareNewSession(
10668
11139
  previousSessionFile ? { parentSession: previousSessionFile } : undefined,
10669
11140
  );
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());
11141
+ if (model) this.sessionManager.appendPreparedModelChange(prepared, `${model.provider}/${model.id}`);
11142
+ this.sessionManager.appendPreparedThinkingLevelChange(prepared, this.thinkingLevel);
11143
+ this.sessionManager.appendPreparedServiceTierChange(prepared, this.serviceTier ?? null);
11144
+ this.sessionManager.appendPreparedCustomMessageEntry(
11145
+ prepared,
11146
+ "handoff",
11147
+ createHandoffContext(handoffText),
11148
+ true,
11149
+ undefined,
11150
+ "agent",
11151
+ );
11152
+ await this.sessionManager.ensurePreparedNewSessionOnDisk(prepared);
11153
+ const sessionContext = this.buildPreparedDisplaySessionContext(prepared);
11154
+ const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
11155
+ // Last fallible action: verified local:// readiness from immutable staged options.
11156
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
11157
+
11158
+ // --- Commit boundary: synchronous adoption is the sole identity publication.
11159
+ this.sessionManager.commitPreparedNewSession(prepared);
11160
+ committed = true;
10675
11161
  this.agent.reset();
10676
11162
  this.#syncAgentSessionId();
10677
11163
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -10680,16 +11166,8 @@ export class AgentSession {
10680
11166
  this.#pendingNextTurnMessages = [];
10681
11167
  this.#scheduledHiddenNextTurnGeneration = undefined;
10682
11168
  this.#todoReminderCount = 0;
10683
- if (model) {
10684
- this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
10685
- }
10686
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
10687
- this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
10688
-
10689
- // Inject the handoff document as a custom message
10690
- const handoffContent = createHandoffContext(handoffText);
10691
- this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
10692
- await this.sessionManager.ensureOnDisk();
11169
+ this.agent.replaceMessages(sessionContext.messages);
11170
+ this.#syncTodoPhasesFromBranch();
10693
11171
  if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
10694
11172
  try {
10695
11173
  const artifactId = await this.sessionManager.saveArtifact(`${handoffText}\n`, "handoff");
@@ -10701,30 +11179,9 @@ export class AgentSession {
10701
11179
  }
10702
11180
  }
10703
11181
 
10704
- // Rebuild agent messages from session
10705
- const sessionContext = this.buildDisplaySessionContext();
10706
- this.agent.replaceMessages(sessionContext.messages);
10707
- this.#syncTodoPhasesFromBranch();
10708
-
10709
- // Construct the successor workflow-gate emitter in the reversible window:
10710
- // FileGateStore load / broker init can throw, and a failure here must roll
10711
- // back rather than corrupt an already-committed switch. Publication is
10712
- // deferred to the (no-throw) commit step below.
10713
- const successorGateEmitter = this.#constructWorkflowGateEmitter();
10714
-
10715
- // --- Commit boundary: the successor is durably persisted and every
10716
- // reversible, fallible step above has succeeded. Cross the irreversible
10717
- // commit BEFORE any identity rotation. Fencing the predecessor gate
10718
- // emitter and rotating provider sessions cannot be undone, so once we
10719
- // begin them a failure must RETAIN the committed successor rather than
10720
- // attempt a (now-impossible) rollback that would resume a fenced emitter.
10721
- committed = true;
11182
+ // The forward-only suffix starts only after the committed successor is live.
10722
11183
  this.#resetInjectedContextSignatures();
10723
- this.#publishWorkflowGateEmitter(
10724
- successorGateEmitter,
10725
- rollbackSessionState.sessionId,
10726
- suspendedWorkflowGateEmitter,
10727
- );
11184
+ this.#publishWorkflowGateEmitter(successorGateEmitter, rollbackSessionState.sessionId);
10728
11185
  this.#closeAllProviderSessions("session handoff");
10729
11186
  this.#rebindProviderSessionState(new Map());
10730
11187
  this.#resetHindsightConversationTrackingIfHindsight();
@@ -10760,12 +11217,12 @@ export class AgentSession {
10760
11217
  return { document: handoffText, savedPath };
10761
11218
  } catch (switchError) {
10762
11219
  if (committed) {
10763
- // The switch already committed; a post-commit step failed. Do not
10764
- // tear down the new session — surface success (the handoff exists).
10765
- logger.warn("Handoff post-commit step failed after the session switch committed", {
10766
- error: switchError instanceof Error ? switchError.message : String(switchError),
11220
+ throw Object.assign(new Error("Handoff committed but successor initialization was degraded."), {
11221
+ code: "handoff_committed_degraded",
11222
+ cause: switchError,
11223
+ handoffDocument: handoffText,
11224
+ savedPath,
10767
11225
  });
10768
- return { document: handoffText, savedPath };
10769
11226
  }
10770
11227
  // Reversible window: roll back to the pre-handoff session so the
10771
11228
  // failure is non-destructive. Predecessor gate emitter, provider
@@ -10773,7 +11230,6 @@ export class AgentSession {
10773
11230
  // were never mutated before commit, so they survive intact.
10774
11231
  this.sessionManager.restoreState(rollbackSessionState);
10775
11232
  this.#syncAgentSessionId(rollbackSessionState.sessionId);
10776
- this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
10777
11233
  this.#rekeyHindsightMemoryForCurrentSessionId();
10778
11234
  this.agent.replaceMessages(rollbackAgentMessages);
10779
11235
  this.agent.clearAllQueues();
@@ -10785,25 +11241,17 @@ export class AgentSession {
10785
11241
  this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
10786
11242
  this.#todoReminderCount = rollbackTodoReminderCount;
10787
11243
  this.#syncTodoPhasesFromBranch();
10788
- // Remove the orphaned successor transcript/artifacts written before the
10789
- // failure so a rolled-back handoff leaves nothing behind.
10790
- if (successorSessionFile && successorSessionFile !== previousSessionFile) {
10791
- try {
10792
- await this.sessionManager.discardUncommittedSession(successorSessionFile);
10793
- } catch (cleanupError) {
10794
- logger.warn("Failed to remove orphaned handoff session after rollback", {
10795
- path: successorSessionFile,
10796
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
10797
- });
10798
- }
10799
- }
11244
+ // Exact-discard only the staged successor; predecessor state was never adopted.
11245
+ const rollbackError = prepared
11246
+ ? await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, switchError)
11247
+ : switchError;
10800
11248
  // Map to cancellation only for a genuine handoff-signal abort; a
10801
11249
  // downstream error keeps its cause and the generated document.
10802
11250
  if (handoffSignal.aborted) {
10803
11251
  throw new Error("Handoff cancelled");
10804
11252
  }
10805
11253
  // Preserve the generated handoff document for copy/retry.
10806
- throw Object.assign(switchError instanceof Error ? switchError : new Error(String(switchError)), {
11254
+ throw Object.assign(rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError)), {
10807
11255
  handoffDocument: handoffText,
10808
11256
  });
10809
11257
  }
@@ -10811,7 +11259,7 @@ export class AgentSession {
10811
11259
  // Genuine handoff-signal cancellation maps to a cancellation error.
10812
11260
  // Errors surfaced by the inner transaction (which already rolled back
10813
11261
  // and, for non-aborts, attached the generated document) pass through.
10814
- if (handoffSignal.aborted) {
11262
+ if (handoffSignal.aborted && !(error instanceof AggregateError)) {
10815
11263
  throw new Error("Handoff cancelled");
10816
11264
  }
10817
11265
  throw error;
@@ -10853,7 +11301,11 @@ export class AgentSession {
10853
11301
  * @param assistantMessage The assistant message to check
10854
11302
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
10855
11303
  */
10856
- async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
11304
+ async #checkCompaction(
11305
+ assistantMessage: AssistantMessage,
11306
+ skipAbortedCheck = true,
11307
+ onTerminalOverflowNoop?: () => void,
11308
+ ): Promise<boolean> {
10857
11309
  // Safety stops are terminal and must not trigger context maintenance.
10858
11310
  if (
10859
11311
  assistantMessage.errorKind === "provider_safety_stop" ||
@@ -10889,8 +11341,10 @@ export class AgentSession {
10889
11341
  // Remove the error message from agent state (it IS saved to session for history,
10890
11342
  // but we don't want it in context for the retry)
10891
11343
  const messages = this.agent.state.messages;
11344
+ let removedOverflowAssistant = false;
10892
11345
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
10893
11346
  this.agent.replaceMessages(messages.slice(0, -1));
11347
+ removedOverflowAssistant = true;
10894
11348
  }
10895
11349
 
10896
11350
  // Try context promotion first - switch to a larger model and retry without compacting
@@ -10905,7 +11359,15 @@ export class AgentSession {
10905
11359
  // No promotion target available fall through to compaction
10906
11360
  const compactionSettings = this.settings.getGroup("compaction");
10907
11361
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
10908
- const status = await this.#runAutoCompaction("overflow", true);
11362
+ const status = await this.#runAutoCompaction("overflow", true, false, {
11363
+ beforeTerminalOverflowNoop: () => {
11364
+ if (onTerminalOverflowNoop) {
11365
+ onTerminalOverflowNoop();
11366
+ } else if (removedOverflowAssistant) {
11367
+ this.agent.appendMessage(assistantMessage);
11368
+ }
11369
+ },
11370
+ });
10909
11371
  return "continuationScheduled" in status && status.continuationScheduled === true;
10910
11372
  }
10911
11373
  return this.#scheduleOverflowRetryContinuation(generation);
@@ -11673,13 +12135,18 @@ export class AgentSession {
11673
12135
  this.#defaultFallbackExhaustedLastTurn = false;
11674
12136
  }
11675
12137
 
12138
+ #setAgentModelWithReasoningContext(model: Model | undefined): void {
12139
+ this.#reasoningControlContextGeneration++;
12140
+ this.agent.setModel(model);
12141
+ }
12142
+
11676
12143
  #setModelWithProviderSessionReset(model: Model | undefined): void {
11677
12144
  this.#defaultModelSelectionMutationRevision++;
11678
12145
  const currentModel = this.model;
11679
12146
  if (currentModel) {
11680
12147
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
11681
12148
  }
11682
- this.agent.setModel(model);
12149
+ this.#setAgentModelWithReasoningContext(model);
11683
12150
  }
11684
12151
 
11685
12152
  #setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
@@ -11688,7 +12155,7 @@ export class AgentSession {
11688
12155
  if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
11689
12156
  const currentModel = this.model;
11690
12157
  if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
11691
- this.agent.setModel(model);
12158
+ this.#setAgentModelWithReasoningContext(model);
11692
12159
  this.#syncAppendOnlyContext(model);
11693
12160
  }
11694
12161
 
@@ -12214,6 +12681,7 @@ export class AgentSession {
12214
12681
  deferHandoffMaintenance?: boolean;
12215
12682
  force?: boolean;
12216
12683
  signal?: AbortSignal;
12684
+ beforeTerminalOverflowNoop?: () => void;
12217
12685
  },
12218
12686
  ): Promise<AutoCompactionTerminalStatus> {
12219
12687
  const compactionSettings = this.settings.getGroup("compaction");
@@ -12351,16 +12819,42 @@ export class AgentSession {
12351
12819
  if (autoCompactionSignal.aborted) return await emitAborted();
12352
12820
 
12353
12821
  if (!preparation) {
12354
- const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
12822
+ // #checkCompaction removes the failed overflow assistant before entering
12823
+ // this path. A resumable tail would therefore resend the same oversized
12824
+ // request even though maintenance made no change. Non-resumable tails
12825
+ // retain the existing synthetic auto-continue recovery, which changes
12826
+ // the request and is covered by the continuation contract.
12827
+ const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
12828
+ const continuationSkipReason =
12829
+ !overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
12830
+ if (overflowNoopWouldReplay) {
12831
+ options?.beforeTerminalOverflowNoop?.();
12832
+ }
12355
12833
  await this.#emitSessionEvent({
12356
12834
  type: "auto_compaction_end",
12357
12835
  action,
12358
12836
  result: undefined,
12359
12837
  aborted: false,
12360
- willRetry: willRetry && !continuationSkipReason,
12838
+ willRetry: overflowNoopWouldReplay ? false : willRetry && !continuationSkipReason,
12839
+ errorMessage: overflowNoopWouldReplay
12840
+ ? "Context overflow recovery skipped: nothing eligible to compact. Run /clear to preserve this session ID, or switch to a larger-context model before retrying."
12841
+ : undefined,
12361
12842
  skipped: true,
12362
12843
  continuationSkipReason,
12363
12844
  });
12845
+ if (overflowNoopWouldReplay) {
12846
+ if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
12847
+ this.#scheduleAgentContinue({
12848
+ delayMs: 100,
12849
+ generation,
12850
+ shouldContinue: () => this.agent.hasQueuedMessages(),
12851
+ onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
12852
+ onError: error => this.#logCompactionContinuationError("queued_continue", error),
12853
+ });
12854
+ return { kind: "skipped", continuationScheduled: true };
12855
+ }
12856
+ return { kind: "skipped" };
12857
+ }
12364
12858
  if (willRetry) {
12365
12859
  return { kind: "skipped", continuationScheduled: this.#scheduleOverflowRetryContinuation(generation) };
12366
12860
  }
@@ -12501,8 +12995,14 @@ export class AgentSession {
12501
12995
  break;
12502
12996
  }
12503
12997
 
12504
- const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
12505
- const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
12998
+ // Legacy parsed Retry-After is capped at retry.maxDelayMs (see
12999
+ // compactionRetryDelay); only managed fallback is uncapped.
13000
+ const delayMs = compactionRetryDelay(
13001
+ retrySettings.baseDelayMs,
13002
+ retrySettings.maxDelayMs,
13003
+ attempt,
13004
+ retryAfterMs,
13005
+ );
12506
13006
 
12507
13007
  // If retry delay is too long (>30s), try next candidate instead of waiting
12508
13008
  const maxAcceptableDelayMs = 30_000;
@@ -12695,7 +13195,7 @@ export class AgentSession {
12695
13195
  );
12696
13196
  }
12697
13197
 
12698
- #alibabaTokenPlanCanonicalTimeoutForApi(api: string): string | undefined {
13198
+ #alibabaTokenPlanTimeoutMessagesForApi(api: string): ReadonlySet<string> | undefined {
12699
13199
  if (api === "openai-responses" || api === "openai-completions") {
12700
13200
  return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
12701
13201
  }
@@ -12704,18 +13204,23 @@ export class AgentSession {
12704
13204
 
12705
13205
  #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
12706
13206
  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;
13207
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
13208
+ return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
12709
13209
  }
12710
13210
 
12711
13211
  #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
12712
13212
  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
- );
13213
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(candidate.api);
13214
+ if (!timeoutMessages) return false;
13215
+ for (const timeoutMessage of timeoutMessages) {
13216
+ if (
13217
+ errorMessage === `Summarization failed: ${timeoutMessage}` ||
13218
+ errorMessage === `Turn prefix summarization failed: ${timeoutMessage}`
13219
+ ) {
13220
+ return true;
13221
+ }
13222
+ }
13223
+ return false;
12719
13224
  }
12720
13225
 
12721
13226
  #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
@@ -13056,8 +13561,15 @@ export class AgentSession {
13056
13561
  type: "maintenance",
13057
13562
  continuation: async ownership => {
13058
13563
  if (!ownership.isCurrent()) return;
13059
- const successorScheduled = await this.#checkCompaction(outcome.message);
13060
- if (successorScheduled || !ownership.isCurrent()) return;
13564
+ let terminalized = false;
13565
+ const successorScheduled = await this.#checkCompaction(outcome.message, true, () => {
13566
+ terminalized = true;
13567
+ this.agent.requestRunTerminal(ownership.logicalRunId, {
13568
+ stopReason: "error",
13569
+ messages: [outcome.message],
13570
+ });
13571
+ });
13572
+ if (terminalized || successorScheduled || !ownership.isCurrent()) return;
13061
13573
  this.agent.requestRunTerminal(ownership.logicalRunId, {
13062
13574
  stopReason: "error",
13063
13575
  messages: [outcome.message],
@@ -13067,7 +13579,7 @@ export class AgentSession {
13067
13579
  }
13068
13580
  if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
13069
13581
  // The managed transport discarded this attempt before session policy saw it.
13070
- // Remove its provisional controller charge and surface the original message.
13582
+ // Remove its provisional charge without changing sticky fallback selection.
13071
13583
  this.#defaultFallbackChain().discardStartedAttempt();
13072
13584
  return {
13073
13585
  type: "terminal",
@@ -14543,7 +15055,10 @@ export class AgentSession {
14543
15055
  * Listeners are preserved and will continue receiving events.
14544
15056
  * @returns true if switch completed, false if cancelled by hook
14545
15057
  */
14546
- async switchSession(sessionPath: string): Promise<boolean> {
15058
+ async switchSession(
15059
+ sessionPath: string,
15060
+ options?: { transition?: SessionSwitchEvent["transition"] },
15061
+ ): Promise<boolean> {
14547
15062
  this.#beginSessionTransition("switch-session");
14548
15063
  try {
14549
15064
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -14600,8 +15115,10 @@ export class AgentSession {
14600
15115
  try {
14601
15116
  await this.sessionManager.setSessionFile(sessionPath);
14602
15117
  // 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).
15118
+ // published; gate its local:// root before publication so the agent,
15119
+ // workflow-gate emitter, and hooks cannot resolve against an ungated
15120
+ // root. The manager-rotation window itself is tracked in #3138
15121
+ // (#2797 / #2925).
14605
15122
  if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14606
15123
  this.#syncAgentSessionId();
14607
15124
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -14667,6 +15184,12 @@ export class AgentSession {
14667
15184
  ? this.#getInheritedThinkingLevel()
14668
15185
  : persistedThinkingLevel,
14669
15186
  );
15187
+ this.#thinkingLevelMutationRevision++;
15188
+ this.#thinkingLevelLiveMutationRevision++;
15189
+ this.#pendingThinkingLevelControlSuccess = undefined;
15190
+ this.#pendingThinkingLevelControlFailure = undefined;
15191
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15192
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14670
15193
  this.#thinkingLevel = nextThinkingLevel;
14671
15194
  this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
14672
15195
  this.agent.serviceTier = hasServiceTierEntry
@@ -14677,6 +15200,7 @@ export class AgentSession {
14677
15200
  // Establish the successor's durable session identity only after every
14678
15201
  // restored state facet is live. Identity-bound extension hooks run below.
14679
15202
  await this.sessionManager.ensureOnDisk();
15203
+ if (!switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14680
15204
 
14681
15205
  if (switchingToDifferentSession) {
14682
15206
  // The local:// migration gate for this successor already ran above,
@@ -14699,6 +15223,7 @@ export class AgentSession {
14699
15223
  type: "session_switch",
14700
15224
  reason: "resume",
14701
15225
  previousSessionFile,
15226
+ ...(options?.transition ? { transition: options.transition } : {}),
14702
15227
  });
14703
15228
  }
14704
15229
  return true;
@@ -14734,8 +15259,14 @@ export class AgentSession {
14734
15259
  this.agent.restoreSteering(previousAgentSteeringQueue);
14735
15260
  this.agent.restoreFollowUp(previousAgentFollowUpQueue);
14736
15261
  if (previousModel) {
14737
- this.agent.setModel(previousModel);
14738
- }
15262
+ this.#setAgentModelWithReasoningContext(previousModel);
15263
+ }
15264
+ this.#thinkingLevelMutationRevision++;
15265
+ this.#thinkingLevelLiveMutationRevision++;
15266
+ this.#pendingThinkingLevelControlSuccess = undefined;
15267
+ this.#pendingThinkingLevelControlFailure = undefined;
15268
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15269
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14739
15270
  this.#thinkingLevel = previousThinkingLevel;
14740
15271
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
14741
15272
  this.agent.serviceTier = previousServiceTier;
@@ -14799,22 +15330,21 @@ export class AgentSession {
14799
15330
  skipConversationRestore = result?.skipConversationRestore ?? false;
14800
15331
  }
14801
15332
 
14802
- // Clear pending messages (bound to old session state)
15333
+ // Flush pending writes before preparing the successor.
15334
+ await this.sessionManager.flush();
15335
+ const prepared = selectedEntry.parentId
15336
+ ? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
15337
+ : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
15338
+ try {
15339
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
15340
+ this.sessionManager.commitPreparedNewSession(prepared);
15341
+ } catch (error) {
15342
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
15343
+ }
14803
15344
  this.#pendingNextTurnMessages = [];
14804
15345
  this.#scheduledHiddenNextTurnGeneration = undefined;
14805
-
14806
- // Flush pending writes before branching
14807
- await this.sessionManager.flush();
14808
15346
  this.#cancelOwnAsyncJobs();
14809
15347
 
14810
- if (!selectedEntry.parentId) {
14811
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
14812
- } else {
14813
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
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());
14818
15348
  this.#syncTodoPhasesFromBranch();
14819
15349
  this.#syncAgentSessionId();
14820
15350
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);