@gajae-code/coding-agent 0.11.11 → 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 (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. 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,
@@ -244,6 +245,7 @@ import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injec
244
245
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
245
246
  import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
246
247
  import type { HookCommandContext } from "../extensibility/hooks/types";
248
+ import type { SessionSwitchEvent } from "../extensibility/shared-events";
247
249
  import {
248
250
  buildSkillPromptMessage,
249
251
  getSkillSlashCommandName,
@@ -254,6 +256,11 @@ import {
254
256
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
255
257
  import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
256
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";
257
264
  import {
258
265
  assertNonEmptyGjcSessionId,
259
266
  modeStatePath as sessionModeStatePath,
@@ -368,6 +375,7 @@ import {
368
375
  prepareContributionPrep,
369
376
  } from "./contribution-prep";
370
377
  import { pruneStaleFileMentions } from "./file-mention-pruning";
378
+ import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
371
379
  import {
372
380
  type BashExecutionMessage,
373
381
  type CompactionSummaryMessage,
@@ -613,6 +621,24 @@ export interface AgentSessionConfig {
613
621
  providerCacheSessionId?: string;
614
622
  }
615
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
+
616
642
  type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
617
643
 
618
644
  type AutoCompactionTerminalStatus =
@@ -642,8 +668,16 @@ export interface PromptOptions {
642
668
  /**
643
669
  * Invoked after all prompt preflight checks pass and immediately before agent execution begins.
644
670
  * Cancellation before this callback rejects the prompt.
671
+ * Prefer `onPreflightAcceptCommit` for async durable acceptance (#3031/#3032).
645
672
  */
646
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;
647
681
  }
648
682
 
649
683
  function promptPreflightCancelledError(): Error {
@@ -1538,6 +1572,47 @@ export class AgentSession {
1538
1572
  #sessionAdmissionClosed = false;
1539
1573
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
1540
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;
1541
1616
  #promptTemplates: PromptTemplate[];
1542
1617
  #slashCommands: FileSlashCommand[];
1543
1618
 
@@ -1789,6 +1864,7 @@ export class AgentSession {
1789
1864
  #constructorMCPToolSelection: string[] | undefined;
1790
1865
  #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
1791
1866
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
1867
+ #memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
1792
1868
 
1793
1869
  // TTSR manager for time-traveling stream rules
1794
1870
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -2244,6 +2320,7 @@ export class AgentSession {
2244
2320
  this.#unregisterResourceGc = registerResourceGcSession({
2245
2321
  sessionId: resourceGcSessionId,
2246
2322
  settings: this.settings,
2323
+ cwd: () => this.sessionManager.getCwd(),
2247
2324
  });
2248
2325
  }
2249
2326
  this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
@@ -2671,9 +2748,15 @@ export class AgentSession {
2671
2748
  this.#defaultFallbackController = scope.fallbackController;
2672
2749
  const previousEditMode = this.#resolveActiveEditMode();
2673
2750
  if (scope.model) {
2674
- this.agent.setModel(scope.model);
2751
+ this.#setAgentModelWithReasoningContext(scope.model);
2675
2752
  this.#syncAppendOnlyContext(scope.model);
2676
2753
  }
2754
+ this.#thinkingLevelMutationRevision++;
2755
+ this.#thinkingLevelLiveMutationRevision++;
2756
+ this.#pendingThinkingLevelControlSuccess = undefined;
2757
+ this.#pendingThinkingLevelControlFailure = undefined;
2758
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
2759
+ this.#pendingThinkingVisibilityControlFailure = undefined;
2677
2760
  this.#thinkingLevel = scope.thinkingLevel;
2678
2761
  this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
2679
2762
  void this.#syncEditToolModeAfterModelChange(previousEditMode);
@@ -5117,6 +5200,7 @@ export class AgentSession {
5117
5200
  * such event.
5118
5201
  */
5119
5202
  #syncAgentSessionId(sessionId?: string): void {
5203
+ this.#reasoningControlContextGeneration++;
5120
5204
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
5121
5205
  this.agent.sessionId = sid;
5122
5206
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
@@ -6441,17 +6525,65 @@ export class AgentSession {
6441
6525
  return this.#promptGeneration;
6442
6526
  }
6443
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
+
6444
6569
  /** The immutable recovery authority, present only before external ownership promotion. */
6445
6570
  get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
6446
6571
  return this.#recoveryHydrationContext;
6447
6572
  }
6448
6573
 
6449
6574
  /** Enables normal session mutations after the owner has published its durable fence and writer lease. */
6450
- async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: RecoveryHydrationPromotionFence): Promise<void> {
6575
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: AgentMemoryGuardPromotionFence): Promise<void> {
6451
6576
  const context = this.#recoveryHydrationContext;
6452
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
+ }
6453
6584
  await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
6454
6585
  this.#recoveryHydrationContext = undefined;
6586
+ this.#memoryGuardClaimsLease = undefined;
6455
6587
  }
6456
6588
 
6457
6589
  /** Recovery hydration must not start a continuation before ownership promotion. */
@@ -6662,7 +6794,7 @@ export class AgentSession {
6662
6794
  async invokeSkill(
6663
6795
  name: string,
6664
6796
  args = "",
6665
- options?: Pick<PromptOptions, "onPreflightAccepted">,
6797
+ options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared">,
6666
6798
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6667
6799
  const skillName = name.trim();
6668
6800
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6695,6 +6827,12 @@ export class AgentSession {
6695
6827
  attribution: "user" as const,
6696
6828
  };
6697
6829
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
6830
+ options?.onSkillPrepared?.({
6831
+ name: skill.name,
6832
+ path: skill.filePath,
6833
+ lineCount: built.details.lineCount,
6834
+ cleanedArgs: activation.cleanedArgs || undefined,
6835
+ });
6698
6836
  await this.promptCustomMessage(skillPromptMessage, options);
6699
6837
  return {
6700
6838
  name: skill.name,
@@ -7350,6 +7488,7 @@ export class AgentSession {
7350
7488
  * @throws Error if no model selected or no API key available (when not streaming)
7351
7489
  */
7352
7490
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7491
+ this.#assertRecoveryHydrationPromoted();
7353
7492
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7354
7493
 
7355
7494
  if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
@@ -7361,7 +7500,14 @@ export class AgentSession {
7361
7500
  await this.invokeSkill(
7362
7501
  invocation.skill.name,
7363
7502
  invocation.args,
7364
- options?.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : undefined,
7503
+ options?.onPreflightAccepted || options?.onPreflightAcceptCommit
7504
+ ? {
7505
+ ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
7506
+ ...(options.onPreflightAcceptCommit
7507
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
7508
+ : {}),
7509
+ }
7510
+ : undefined,
7365
7511
  );
7366
7512
  return;
7367
7513
  }
@@ -7415,7 +7561,8 @@ export class AgentSession {
7415
7561
  if (workflowIntentDiff) {
7416
7562
  this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
7417
7563
  }
7418
- options?.onPreflightAccepted?.();
7564
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
7565
+ else options?.onPreflightAccepted?.();
7419
7566
  return;
7420
7567
  }
7421
7568
 
@@ -7547,7 +7694,10 @@ export class AgentSession {
7547
7694
 
7548
7695
  async promptCustomMessage<T = unknown>(
7549
7696
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7550
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted">,
7697
+ options?: Pick<
7698
+ PromptOptions,
7699
+ "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7700
+ >,
7551
7701
  ): Promise<void> {
7552
7702
  const textContent =
7553
7703
  typeof message.content === "string"
@@ -7609,7 +7759,10 @@ export class AgentSession {
7609
7759
  async #promptWithMessage(
7610
7760
  message: AgentMessage,
7611
7761
  expandedText: string,
7612
- options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted"> & {
7762
+ options?: Pick<
7763
+ PromptOptions,
7764
+ "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7765
+ > & {
7613
7766
  prependMessages?: AgentMessage[];
7614
7767
  skipPostPromptRecoveryWait?: boolean;
7615
7768
  predecessorAgentEndHold?: symbol;
@@ -7716,7 +7869,7 @@ export class AgentSession {
7716
7869
  // A newer abort/prompt cycle superseded this preflight. Callers awaiting
7717
7870
  // acceptance (onPreflightAccepted) must be told it never ran; direct
7718
7871
  // callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
7719
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7872
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7720
7873
  return;
7721
7874
  }
7722
7875
 
@@ -7830,7 +7983,7 @@ export class AgentSession {
7830
7983
  this.#resetInjectedContextSignatures();
7831
7984
  // Ack-waiting callers are told the preflight never ran; direct callers
7832
7985
  // (aborted after setup) resolve gracefully as before f24f46ff5.
7833
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7986
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7834
7987
  return;
7835
7988
  }
7836
7989
 
@@ -7843,7 +7996,8 @@ export class AgentSession {
7843
7996
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
7844
7997
  },
7845
7998
  };
7846
- options?.onPreflightAccepted?.();
7999
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8000
+ else options?.onPreflightAccepted?.();
7847
8001
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
7848
8002
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
7849
8003
  const terminalAssistant = this.#findLastAssistantMessage();
@@ -7862,7 +8016,12 @@ export class AgentSession {
7862
8016
  // Session identity changes historically cancel local setup silently. Only SDK
7863
8017
  // submissions provide an acceptance callback and require an explicit terminal
7864
8018
  // preflight failure for their remote request authority.
7865
- if (isPromptPreflightCancelledError(error) && !options?.onPreflightAccepted) return;
8019
+ if (
8020
+ isPromptPreflightCancelledError(error) &&
8021
+ !options?.onPreflightAccepted &&
8022
+ !options?.onPreflightAcceptCommit
8023
+ )
8024
+ return;
7866
8025
  throw error;
7867
8026
  } finally {
7868
8027
  this.#removeEphemeralCustomMessages();
@@ -7963,7 +8122,7 @@ export class AgentSession {
7963
8122
  }
7964
8123
  return false;
7965
8124
  },
7966
- invokeSkill: (name, args) => this.invokeSkill(name, args),
8125
+ invokeSkill: (name, args, options) => this.invokeSkill(name, args, options),
7967
8126
  setPlanMode: on => this.setSdkPlanMode(on),
7968
8127
  operateGoal: (op, objective) => this.operateGoal(op, objective),
7969
8128
  getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -8078,6 +8237,7 @@ export class AgentSession {
8078
8237
  * Queue a steering message to interrupt the agent mid-run.
8079
8238
  */
8080
8239
  async steer(text: string, images?: ImageContent[]): Promise<void> {
8240
+ this.#assertRecoveryHydrationPromoted();
8081
8241
  if (text.startsWith("/")) {
8082
8242
  this.#throwIfExtensionCommand(text);
8083
8243
  }
@@ -8095,6 +8255,7 @@ export class AgentSession {
8095
8255
  images?: ImageContent[],
8096
8256
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
8097
8257
  ): Promise<void> {
8258
+ this.#assertRecoveryHydrationPromoted();
8098
8259
  if (text.startsWith("/")) {
8099
8260
  this.#throwIfExtensionCommand(text);
8100
8261
  }
@@ -8329,6 +8490,7 @@ export class AgentSession {
8329
8490
  followUpQueuePolicy?: "respect-mode" | "sequential";
8330
8491
  },
8331
8492
  ): Promise<void> {
8493
+ this.#assertRecoveryHydrationPromoted();
8332
8494
  const appMessage: CustomMessage<T> = {
8333
8495
  role: "custom",
8334
8496
  customType: message.customType,
@@ -8470,8 +8632,13 @@ export class AgentSession {
8470
8632
  */
8471
8633
  async sendUserMessage(
8472
8634
  content: string | (TextContent | ImageContent)[],
8473
- options?: { deliverAs?: "steer" | "followUp"; onPreflightAccepted?: () => void },
8635
+ options?: {
8636
+ deliverAs?: "steer" | "followUp";
8637
+ onPreflightAccepted?: () => void;
8638
+ onPreflightAcceptCommit?: () => void | Promise<void>;
8639
+ },
8474
8640
  ): Promise<void> {
8641
+ this.#assertRecoveryHydrationPromoted();
8475
8642
  // Normalize content to text string + optional images
8476
8643
  let text: string;
8477
8644
  let images: ImageContent[] | undefined;
@@ -8493,11 +8660,13 @@ export class AgentSession {
8493
8660
  }
8494
8661
 
8495
8662
  if (options?.deliverAs === "followUp") {
8663
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8496
8664
  await this.#queueFollowUp(text, images, { claimsGenuineUserIntent: true });
8497
8665
  options.onPreflightAccepted?.();
8498
8666
  return;
8499
8667
  }
8500
8668
  if (options?.deliverAs === "steer") {
8669
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8501
8670
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8502
8671
  options.onPreflightAccepted?.();
8503
8672
  return;
@@ -8509,6 +8678,7 @@ export class AgentSession {
8509
8678
  // in-flight compaction internally, and #queueSteer would otherwise park
8510
8679
  // the message in the steering queue with no turn to consume it.
8511
8680
  if (this.isStreaming) {
8681
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8512
8682
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8513
8683
  options?.onPreflightAccepted?.();
8514
8684
  return;
@@ -8519,6 +8689,7 @@ export class AgentSession {
8519
8689
  expandPromptTemplates: false,
8520
8690
  images,
8521
8691
  onPreflightAccepted: options?.onPreflightAccepted,
8692
+ onPreflightAcceptCommit: options?.onPreflightAcceptCommit,
8522
8693
  });
8523
8694
  }
8524
8695
 
@@ -9258,6 +9429,12 @@ export class AgentSession {
9258
9429
  ): Promise<void> {
9259
9430
  this.#clearConstructorToolSelectionAuthority();
9260
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;
9261
9438
  this.#thinkingLevel = inheritedThinkingLevel;
9262
9439
  this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
9263
9440
  this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
@@ -9607,7 +9784,7 @@ export class AgentSession {
9607
9784
  const ownsScope = scope !== undefined && !suppliedScope;
9608
9785
  try {
9609
9786
  if (isTemporaryOperation) {
9610
- this.agent.setModel(model);
9787
+ this.#setAgentModelWithReasoningContext(model);
9611
9788
  this.#syncAppendOnlyContext(model);
9612
9789
  } else {
9613
9790
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
@@ -9692,6 +9869,12 @@ export class AgentSession {
9692
9869
  this.#clearActiveRetryFallback();
9693
9870
  this.#setModelWithProviderSessionReset(model);
9694
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;
9695
9878
  this.#thinkingLevel = thinkingLevel;
9696
9879
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
9697
9880
  if (thinkingLevelChanged) {
@@ -10000,7 +10183,19 @@ export class AgentSession {
10000
10183
  * Set thinking level.
10001
10184
  * Saves the effective metadata-clamped level to the session, and to settings when requested.
10002
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
+
10003
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;
10004
10199
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
10005
10200
  const isChanging = effectiveLevel !== this.#thinkingLevel;
10006
10201
 
@@ -10020,11 +10215,10 @@ export class AgentSession {
10020
10215
  }
10021
10216
 
10022
10217
  /**
10023
- * 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.
10024
10219
  */
10025
10220
  async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
10026
10221
  const previousThinkingLevel = this.thinkingLevel;
10027
- const previousScope = this.getThinkingScopeForControl();
10028
10222
  if (!persist) {
10029
10223
  this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
10030
10224
  if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
@@ -10033,26 +10227,99 @@ export class AgentSession {
10033
10227
  return;
10034
10228
  }
10035
10229
 
10036
- const previousDefaultThinkingLevel = this.settings.getGlobal("defaultThinkingLevel");
10037
- if (level === ThinkingLevel.Inherit) {
10038
- const defaultLevel = getDefault("defaultThinkingLevel");
10039
- this.settings.set("defaultThinkingLevel", defaultLevel);
10040
- this.setThinkingLevel(this.#getInheritedThinkingLevel());
10041
- } else {
10042
- this.setThinkingLevel(level, true);
10043
- }
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;
10044
10241
  try {
10045
- await this.settings.flushOrThrow();
10046
- this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10242
+ await this.settings.commitAtomicBatch([{ path: "defaultThinkingLevel", op: "set", value: persistedLevel }]);
10047
10243
  } catch {
10048
- this.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel ?? getDefault("defaultThinkingLevel"));
10049
- this.setThinkingLevel(previousThinkingLevel);
10050
- this.sessionManager.appendThinkingLevelChange(
10051
- previousScope === "global config" ? ThinkingLevel.Inherit : previousThinkingLevel,
10052
- );
10053
- 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
+ }
10054
10272
  throw new Error("Unable to persist reasoning settings.");
10055
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);
10056
10323
  }
10057
10324
 
10058
10325
  getThinkingScopeForControl(): "session" | "global config" {
@@ -10068,6 +10335,11 @@ export class AgentSession {
10068
10335
  }
10069
10336
 
10070
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;
10071
10343
  this.agent.hideThinkingSummary = visibility === "hidden";
10072
10344
  if (persist) {
10073
10345
  this.settings.set("hideThinkingBlock", visibility === "hidden");
@@ -10075,7 +10347,7 @@ export class AgentSession {
10075
10347
  }
10076
10348
 
10077
10349
  /**
10078
- * 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.
10079
10351
  */
10080
10352
  async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
10081
10353
  if (!persist) {
@@ -10083,17 +10355,88 @@ export class AgentSession {
10083
10355
  return;
10084
10356
  }
10085
10357
 
10086
- const previousVisibility = this.getThinkingVisibility();
10087
- const previousHideThinkingBlock = this.settings.getGlobal("hideThinkingBlock");
10088
- 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;
10089
10364
  try {
10090
- await this.settings.flushOrThrow();
10365
+ await this.settings.commitAtomicBatch([
10366
+ { path: "hideThinkingBlock", op: "set", value: visibility === "hidden" },
10367
+ ]);
10091
10368
  } catch {
10092
- this.settings.set("hideThinkingBlock", previousHideThinkingBlock ?? getDefault("hideThinkingBlock"));
10093
- this.setThinkingVisibility(previousVisibility);
10094
- 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
+ }
10095
10394
  throw new Error("Unable to persist reasoning settings.");
10096
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);
10097
10440
  }
10098
10441
 
10099
10442
  /**
@@ -10270,6 +10613,7 @@ export class AgentSession {
10270
10613
  * Saves to settings.
10271
10614
  */
10272
10615
  setSteeringMode(mode: "all" | "one-at-a-time"): void {
10616
+ this.#assertDurableSettingsWritable();
10273
10617
  this.agent.setSteeringMode(mode);
10274
10618
  this.settings.set("steeringMode", mode);
10275
10619
  }
@@ -10279,6 +10623,7 @@ export class AgentSession {
10279
10623
  * Saves to settings.
10280
10624
  */
10281
10625
  setFollowUpMode(mode: "all" | "one-at-a-time"): void {
10626
+ this.#assertDurableSettingsWritable();
10282
10627
  this.agent.setFollowUpMode(mode);
10283
10628
  this.settings.set("followUpMode", mode);
10284
10629
  }
@@ -10288,6 +10633,7 @@ export class AgentSession {
10288
10633
  * Saves to settings.
10289
10634
  */
10290
10635
  setInterruptMode(mode: "immediate" | "wait"): void {
10636
+ this.#assertDurableSettingsWritable();
10291
10637
  this.agent.setInterruptMode(mode);
10292
10638
  this.settings.set("interruptMode", mode);
10293
10639
  }
@@ -10955,7 +11301,11 @@ export class AgentSession {
10955
11301
  * @param assistantMessage The assistant message to check
10956
11302
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
10957
11303
  */
10958
- async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
11304
+ async #checkCompaction(
11305
+ assistantMessage: AssistantMessage,
11306
+ skipAbortedCheck = true,
11307
+ onTerminalOverflowNoop?: () => void,
11308
+ ): Promise<boolean> {
10959
11309
  // Safety stops are terminal and must not trigger context maintenance.
10960
11310
  if (
10961
11311
  assistantMessage.errorKind === "provider_safety_stop" ||
@@ -10991,8 +11341,10 @@ export class AgentSession {
10991
11341
  // Remove the error message from agent state (it IS saved to session for history,
10992
11342
  // but we don't want it in context for the retry)
10993
11343
  const messages = this.agent.state.messages;
11344
+ let removedOverflowAssistant = false;
10994
11345
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
10995
11346
  this.agent.replaceMessages(messages.slice(0, -1));
11347
+ removedOverflowAssistant = true;
10996
11348
  }
10997
11349
 
10998
11350
  // Try context promotion first - switch to a larger model and retry without compacting
@@ -11007,7 +11359,15 @@ export class AgentSession {
11007
11359
  // No promotion target available fall through to compaction
11008
11360
  const compactionSettings = this.settings.getGroup("compaction");
11009
11361
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
11010
- 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
+ });
11011
11371
  return "continuationScheduled" in status && status.continuationScheduled === true;
11012
11372
  }
11013
11373
  return this.#scheduleOverflowRetryContinuation(generation);
@@ -11775,13 +12135,18 @@ export class AgentSession {
11775
12135
  this.#defaultFallbackExhaustedLastTurn = false;
11776
12136
  }
11777
12137
 
12138
+ #setAgentModelWithReasoningContext(model: Model | undefined): void {
12139
+ this.#reasoningControlContextGeneration++;
12140
+ this.agent.setModel(model);
12141
+ }
12142
+
11778
12143
  #setModelWithProviderSessionReset(model: Model | undefined): void {
11779
12144
  this.#defaultModelSelectionMutationRevision++;
11780
12145
  const currentModel = this.model;
11781
12146
  if (currentModel) {
11782
12147
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
11783
12148
  }
11784
- this.agent.setModel(model);
12149
+ this.#setAgentModelWithReasoningContext(model);
11785
12150
  }
11786
12151
 
11787
12152
  #setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
@@ -11790,7 +12155,7 @@ export class AgentSession {
11790
12155
  if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
11791
12156
  const currentModel = this.model;
11792
12157
  if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
11793
- this.agent.setModel(model);
12158
+ this.#setAgentModelWithReasoningContext(model);
11794
12159
  this.#syncAppendOnlyContext(model);
11795
12160
  }
11796
12161
 
@@ -12316,6 +12681,7 @@ export class AgentSession {
12316
12681
  deferHandoffMaintenance?: boolean;
12317
12682
  force?: boolean;
12318
12683
  signal?: AbortSignal;
12684
+ beforeTerminalOverflowNoop?: () => void;
12319
12685
  },
12320
12686
  ): Promise<AutoCompactionTerminalStatus> {
12321
12687
  const compactionSettings = this.settings.getGroup("compaction");
@@ -12453,16 +12819,42 @@ export class AgentSession {
12453
12819
  if (autoCompactionSignal.aborted) return await emitAborted();
12454
12820
 
12455
12821
  if (!preparation) {
12456
- 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
+ }
12457
12833
  await this.#emitSessionEvent({
12458
12834
  type: "auto_compaction_end",
12459
12835
  action,
12460
12836
  result: undefined,
12461
12837
  aborted: false,
12462
- 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,
12463
12842
  skipped: true,
12464
12843
  continuationSkipReason,
12465
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
+ }
12466
12858
  if (willRetry) {
12467
12859
  return { kind: "skipped", continuationScheduled: this.#scheduleOverflowRetryContinuation(generation) };
12468
12860
  }
@@ -13169,8 +13561,15 @@ export class AgentSession {
13169
13561
  type: "maintenance",
13170
13562
  continuation: async ownership => {
13171
13563
  if (!ownership.isCurrent()) return;
13172
- const successorScheduled = await this.#checkCompaction(outcome.message);
13173
- 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;
13174
13573
  this.agent.requestRunTerminal(ownership.logicalRunId, {
13175
13574
  stopReason: "error",
13176
13575
  messages: [outcome.message],
@@ -14656,7 +15055,10 @@ export class AgentSession {
14656
15055
  * Listeners are preserved and will continue receiving events.
14657
15056
  * @returns true if switch completed, false if cancelled by hook
14658
15057
  */
14659
- async switchSession(sessionPath: string): Promise<boolean> {
15058
+ async switchSession(
15059
+ sessionPath: string,
15060
+ options?: { transition?: SessionSwitchEvent["transition"] },
15061
+ ): Promise<boolean> {
14660
15062
  this.#beginSessionTransition("switch-session");
14661
15063
  try {
14662
15064
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -14782,6 +15184,12 @@ export class AgentSession {
14782
15184
  ? this.#getInheritedThinkingLevel()
14783
15185
  : persistedThinkingLevel,
14784
15186
  );
15187
+ this.#thinkingLevelMutationRevision++;
15188
+ this.#thinkingLevelLiveMutationRevision++;
15189
+ this.#pendingThinkingLevelControlSuccess = undefined;
15190
+ this.#pendingThinkingLevelControlFailure = undefined;
15191
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15192
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14785
15193
  this.#thinkingLevel = nextThinkingLevel;
14786
15194
  this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
14787
15195
  this.agent.serviceTier = hasServiceTierEntry
@@ -14792,6 +15200,7 @@ export class AgentSession {
14792
15200
  // Establish the successor's durable session identity only after every
14793
15201
  // restored state facet is live. Identity-bound extension hooks run below.
14794
15202
  await this.sessionManager.ensureOnDisk();
15203
+ if (!switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14795
15204
 
14796
15205
  if (switchingToDifferentSession) {
14797
15206
  // The local:// migration gate for this successor already ran above,
@@ -14814,6 +15223,7 @@ export class AgentSession {
14814
15223
  type: "session_switch",
14815
15224
  reason: "resume",
14816
15225
  previousSessionFile,
15226
+ ...(options?.transition ? { transition: options.transition } : {}),
14817
15227
  });
14818
15228
  }
14819
15229
  return true;
@@ -14849,8 +15259,14 @@ export class AgentSession {
14849
15259
  this.agent.restoreSteering(previousAgentSteeringQueue);
14850
15260
  this.agent.restoreFollowUp(previousAgentFollowUpQueue);
14851
15261
  if (previousModel) {
14852
- this.agent.setModel(previousModel);
15262
+ this.#setAgentModelWithReasoningContext(previousModel);
14853
15263
  }
15264
+ this.#thinkingLevelMutationRevision++;
15265
+ this.#thinkingLevelLiveMutationRevision++;
15266
+ this.#pendingThinkingLevelControlSuccess = undefined;
15267
+ this.#pendingThinkingLevelControlFailure = undefined;
15268
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15269
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14854
15270
  this.#thinkingLevel = previousThinkingLevel;
14855
15271
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
14856
15272
  this.agent.serviceTier = previousServiceTier;