@gajae-code/coding-agent 0.15.6 → 0.16.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 (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -315,10 +315,16 @@ import {
315
315
  } from "../gjc-runtime/session-layout";
316
316
  import {
317
317
  type CoordinatorToolObservation,
318
+ clearCoordinatorRuntimeStateRescope,
319
+ hasCoordinatorRuntimeStateRescopeJournal,
320
+ markCoordinatorRuntimeStateRescopePublishing,
318
321
  ownerTerminalContextFromEnvironment,
319
322
  persistCoordinatorRuntimeStateFromEvent,
320
323
  persistCoordinatorWorkerIntegrationOutcome,
324
+ prepareCoordinatorRuntimeStateRescope,
325
+ recoverCoordinatorRuntimeStateRescope,
321
326
  registerCoordinatorRuntimeStateFinalizer,
327
+ relocateCoordinatorRuntimeStateForRescope,
322
328
  UNPROVEN_TOOL_LABEL,
323
329
  } from "../gjc-runtime/session-state-sidecar";
324
330
  import {
@@ -2386,6 +2392,7 @@ export class AgentSession {
2386
2392
  #preProfileModel: Model | undefined;
2387
2393
  #sessionAdmissionQueue: SessionAdmissionEntry[] = [];
2388
2394
  #activeSessionAdmission: SessionAdmissionEntry | undefined;
2395
+ #startupPromptWaiters = new Set<Promise<void>>();
2389
2396
  #sessionAdmissionClosing = false;
2390
2397
  #sessionAdmissionClosed = false;
2391
2398
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
@@ -2814,6 +2821,27 @@ export class AgentSession {
2814
2821
  /** Idempotent unregister handle for this session's resource-GC registration. */
2815
2822
  #unregisterResourceGc?: () => void;
2816
2823
  #unregisterRuntimeStateFinalizer?: () => void;
2824
+ #unregisterBeforeMoveListener?: () => void;
2825
+ #unregisterMoveAbortListener?: () => void;
2826
+ #unregisterMovePublicationListener?: () => void;
2827
+ #unregisterAfterMoveListener?: () => void;
2828
+ /**
2829
+ * (Re)register the postmortem finalizer bound to the CURRENT cwd/session file. Called at
2830
+ * construction and after every committed rescope, so an unexpected exit writes terminal
2831
+ * state to the session's live location rather than the abandoned launch root.
2832
+ */
2833
+ #registerRuntimeStateFinalizer(): void {
2834
+ this.#unregisterRuntimeStateFinalizer?.();
2835
+ const currentContext = () => ({
2836
+ sessionId: this.sessionId,
2837
+ cwd: this.sessionManager.getCwd(),
2838
+ sessionFile: this.sessionManager.getSessionFile(),
2839
+ });
2840
+ this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer(
2841
+ currentContext(),
2842
+ currentContext,
2843
+ );
2844
+ }
2817
2845
  #unregisterSessionMemorySettings?: () => void;
2818
2846
  /**
2819
2847
  * AsyncJobManager owned by this session (top-level only). Subagents leave
@@ -2823,6 +2851,7 @@ export class AgentSession {
2823
2851
  readonly #disposeAsyncJobManager: boolean;
2824
2852
  #ownedMcpManager: MCPManager | undefined;
2825
2853
  #startupTurnBarrier: Promise<void> | undefined;
2854
+ #startupTurnBarrierPending = false;
2826
2855
  #pendingPythonMessages: Array<{
2827
2856
  message: PythonExecutionMessage;
2828
2857
  onPersisted?: () => void;
@@ -2848,6 +2877,9 @@ export class AgentSession {
2848
2877
  readonly #asyncJobProviderSessionId: string | undefined;
2849
2878
  #isDisposed = false;
2850
2879
  #disposePromise: Promise<void> | undefined;
2880
+ readonly #disposeAbortController = new AbortController();
2881
+ #disposeAdmissionClosed: Promise<void> | undefined;
2882
+ #disposePostPromptDrain: Promise<void> | undefined;
2851
2883
  readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
2852
2884
  readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
2853
2885
  readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
@@ -3019,6 +3051,7 @@ export class AgentSession {
3019
3051
  #cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
3020
3052
  #postPromptTasks = new Set<Promise<void>>();
3021
3053
  #postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
3054
+ #postPromptTaskRecoveryExcluded = new Set<Promise<void>>();
3022
3055
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
3023
3056
  #postPromptTasksResolve: (() => void) | undefined = undefined;
3024
3057
  #postPromptTasksAbortController = new AbortController();
@@ -3206,6 +3239,65 @@ export class AgentSession {
3206
3239
  */
3207
3240
  #sessionTransitionKind: string | undefined;
3208
3241
  #coordinatorPersistGeneration = 0;
3242
+ #coordinatorRescopeBarrier: Promise<void> | undefined;
3243
+ #releaseCoordinatorRescopeBarrier: (() => void) | undefined;
3244
+ #coordinatorRescopeMoveId: string | undefined;
3245
+ #coordinatorEventHandlers = new Set<Promise<void>>();
3246
+ #coordinatorUnbarrieredPersists = new Set<Promise<void>>();
3247
+ #coordinatorReleasedBarrierPersists = new Set<Promise<void>>();
3248
+
3249
+ #beginCoordinatorRescopeBarrier(): {
3250
+ eventHandlers: Promise<void>[];
3251
+ releasedBarrierPersists: Promise<void>[];
3252
+ } {
3253
+ if (this.#coordinatorRescopeBarrier) throw new Error("Coordinator rescope barrier is already active.");
3254
+ const releasedBarrierPersists = [...this.#coordinatorReleasedBarrierPersists];
3255
+ const barrier = Promise.withResolvers<void>();
3256
+ this.#coordinatorRescopeBarrier = barrier.promise;
3257
+ this.#releaseCoordinatorRescopeBarrier = barrier.resolve;
3258
+ this.#bindPendingAgentEndToRescopeBarrier();
3259
+ return { eventHandlers: [...this.#coordinatorEventHandlers], releasedBarrierPersists };
3260
+ }
3261
+
3262
+ #trackReleasedBarrierPersist(persist: Promise<void>): void {
3263
+ this.#coordinatorReleasedBarrierPersists.add(persist);
3264
+ void persist.then(
3265
+ () => this.#coordinatorReleasedBarrierPersists.delete(persist),
3266
+ () => this.#coordinatorReleasedBarrierPersists.delete(persist),
3267
+ );
3268
+ }
3269
+
3270
+ #bindPendingAgentEndToRescopeBarrier(): void {
3271
+ if (!this.#coordinatorRescopeBarrier || !this.#pendingAgentEndEmit) return;
3272
+ const admission = this.#agentEventAdmission.get(this.#pendingAgentEndEmit);
3273
+ if (admission) admission.persistBarrier = this.#coordinatorRescopeBarrier;
3274
+ }
3275
+
3276
+ #appendCoordinatorPersist(run: () => Promise<void>): Promise<void> {
3277
+ const queued = this.#coordinatorPersistQueue.then(run, run);
3278
+ this.#coordinatorPersistQueue = queued.catch(() => {});
3279
+ return queued;
3280
+ }
3281
+
3282
+ #trackUnbarrieredCoordinatorPersist(persist: Promise<void>): void {
3283
+ this.#coordinatorUnbarrieredPersists.add(persist);
3284
+ void persist.then(
3285
+ () => this.#coordinatorUnbarrieredPersists.delete(persist),
3286
+ () => this.#coordinatorUnbarrieredPersists.delete(persist),
3287
+ );
3288
+ }
3289
+
3290
+ async #drainUnbarrieredCoordinatorPersists(): Promise<void> {
3291
+ while (this.#coordinatorUnbarrieredPersists.size > 0) {
3292
+ await Promise.allSettled([...this.#coordinatorUnbarrieredPersists]);
3293
+ }
3294
+ }
3295
+
3296
+ #endCoordinatorRescopeBarrier(): void {
3297
+ this.#releaseCoordinatorRescopeBarrier?.();
3298
+ this.#releaseCoordinatorRescopeBarrier = undefined;
3299
+ this.#coordinatorRescopeBarrier = undefined;
3300
+ }
3209
3301
 
3210
3302
  #beginSessionTransition(kind: string): void {
3211
3303
  if (this.#sessionTransitionKind !== undefined) {
@@ -3242,16 +3334,61 @@ export class AgentSession {
3242
3334
  return { entry, capability: entry.continuationCapability };
3243
3335
  }
3244
3336
 
3245
- async #awaitStartupTurnBarrier(): Promise<void> {
3246
- const barrier = this.#startupTurnBarrier;
3337
+ #setStartupTurnBarrier(barrier: Promise<void> | undefined): void {
3338
+ this.#startupTurnBarrier = barrier;
3339
+ this.#startupTurnBarrierPending = barrier !== undefined;
3247
3340
  if (!barrier) return;
3248
- await barrier;
3249
- if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
3341
+ void barrier.then(
3342
+ () => {
3343
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
3344
+ },
3345
+ () => {
3346
+ if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
3347
+ },
3348
+ );
3349
+ }
3350
+ async #awaitStartupTurnBarrier(signal?: AbortSignal): Promise<void> {
3351
+ while (true) {
3352
+ const barrier = this.#startupTurnBarrier;
3353
+ if (!barrier) return;
3354
+ await awaitPromptInvocationPreflight(barrier, signal);
3355
+ // Cross one task boundary before clearing readiness. Promise reaction
3356
+ // chains fully drain before timers, so every extension published by the
3357
+ // captured barrier's settlement becomes visible without an arbitrary
3358
+ // microtask-depth cap.
3359
+ const publicationTurn = Promise.withResolvers<void>();
3360
+ const timer = setTimeout(publicationTurn.resolve, 0);
3361
+ try {
3362
+ await awaitPromptInvocationPreflight(publicationTurn.promise, signal);
3363
+ } finally {
3364
+ clearTimeout(timer);
3365
+ }
3366
+ if (this.#startupTurnBarrier !== barrier) continue;
3367
+ this.#setStartupTurnBarrier(undefined);
3368
+ return;
3369
+ }
3370
+ }
3371
+ async #awaitStartupPromptWaiters(signal?: AbortSignal): Promise<void> {
3372
+ while (this.#startupPromptWaiters.size > 0) {
3373
+ await awaitPromptInvocationPreflight(Promise.all([...this.#startupPromptWaiters]), signal);
3374
+ await Promise.resolve();
3375
+ }
3376
+ }
3377
+ #reserveStartupPromptWaiter(): () => void {
3378
+ const waiter = Promise.withResolvers<void>();
3379
+ this.#startupPromptWaiters.add(waiter.promise);
3380
+ let released = false;
3381
+ return () => {
3382
+ if (released) return;
3383
+ released = true;
3384
+ this.#startupPromptWaiters.delete(waiter.promise);
3385
+ waiter.resolve();
3386
+ };
3250
3387
  }
3251
3388
  extendStartupTurnBarrier(barrier: Promise<void>): void {
3252
3389
  const current = this.#startupTurnBarrier;
3253
- this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
3254
- void this.#startupTurnBarrier.catch(() => {});
3390
+ this.#setStartupTurnBarrier(current ? Promise.all([current, barrier]).then(() => {}) : barrier);
3391
+ void this.#startupTurnBarrier?.catch(() => {});
3255
3392
  }
3256
3393
 
3257
3394
  async #withSessionAdmission<T>(
@@ -3263,6 +3400,8 @@ export class AgentSession {
3263
3400
  allowDuringClosing?: boolean;
3264
3401
  bypassSelectionFenceGeneration?: number;
3265
3402
  allowPromptContinuationReentry?: boolean;
3403
+ idleDelivery?: boolean;
3404
+ startupPromptWaiterRelease?: () => void;
3266
3405
  },
3267
3406
  ): Promise<T> {
3268
3407
  const owner = this.#sessionAdmissionContext.getStore();
@@ -3277,7 +3416,21 @@ export class AgentSession {
3277
3416
  }
3278
3417
  throw this.#sessionAdmissionBusyError();
3279
3418
  }
3280
- if (kind === "prompt") await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(), signal);
3419
+ const releaseStartupPromptWaiter =
3420
+ kind === "prompt" && options?.idleDelivery !== true
3421
+ ? (options?.startupPromptWaiterRelease ?? this.#reserveStartupPromptWaiter())
3422
+ : undefined;
3423
+ if (kind === "prompt") {
3424
+ const startupSignal = signal
3425
+ ? AbortSignal.any([signal, this.#disposeAbortController.signal])
3426
+ : this.#disposeAbortController.signal;
3427
+ try {
3428
+ await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(startupSignal), startupSignal);
3429
+ } finally {
3430
+ releaseStartupPromptWaiter?.();
3431
+ }
3432
+ if (options?.idleDelivery === true) await this.#awaitStartupPromptWaiters(startupSignal);
3433
+ }
3281
3434
  const bypassesSelectionFence =
3282
3435
  options?.bypassSelectionFenceGeneration !== undefined &&
3283
3436
  options.bypassSelectionFenceGeneration < this.#selectionFenceGeneration;
@@ -4039,10 +4192,96 @@ export class AgentSession {
4039
4192
  cwd: () => this.sessionManager.getCwd(),
4040
4193
  });
4041
4194
  }
4042
- this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
4043
- sessionId: this.sessionId,
4044
- cwd: this.sessionManager.getCwd(),
4045
- sessionFile: this.sessionManager.getSessionFile(),
4195
+ this.#registerRuntimeStateFinalizer();
4196
+ this.#unregisterBeforeMoveListener = this.sessionManager.registerBeforeMoveListener(async move => {
4197
+ const admittedBeforeBarrier = this.#beginCoordinatorRescopeBarrier();
4198
+ await Promise.allSettled([
4199
+ ...admittedBeforeBarrier.eventHandlers,
4200
+ ...admittedBeforeBarrier.releasedBarrierPersists,
4201
+ ]);
4202
+ this.#bindPendingAgentEndToRescopeBarrier();
4203
+ await this.#drainUnbarrieredCoordinatorPersists();
4204
+ this.#coordinatorPersistGeneration += 1;
4205
+ const moveId = crypto.randomUUID();
4206
+ this.#coordinatorRescopeMoveId = moveId;
4207
+ await prepareCoordinatorRuntimeStateRescope({
4208
+ moveId,
4209
+ sessionId: this.sessionId,
4210
+ previousCwd: move.previousCwd,
4211
+ newCwd: move.newCwd,
4212
+ previousCwdIdentity: move.previousCwdIdentity,
4213
+ newCwdIdentity: move.newCwdIdentity,
4214
+ previousSessionFile: move.previousSessionFile ?? null,
4215
+ newSessionFile: move.newSessionFile ?? null,
4216
+ });
4217
+ });
4218
+ this.#unregisterMoveAbortListener = this.sessionManager.registerMoveAbortListener(async move => {
4219
+ const moveId = this.#coordinatorRescopeMoveId;
4220
+ try {
4221
+ if (moveId && !move.preserveRecoveryJournal) {
4222
+ await clearCoordinatorRuntimeStateRescope(
4223
+ {
4224
+ sessionId: this.sessionId,
4225
+ cwd: move.newCwd,
4226
+ sessionFile: move.newSessionFile ?? null,
4227
+ },
4228
+ moveId,
4229
+ move.previousCwd,
4230
+ );
4231
+ }
4232
+ } finally {
4233
+ this.#coordinatorRescopeMoveId = undefined;
4234
+ this.#endCoordinatorRescopeBarrier();
4235
+ }
4236
+ });
4237
+ this.#unregisterMovePublicationListener = this.sessionManager.registerMovePublicationListener(async move => {
4238
+ const moveId = this.#coordinatorRescopeMoveId;
4239
+ if (!moveId) throw new Error("Coordinator rescope publication has no prepared move identity.");
4240
+ await markCoordinatorRuntimeStateRescopePublishing(
4241
+ {
4242
+ sessionId: this.sessionId,
4243
+ cwd: move.newCwd,
4244
+ sessionFile: move.newSessionFile ?? null,
4245
+ },
4246
+ move.previousCwd,
4247
+ moveId,
4248
+ );
4249
+ });
4250
+ // Every committed rescope (`move_session`, `/move`, SDK/ACP `session.cwd.move`) funnels
4251
+ // through SessionManager.moveTo, so one after-move listener covers all surfaces: it
4252
+ // relocates the coordinator-shared runtime state to the new cwd (otherwise the identity
4253
+ // fence refuses every later persist) and rebinds the postmortem finalizer, which
4254
+ // otherwise keeps writing terminal state to the launch root's cwd/session file.
4255
+ this.#unregisterAfterMoveListener = this.sessionManager.registerAfterMoveListener(async move => {
4256
+ let completed = false;
4257
+ try {
4258
+ const relocated = await relocateCoordinatorRuntimeStateForRescope(
4259
+ {
4260
+ sessionId: this.sessionId,
4261
+ cwd: move.newCwd,
4262
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4263
+ previousSessionFile: move.previousSessionFile ?? null,
4264
+ },
4265
+ move.previousCwd,
4266
+ );
4267
+ if (!relocated) throw new Error("Coordinator runtime state rescope was refused.");
4268
+ await clearCoordinatorRuntimeStateRescope(
4269
+ {
4270
+ sessionId: this.sessionId,
4271
+ cwd: move.newCwd,
4272
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4273
+ },
4274
+ this.#coordinatorRescopeMoveId,
4275
+ move.previousCwd,
4276
+ );
4277
+ completed = true;
4278
+ } finally {
4279
+ this.#registerRuntimeStateFinalizer();
4280
+ if (completed) {
4281
+ this.#coordinatorRescopeMoveId = undefined;
4282
+ this.#endCoordinatorRescopeBarrier();
4283
+ }
4284
+ }
4046
4285
  });
4047
4286
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
4048
4287
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
@@ -4051,7 +4290,19 @@ export class AgentSession {
4051
4290
  this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
4052
4291
  this.#retainedMemorySampler = config.retainedMemorySampler;
4053
4292
  this.#ownedMcpManager = config.ownedMcpManager;
4054
- this.#startupTurnBarrier = config.startupTurnBarrier;
4293
+ this.#setStartupTurnBarrier(config.startupTurnBarrier);
4294
+ // Only arm the recovery barrier when a rescope journal is actually pending. Arming it
4295
+ // unconditionally flips #startupTurnBarrier from absent to a (resolved) promise for
4296
+ // every session, which makes barrier-gated post-turn work (e.g. the hidden-next-turn
4297
+ // drain) take an extra scheduler hop and reorder against terminal-abort requeue.
4298
+ const rescopeRecoveryContext = {
4299
+ sessionId: this.sessionId,
4300
+ cwd: this.sessionManager.getCwd(),
4301
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4302
+ };
4303
+ if (hasCoordinatorRuntimeStateRescopeJournal(rescopeRecoveryContext)) {
4304
+ this.extendStartupTurnBarrier(recoverCoordinatorRuntimeStateRescope(rescopeRecoveryContext));
4305
+ }
4055
4306
  this.#scopedModels = config.scopedModels ?? [];
4056
4307
  this.#thinkingLevel = config.thinkingLevel;
4057
4308
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -4139,7 +4390,7 @@ export class AgentSession {
4139
4390
  // agent.prompt directly, allocates right before admission.
4140
4391
  this.agent.followUp(message);
4141
4392
  },
4142
- injectIdle: async messages => {
4393
+ injectIdle: async (messages, signal) => {
4143
4394
  // Mandated boundary comment (corrected turn semantics): same origin
4144
4395
  // split as the streaming injector — an allowed owned-completion
4145
4396
  // delivery starts a fresh turn attempt/lineage and is not a
@@ -4153,37 +4404,43 @@ export class AgentSession {
4153
4404
  if (dropped.length > 0) this.#settleDeliveredOwnedRegistrations(dropped);
4154
4405
  const first = survivors[0];
4155
4406
  if (!first) return;
4156
- await this.#awaitStartupTurnBarrier();
4157
- if (this.#isDisposed) return;
4158
- // A user prompt may have started during the barrier/scheduling
4159
- // delay: if the session is now streaming, mutating the epoch and
4160
- // lineage here would corrupt the ACTIVE user turn (and
4161
- // agent.prompt would then reject as busy, losing the drained
4162
- // completion). Route the survivors through followUp — the
4163
- // streaming injector's path — which allocates the fresh resume
4164
- // lineage at actual admission (review thread P1).
4165
- if (this.isStreaming) {
4166
- for (const message of survivors) this.agent.followUp(message);
4167
- return;
4168
- }
4169
- if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
4170
- this.#resumeFromOwnedCompletion();
4407
+ const settleIfDisposing = (): boolean => {
4408
+ if (!this.#isDisposed && !this.#sessionAdmissionClosing && !this.#disposeAbortController.signal.aborted)
4409
+ return false;
4410
+ this.#settleDeliveredOwnedRegistrations(survivors);
4411
+ return true;
4412
+ };
4171
4413
  try {
4172
- if (survivors.length === 1) {
4173
- await this.agent.prompt(first, {
4174
- ...this.#managedFallbackPromptOptions(),
4175
- onRunAccepted: (handle: AttemptRunHandle) => {
4176
- if (handle) this.#acceptSdkAttemptRun(handle);
4177
- },
4178
- });
4179
- } else {
4180
- await this.agent.prompt(survivors, {
4181
- ...this.#managedFallbackPromptOptions(),
4182
- onRunAccepted: (handle: AttemptRunHandle) => {
4183
- if (handle) this.#acceptSdkAttemptRun(handle);
4184
- },
4185
- });
4186
- }
4414
+ await this.#withSessionAdmission(
4415
+ "prompt",
4416
+ async () => {
4417
+ if (settleIfDisposing()) return;
4418
+ if (this.isStreaming) {
4419
+ await awaitPromptInvocationPreflight(this.agent.waitForIdle(), signal);
4420
+ if (settleIfDisposing()) return;
4421
+ }
4422
+ if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
4423
+ this.#resumeFromOwnedCompletion();
4424
+ if (survivors.length === 1) {
4425
+ await this.agent.prompt(first, {
4426
+ ...this.#managedFallbackPromptOptions(),
4427
+ onRunAccepted: (handle: AttemptRunHandle) => {
4428
+ if (handle) this.#acceptSdkAttemptRun(handle);
4429
+ },
4430
+ });
4431
+ } else {
4432
+ await this.agent.prompt(survivors, {
4433
+ ...this.#managedFallbackPromptOptions(),
4434
+ onRunAccepted: (handle: AttemptRunHandle) => {
4435
+ if (handle) this.#acceptSdkAttemptRun(handle);
4436
+ },
4437
+ });
4438
+ }
4439
+ },
4440
+ signal,
4441
+ undefined,
4442
+ { idleDelivery: true },
4443
+ );
4187
4444
  } finally {
4188
4445
  // The owned completions were delivered OR the prompt attempt
4189
4446
  // failed (e.g. provider rejection): either way the yield
@@ -4195,16 +4452,19 @@ export class AgentSession {
4195
4452
  this.#settleDeliveredOwnedRegistrations(survivors);
4196
4453
  }
4197
4454
  },
4198
- scheduleIdleFlush: run => {
4455
+ scheduleIdleFlush: (run, onSkip) => {
4456
+ // The startup barrier already gates injectIdle, so begin waiting on a
4457
+ // pending barrier immediately. Once readiness has settled, ordinary
4458
+ // idle wakes retain the fixed merge window.
4459
+ const delayMs = this.#startupTurnBarrierPending ? 0 : FOLD_WAKE_MERGE_WINDOW_MS;
4199
4460
  this.#schedulePostPromptTask(
4200
- async () => {
4201
- await run();
4461
+ async signal => {
4462
+ await run(signal);
4202
4463
  },
4203
- // One merge window, so staggered completions share a single wake
4204
- // turn instead of each buying its own.
4205
- { delayMs: FOLD_WAKE_MERGE_WINDOW_MS },
4464
+ { delayMs, onSkip, excludeFromPostPromptRecovery: true },
4206
4465
  );
4207
4466
  },
4467
+ getIdleFlushSignal: () => this.#postPromptTasksAbortController.signal,
4208
4468
  });
4209
4469
  this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
4210
4470
  // Stop-after-result, never abort: a fold arms this once and the loop ends the
@@ -5337,7 +5597,7 @@ export class AgentSession {
5337
5597
  #coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
5338
5598
  #agentEventAdmission = new WeakMap<
5339
5599
  object,
5340
- { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number }
5600
+ { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
5341
5601
  >();
5342
5602
 
5343
5603
  /**
@@ -5398,6 +5658,7 @@ export class AgentSession {
5398
5658
  scope: this.#activeAttemptScope,
5399
5659
  sdkRunToken: this.#activeSdkRunToken,
5400
5660
  persistGeneration: this.#coordinatorPersistGeneration,
5661
+ persistBarrier: this.#coordinatorRescopeBarrier,
5401
5662
  });
5402
5663
  // First statement of the listener: the observation must precede every claim,
5403
5664
  // reservation, and async hop this handler performs.
@@ -5507,6 +5768,11 @@ export class AgentSession {
5507
5768
  this.#resolveSessionSettlement();
5508
5769
  }
5509
5770
  })();
5771
+ this.#coordinatorEventHandlers.add(handler);
5772
+ void handler.then(
5773
+ () => this.#coordinatorEventHandlers.delete(handler),
5774
+ () => this.#coordinatorEventHandlers.delete(handler),
5775
+ );
5510
5776
  if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
5511
5777
  return handler;
5512
5778
  };
@@ -5530,15 +5796,39 @@ export class AgentSession {
5530
5796
  message: `${kind} ${outcome.status} for ${correlation}${safeError ? `: ${safeError}` : ""}`,
5531
5797
  });
5532
5798
  }
5533
- const persist = () =>
5534
- persistCoordinatorWorkerIntegrationOutcome(context, {
5535
- ...outcome,
5536
- kind,
5537
- correlationId,
5538
- error: sanitizePostPublicationError(outcome.error),
5539
- });
5540
- const queued = this.#coordinatorPersistQueue.then(persist, persist);
5541
- this.#coordinatorPersistQueue = queued.catch(() => {});
5799
+ const generation = this.#coordinatorPersistGeneration;
5800
+ const barrier = this.#coordinatorRescopeBarrier;
5801
+ const persist = async () => {
5802
+ if (barrier) {
5803
+ await barrier;
5804
+ return await persistCoordinatorWorkerIntegrationOutcome(
5805
+ {
5806
+ sessionId: this.sessionId,
5807
+ cwd: this.sessionManager.getCwd(),
5808
+ sessionFile: this.sessionManager.getSessionFile(),
5809
+ },
5810
+ {
5811
+ ...outcome,
5812
+ kind,
5813
+ correlationId,
5814
+ error: sanitizePostPublicationError(outcome.error),
5815
+ },
5816
+ );
5817
+ }
5818
+ return generation === this.#coordinatorPersistGeneration
5819
+ ? await persistCoordinatorWorkerIntegrationOutcome(context, {
5820
+ ...outcome,
5821
+ kind,
5822
+ correlationId,
5823
+ error: sanitizePostPublicationError(outcome.error),
5824
+ })
5825
+ : undefined;
5826
+ };
5827
+ const queued = barrier
5828
+ ? barrier.then(() => this.#appendCoordinatorPersist(persist))
5829
+ : this.#appendCoordinatorPersist(persist);
5830
+ if (barrier) this.#trackReleasedBarrierPersist(queued);
5831
+ else this.#trackUnbarrieredCoordinatorPersist(queued);
5542
5832
  void queued.catch(error =>
5543
5833
  logger.warn("Failed to persist terminal reconciliation outcome", { error: String(error) }),
5544
5834
  );
@@ -5559,18 +5849,38 @@ export class AgentSession {
5559
5849
  #queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
5560
5850
  if (isNonDispatchedToolEvent(event)) return Promise.resolve();
5561
5851
  const observation = this.#coordinatorToolObservations.get(event);
5852
+ const admission = this.#agentEventAdmission.get(event);
5853
+ const barrier = admission?.persistBarrier;
5854
+ if (barrier) {
5855
+ const run = async () => {
5856
+ await barrier;
5857
+ await this.#persistRuntimeStateInBackground(
5858
+ event,
5859
+ {
5860
+ sessionId: this.sessionId,
5861
+ cwd: this.sessionManager.getCwd(),
5862
+ sessionFile: this.sessionManager.getSessionFile(),
5863
+ },
5864
+ observation,
5865
+ propagateFailure,
5866
+ );
5867
+ };
5868
+ const queued = barrier.then(() => this.#appendCoordinatorPersist(run));
5869
+ this.#trackReleasedBarrierPersist(queued);
5870
+ return queued;
5871
+ }
5562
5872
  const context = {
5563
5873
  sessionId: this.sessionId,
5564
5874
  cwd: this.sessionManager.getCwd(),
5565
5875
  sessionFile: this.sessionManager.getSessionFile(),
5566
5876
  };
5567
- const generation = this.#agentEventAdmission.get(event)?.persistGeneration ?? this.#coordinatorPersistGeneration;
5877
+ const generation = admission?.persistGeneration ?? this.#coordinatorPersistGeneration;
5568
5878
  const run = () =>
5569
5879
  generation === this.#coordinatorPersistGeneration
5570
5880
  ? this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure)
5571
5881
  : Promise.resolve();
5572
- const queued = this.#coordinatorPersistQueue.then(run, run);
5573
- this.#coordinatorPersistQueue = queued.catch(() => {});
5882
+ const queued = this.#appendCoordinatorPersist(run);
5883
+ this.#trackUnbarrieredCoordinatorPersist(queued);
5574
5884
  return queued;
5575
5885
  }
5576
5886
 
@@ -6530,9 +6840,11 @@ export class AgentSession {
6530
6840
  });
6531
6841
  }
6532
6842
  this.#resolveRetry();
6843
+ if (this.#isDisposed || this.#sessionAdmissionClosing) return;
6533
6844
 
6534
6845
  const compactionTask = this.#schedulePostPromptTask(
6535
6846
  async () => {
6847
+ if (this.#isDisposed || this.#sessionAdmissionClosing) return;
6536
6848
  await this.#checkCompaction(msg, true, undefined, activePromptHandle);
6537
6849
  },
6538
6850
  { resourceRunId: activePromptHandle },
@@ -6605,9 +6917,11 @@ export class AgentSession {
6605
6917
  selectionFenceGeneration: number,
6606
6918
  lease?: RunResourceProducerLease,
6607
6919
  leaseTask: Promise<void> = task,
6920
+ excludeFromRecovery = false,
6608
6921
  ): void {
6609
6922
  this.#postPromptTasks.add(task);
6610
6923
  this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
6924
+ if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
6611
6925
  this.#ensurePostPromptTasksPromise();
6612
6926
  if (lease) {
6613
6927
  lease.track("post_prompt", "agent-session", leaseTask);
@@ -6618,6 +6932,7 @@ export class AgentSession {
6618
6932
  .finally(() => {
6619
6933
  this.#postPromptTasks.delete(task);
6620
6934
  this.#postPromptTaskSelectionFenceGenerations.delete(task);
6935
+ this.#postPromptTaskRecoveryExcluded.delete(task);
6621
6936
  if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
6622
6937
  });
6623
6938
  }
@@ -6631,6 +6946,7 @@ export class AgentSession {
6631
6946
  resourceRunId?: string;
6632
6947
  leaseTask?: Promise<void>;
6633
6948
  selectionFenceGeneration?: number;
6949
+ excludeFromPostPromptRecovery?: boolean;
6634
6950
  },
6635
6951
  ): Promise<void> {
6636
6952
  const selectionFenceGeneration =
@@ -6657,7 +6973,11 @@ export class AgentSession {
6657
6973
  options?.onSkip?.();
6658
6974
  return Promise.resolve();
6659
6975
  }
6660
- const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
6976
+ const taskSignal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
6977
+ // Disposal is a session-wide terminal fence. Compose it with the selected
6978
+ // logical-resource signal so stale leases from earlier runs cannot keep a
6979
+ // post-prompt task alive after the current run was quarantined.
6980
+ const signal = AbortSignal.any([taskSignal, this.#disposeAbortController.signal]);
6661
6981
  const runScheduled = async () => {
6662
6982
  if (delayMs > 0) {
6663
6983
  try {
@@ -6685,6 +7005,7 @@ export class AgentSession {
6685
7005
  selectionFenceGeneration,
6686
7006
  reservation?.ok ? reservation.lease : undefined,
6687
7007
  options?.leaseTask,
7008
+ options?.excludeFromPostPromptRecovery,
6688
7009
  );
6689
7010
  return scheduled;
6690
7011
  }
@@ -6818,7 +7139,7 @@ export class AgentSession {
6818
7139
  settleLease();
6819
7140
  return;
6820
7141
  }
6821
- await this.#awaitStartupTurnBarrier();
7142
+ await this.#awaitStartupTurnBarrier(scheduledSignal);
6822
7143
  if (!canContinue()) {
6823
7144
  settleLease();
6824
7145
  return;
@@ -7222,6 +7543,7 @@ export class AgentSession {
7222
7543
  this.#postPromptTasksAbortController = new AbortController();
7223
7544
  this.#postPromptTasks.clear();
7224
7545
  this.#postPromptTaskSelectionFenceGenerations.clear();
7546
+ this.#postPromptTaskRecoveryExcluded.clear();
7225
7547
  this.#releaseDeferredAgentEndContinuations();
7226
7548
  this.#resolveTtsrResume();
7227
7549
  this.#resolvePostPromptTasks();
@@ -7243,8 +7565,11 @@ export class AgentSession {
7243
7565
  await this.#ttsrResumePromise;
7244
7566
  continue;
7245
7567
  }
7246
- if (this.#postPromptTasksPromise) {
7247
- await this.#postPromptTasksPromise;
7568
+ const recoveryTasks = [...this.#postPromptTasks].filter(
7569
+ task => !this.#postPromptTaskRecoveryExcluded.has(task),
7570
+ );
7571
+ if (recoveryTasks.length > 0) {
7572
+ await Promise.allSettled(recoveryTasks);
7248
7573
  continue;
7249
7574
  }
7250
7575
  // Tracked post-prompt tasks cover deferred continuations scheduled from
@@ -7266,7 +7591,9 @@ export class AgentSession {
7266
7591
  */
7267
7592
  async #waitForPostPromptTasksBeforeSelectionFence(selectionFenceGeneration: number): Promise<void> {
7268
7593
  const precedingTasks = [...this.#postPromptTasks].filter(
7269
- task => (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
7594
+ task =>
7595
+ !this.#postPromptTaskRecoveryExcluded.has(task) &&
7596
+ (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
7270
7597
  );
7271
7598
  if (precedingTasks.length > 0) await Promise.allSettled(precedingTasks);
7272
7599
  }
@@ -8388,13 +8715,35 @@ export class AgentSession {
8388
8715
  if (this.#disposePromise) return this.#disposePromise;
8389
8716
  const { promise, resolve, reject } = Promise.withResolvers<void>();
8390
8717
  this.#disposePromise = promise;
8718
+ this.#abortAdmissionEpoch++;
8719
+ this.#isDisposed = true;
8720
+ this.#disposeAbortController.abort();
8721
+ this.#disposeAdmissionClosed = this.#closeSessionAdmission();
8722
+ this.#disposePostPromptDrain = this.#cancelPostPromptTasks();
8723
+ this.#settleDeliveredOwnedRegistrations(this.#pendingNextTurnMessages.map(entry => entry.message));
8724
+ this.#pendingNextTurnMessages = [];
8725
+ this.#scheduledHiddenNextTurnGeneration = undefined;
8726
+ this.#abortActiveMidRunBarriers();
8727
+ this.abortCompaction();
8728
+ this.abortRetry();
8729
+ this.#quarantineAgentRunResources();
8730
+ this.agent.abort();
8731
+ this.agent.setMainAttemptScopeObserver(undefined);
8732
+ this.#disconnectFromAgent();
8391
8733
  void this.#dispose().then(resolve, reject);
8392
8734
  return promise;
8393
8735
  }
8394
8736
 
8737
+ /** Cancel the active logical run domain, including managed continuations detached from Agent's active attempt. */
8738
+ #quarantineAgentRunResources(): void {
8739
+ const resourceRunId = this.agent.currentManagedLogicalRunId ?? this.agent.activeResourceRunId;
8740
+ if (resourceRunId !== undefined) this.agent.resourceLedger.quarantine(String(resourceRunId));
8741
+ }
8742
+
8395
8743
  async #dispose(): Promise<void> {
8396
- await this.sessionManager.joinCwdTransition();
8397
- const admissionClosed = this.#closeSessionAdmission();
8744
+ const admissionClosed = this.#disposeAdmissionClosed ?? this.#closeSessionAdmission();
8745
+ await admissionClosed;
8746
+ await this.sessionManager.closeCwdMoveAdmission();
8398
8747
  this.#isDisposed = true;
8399
8748
  // Reject new direct Python starts as soon as disposal begins (synchronously,
8400
8749
  // before any await) so callers cannot race a start against teardown.
@@ -8432,18 +8781,22 @@ export class AgentSession {
8432
8781
  this.agent.forceAbort("Session disposed");
8433
8782
  }
8434
8783
  }
8435
- await admissionClosed;
8436
8784
  await this.#agentEndPublicationPromise;
8437
8785
  await this.#queuedExtensionEvents;
8786
+ await this.#agentEndHandlingPromise;
8438
8787
  // Drain the sidecar write order for the same reason the two queues above are
8439
8788
  // drained: each entry writes under the native identity-bound state-file lock, so a
8440
- // still-queued write would run after the session that owns it is gone — releasing
8789
+ // a still-queued write would run after the session that owns it is gone — releasing
8441
8790
  // a lock whose owner no longer exists, and under `bun test --isolate` calling into
8442
8791
  // the addon after the runtime tore down the context it was scheduled in. The chain
8443
8792
  // is already failure-absorbing, so this only waits.
8444
8793
  await this.#coordinatorPersistQueue;
8794
+ // Terminal publication records its reconciliation from a promise reaction, which
8795
+ // can append after the queue snapshot above was awaited. Drain the tracked writes
8796
+ // again so disposal cannot return while that late reconciliation still owns a lock.
8797
+ await this.#drainUnbarrieredCoordinatorPersists();
8445
8798
  this.#pendingBackgroundExchanges = [];
8446
- this.yieldQueue.clear();
8799
+ this.#drainTerminalOwnedYieldEntries();
8447
8800
 
8448
8801
  this.agent.setOnBeforeYield(undefined);
8449
8802
  try {
@@ -8457,12 +8810,25 @@ export class AgentSession {
8457
8810
  this.#workflowGateEmitter = undefined;
8458
8811
  this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
8459
8812
  await this.#flushWorkerIntegrationAttempt();
8460
- await this.#cancelPostPromptTasks();
8813
+ // Worker integration completion can enqueue terminal reconciliation while the
8814
+ // flush above is pending; drain that late producer before disposal resolves.
8815
+ await this.#coordinatorPersistQueue;
8816
+ await this.#drainUnbarrieredCoordinatorPersists();
8817
+ await (this.#disposePostPromptDrain ?? this.#cancelPostPromptTasks());
8461
8818
  // Cancel jobs this agent registered so a subagent's teardown doesn't
8462
8819
  // leak its background bash/task work into the parent's manager. Only
8463
8820
  // the session that owns the manager goes on to dispose it (which itself
8464
8821
  // nukes any leftover jobs and pending deliveries).
8465
8822
  this.#cancelOwnAsyncJobs();
8823
+ // Final drain (Codex review P2 on #5088): an async-job completion that
8824
+ // enqueued during the awaited session_shutdown window above survived the
8825
+ // first drain with a retained delivery claim, and the already-aborted
8826
+ // disposal signal makes its idle-flush task exit before YieldQueue.flush
8827
+ // can run. Producers are now cancelled, so drain once more to settle any
8828
+ // straggler envelope and release its claim before owned-manager teardown
8829
+ // — a shared manager would otherwise keep the claim past this session's
8830
+ // disposal.
8831
+ this.#drainTerminalOwnedYieldEntries();
8466
8832
  await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
8467
8833
  const ownedAsyncManager = this.#ownedAsyncJobManager;
8468
8834
  if (ownedAsyncManager && this.#disposeAsyncJobManager) {
@@ -8499,6 +8865,14 @@ export class AgentSession {
8499
8865
  this.#unregisterSessionMemorySettings = undefined;
8500
8866
  if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
8501
8867
  this.#unregisterRuntimeStateFinalizer = undefined;
8868
+ this.#unregisterBeforeMoveListener?.();
8869
+ this.#unregisterBeforeMoveListener = undefined;
8870
+ this.#unregisterMoveAbortListener?.();
8871
+ this.#unregisterMoveAbortListener = undefined;
8872
+ this.#unregisterMovePublicationListener?.();
8873
+ this.#unregisterMovePublicationListener = undefined;
8874
+ this.#unregisterAfterMoveListener?.();
8875
+ this.#unregisterAfterMoveListener = undefined;
8502
8876
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
8503
8877
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
8504
8878
  );
@@ -8663,14 +9037,24 @@ export class AgentSession {
8663
9037
  await this.#waitForPostPromptTasksBeforeSelectionFence(ignoreSelectionFenceGeneration);
8664
9038
  }
8665
9039
  await this.#waitForSessionSettlement(ignoreSelectionFenceGeneration);
9040
+ const hasBlockingPostPromptTasks =
9041
+ ignoreSelectionFenceGeneration === undefined
9042
+ ? this.#postPromptTasks.size > 0
9043
+ : [...this.#postPromptTasks].some(
9044
+ task =>
9045
+ !this.#postPromptTaskRecoveryExcluded.has(task) &&
9046
+ (this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < ignoreSelectionFenceGeneration,
9047
+ );
8666
9048
  if (
8667
9049
  !this.agent.state.isStreaming &&
8668
9050
  !this.#retryPromise &&
8669
9051
  !this.#ttsrResumePromise &&
8670
- !this.#postPromptTasksPromise &&
9052
+ !hasBlockingPostPromptTasks &&
8671
9053
  !this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
8672
9054
  )
8673
9055
  return;
9056
+ if (ignoreSelectionFenceGeneration === undefined && hasBlockingPostPromptTasks && this.#postPromptTasksPromise)
9057
+ await this.#postPromptTasksPromise;
8674
9058
  }
8675
9059
  }
8676
9060
 
@@ -8695,6 +9079,35 @@ export class AgentSession {
8695
9079
  await this.sessionManager.flush();
8696
9080
  }
8697
9081
 
9082
+ queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
9083
+ this.#agentEventAdmission.set(event, {
9084
+ persistGeneration: this.#coordinatorPersistGeneration,
9085
+ persistBarrier: this.#coordinatorRescopeBarrier,
9086
+ });
9087
+ const task = (async () => {
9088
+ await gate;
9089
+ await this.#queueCoordinatorRuntimeStatePersist(event, true);
9090
+ })();
9091
+ this.#coordinatorEventHandlers.add(task);
9092
+ void task.then(
9093
+ () => this.#coordinatorEventHandlers.delete(task),
9094
+ () => this.#coordinatorEventHandlers.delete(task),
9095
+ );
9096
+ return task;
9097
+ }
9098
+
9099
+ parkAgentEndForCoordinatorPersistForTests(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
9100
+ this.#agentEventAdmission.set(event, {
9101
+ persistGeneration: this.#coordinatorPersistGeneration,
9102
+ persistBarrier: this.#coordinatorRescopeBarrier,
9103
+ });
9104
+ this.#pendingAgentEndEmit = event;
9105
+ }
9106
+
9107
+ flushParkedAgentEndForCoordinatorPersistForTests(): void {
9108
+ this.#flushPendingAgentEnd();
9109
+ }
9110
+
8698
9111
  async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
8699
9112
  const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
8700
9113
  if (!manager) return false;
@@ -10924,6 +11337,19 @@ export class AgentSession {
10924
11337
  * @throws Error if no model selected or no API key available (when not streaming)
10925
11338
  */
10926
11339
  async prompt(text: string, options?: PromptOptions): Promise<void> {
11340
+ const releaseStartupPromptWaiter = this.#reserveStartupPromptWaiter();
11341
+ try {
11342
+ await this.#promptInternal(text, options, releaseStartupPromptWaiter);
11343
+ } finally {
11344
+ releaseStartupPromptWaiter();
11345
+ }
11346
+ }
11347
+
11348
+ async #promptInternal(
11349
+ text: string,
11350
+ options: PromptOptions | undefined,
11351
+ releaseStartupPromptWaiter: () => void,
11352
+ ): Promise<void> {
10927
11353
  const hasUsableImage =
10928
11354
  options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
10929
11355
  if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
@@ -11069,7 +11495,12 @@ export class AgentSession {
11069
11495
  attribution: promptAttribution,
11070
11496
  timestamp: Date.now(),
11071
11497
  }
11072
- : { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
11498
+ : {
11499
+ role: "user" as const,
11500
+ content: userContent,
11501
+ attribution: promptAttribution,
11502
+ timestamp: Date.now(),
11503
+ };
11073
11504
  if (deepInterviewUserIntentEpoch !== undefined)
11074
11505
  this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
11075
11506
  await this.refreshGjcSubskillTools();
@@ -11097,6 +11528,8 @@ export class AgentSession {
11097
11528
  }
11098
11529
  },
11099
11530
  options?.preflightSignal,
11531
+ undefined,
11532
+ { startupPromptWaiterRelease: releaseStartupPromptWaiter },
11100
11533
  );
11101
11534
  }
11102
11535
 
@@ -12220,7 +12653,16 @@ export class AgentSession {
12220
12653
  }
12221
12654
  this.#scheduledHiddenNextTurnGeneration = generation;
12222
12655
  this.#schedulePostPromptTask(
12223
- async () => {
12656
+ async signal => {
12657
+ if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
12658
+ if (this.#startupTurnBarrier) {
12659
+ try {
12660
+ await this.#awaitStartupTurnBarrier(signal);
12661
+ } catch {
12662
+ return;
12663
+ }
12664
+ }
12665
+ if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
12224
12666
  if (this.#scheduledHiddenNextTurnGeneration === generation) {
12225
12667
  this.#scheduledHiddenNextTurnGeneration = undefined;
12226
12668
  }
@@ -12249,8 +12691,12 @@ export class AgentSession {
12249
12691
  return;
12250
12692
  }
12251
12693
  try {
12252
- await this.#promptQueuedHiddenNextTurnMessages();
12694
+ await this.#promptQueuedHiddenNextTurnMessages(signal);
12253
12695
  } catch {
12696
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12697
+ this.#pendingNextTurnMessages = [];
12698
+ return;
12699
+ }
12254
12700
  // Leave the hidden next-turn messages queued for the next explicit prompt.
12255
12701
  }
12256
12702
  },
@@ -12265,7 +12711,7 @@ export class AgentSession {
12265
12711
  );
12266
12712
  }
12267
12713
 
12268
- async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
12714
+ async #promptQueuedHiddenNextTurnMessages(signal?: AbortSignal): Promise<void> {
12269
12715
  if (this.#pendingNextTurnMessages.length === 0) {
12270
12716
  return;
12271
12717
  }
@@ -12295,6 +12741,14 @@ export class AgentSession {
12295
12741
  if (reclassified.length === 0) {
12296
12742
  return;
12297
12743
  }
12744
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12745
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12746
+ return;
12747
+ }
12748
+ if (signal?.aborted) {
12749
+ this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
12750
+ return;
12751
+ }
12298
12752
  // Allocate a fresh root-turn lineage when the drained batch contains a
12299
12753
  // fresh owned envelope OR an EXTERNAL hidden trigger: an external
12300
12754
  // nextTurn message has no owned envelope, so without this it would
@@ -12316,11 +12770,24 @@ export class AgentSession {
12316
12770
  const textContent = this.#getCustomMessageTextContent(message);
12317
12771
  await this.#syncSkillPromptActiveStateSafely(message, true);
12318
12772
  try {
12773
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12774
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12775
+ return;
12776
+ }
12777
+ if (signal?.aborted) {
12778
+ this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
12779
+ return;
12780
+ }
12319
12781
  await this.#promptWithMessage(message, textContent, {
12320
12782
  prependMessages,
12321
12783
  skipPostPromptRecoveryWait: true,
12784
+ preflightSignal: signal,
12322
12785
  });
12323
12786
  } catch (error) {
12787
+ if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
12788
+ this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
12789
+ return;
12790
+ }
12324
12791
  // Requeue only the SURVIVING reclassified entries: a completion
12325
12792
  // denied by scope:"owned" was settled (dropped) above and must
12326
12793
  // never reach a later prompt — once its scope and now-unoccupied
@@ -12347,19 +12814,55 @@ export class AgentSession {
12347
12814
  // another session's manager (which lacks the same local job id) would
12348
12815
  // see job === undefined and remove a still-live registration (review
12349
12816
  // thread P1).
12350
- const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
12351
12817
  for (const message of messages) {
12352
12818
  const details = (message as { details?: { ownedCompletions?: OwnedCompletionEnvelope[] } }).details;
12353
- for (const envelope of details?.ownedCompletions ?? []) {
12354
- const job = manager?.getJob(envelope.registration.jobId);
12355
- const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
12356
- // Evicted jobs have no live record (job === undefined); terminal
12357
- // statuses settle the registration.
12358
- if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
12359
- unregisterOwnedRegistration(envelope.registration);
12819
+ for (const envelope of details?.ownedCompletions ?? []) this.#settleOwnedCompletionEnvelope(envelope);
12820
+ }
12821
+ }
12822
+
12823
+ #settleOwnedCompletionEnvelope(envelope: OwnedCompletionEnvelope): void {
12824
+ const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
12825
+ const job = manager?.getJob(envelope.registration.jobId);
12826
+ const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
12827
+ // Evicted jobs have no live record (job === undefined); terminal statuses
12828
+ // settle the registration.
12829
+ if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
12830
+ unregisterOwnedRegistration(envelope.registration);
12831
+ }
12832
+ }
12833
+
12834
+ /**
12835
+ * Drop every queued yield entry, releasing its retained delivery claim and
12836
+ * settling terminal owned-completion envelopes so neither outlives this
12837
+ * session's disposal. Claims resolve to the manager that actually retained
12838
+ * them: owned entries via their registration endpoint (subagents inherit a
12839
+ * parent's manager, and the process-global instance may belong to a
12840
+ * different concurrent session), ordinary entries only on the session-OWNED
12841
+ * manager. Disposal runs this twice: before the awaited session_shutdown
12842
+ * emit, and again after producer cancellation — a completion enqueued
12843
+ * inside that window would otherwise survive disposal, its claim retained
12844
+ * on a shared manager and interfering with later delivery teardown (Codex
12845
+ * review P2 on #5088).
12846
+ */
12847
+ #drainTerminalOwnedYieldEntries(): void {
12848
+ this.yieldQueue.clear((kind, entries) => {
12849
+ if (kind !== "async-result") return;
12850
+ const ownedManager = this.#ownedAsyncJobManager;
12851
+ for (const entry of entries) {
12852
+ if (typeof entry !== "object" || entry === null) continue;
12853
+ const record = entry as { generation?: unknown; ownedCompletion?: unknown };
12854
+ if (typeof record.generation === "string" && record.generation !== "") {
12855
+ const envelope = isOwnedCompletionEnvelope(record.ownedCompletion) ? record.ownedCompletion : undefined;
12856
+ const endpointId = envelope?.registration.endpointId;
12857
+ const manager =
12858
+ (endpointId !== undefined ? AsyncJobManager.forEndpoint(endpointId) : undefined) ?? ownedManager;
12859
+ manager?.releaseDeliveryClaim(record.generation);
12860
+ }
12861
+ if (isOwnedCompletionEnvelope(record.ownedCompletion)) {
12862
+ this.#settleOwnedCompletionEnvelope(record.ownedCompletion);
12360
12863
  }
12361
12864
  }
12362
- }
12865
+ });
12363
12866
  }
12364
12867
 
12365
12868
  #getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
@@ -13415,12 +13918,14 @@ export class AgentSession {
13415
13918
  this.#promptPreflightCancellationGeneration++;
13416
13919
  this.#promptPreflightAbortController.abort();
13417
13920
  this.#promptPreflightAbortController = new AbortController();
13921
+ this.#drainTerminalOwnedYieldEntries();
13922
+ const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
13418
13923
  // Abort visibility is per-request: a later real abort must not inherit an
13419
13924
  // earlier silent abort's suppression and swallow the user-visible notice.
13420
13925
  if (options?.silent !== true) this.#silentAbortPending = false;
13421
13926
  // Capture the unwind: the field clears once the first abort settles, and
13422
13927
  // the awaits below must keep watching THIS unwind, not a successor's.
13423
- const sharedUnwind = this.#abortUnwind;
13928
+ const sharedUnwind = Promise.all([this.#abortUnwind, overlappingPostPromptDrain]).then(() => {});
13424
13929
  if (options?.timeoutMs !== undefined) {
13425
13930
  const timeoutMs = Math.max(0, options.timeoutMs);
13426
13931
  const deadline = Date.now() + timeoutMs;
@@ -13451,6 +13956,7 @@ export class AgentSession {
13451
13956
  this.#abortUnwind = unwind.promise;
13452
13957
  try {
13453
13958
  this.#abortOptions(options);
13959
+ this.#drainTerminalOwnedYieldEntries();
13454
13960
  const postPromptDrain = this.#cancelPostPromptTasks();
13455
13961
  const managedLogicalRunId =
13456
13962
  this.#defaultFallbackChain().chain.entries.length > 1 ? this.agent.currentManagedLogicalRunId : undefined;
@@ -19493,11 +19999,15 @@ export class AgentSession {
19493
19999
  }
19494
20000
 
19495
20001
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
20002
+ const attemptAbortEpoch = this.#abortAdmissionEpoch;
20003
+ const attemptCancelled = () =>
20004
+ this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
19496
20005
  const activePromptHandle = this.activePromptHandle;
19497
20006
  const cancellationSignal = activePromptHandle
19498
20007
  ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
19499
20008
  : undefined;
19500
- if (cancellationSignal?.aborted) return { type: "terminal", terminal: { stopReason: "cancelled" } };
20009
+ if (attemptCancelled() || cancellationSignal?.aborted)
20010
+ return { type: "terminal", terminal: { stopReason: "cancelled" } };
19501
20011
  if (outcome.type === "run_terminal") {
19502
20012
  this.#defaultFallbackChain().resetAttemptBudget();
19503
20013
  return { type: "terminal", terminal: { stopReason: outcome.reason } };
@@ -19533,7 +20043,6 @@ export class AgentSession {
19533
20043
  const controllerStateBeforeFailure = controller.snapshotRuntimeState();
19534
20044
  const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
19535
20045
  this.#escapedNonAsciiManagedRetries = 0;
19536
- const abortEpoch = this.#abortAdmissionEpoch;
19537
20046
  if (advanced) {
19538
20047
  const transitionGeneration = ++this.#fallbackTransitionGeneration;
19539
20048
  return {
@@ -19554,7 +20063,9 @@ export class AgentSession {
19554
20063
  !ownership.isCurrent() ||
19555
20064
  ownership.lease.signal.aborted ||
19556
20065
  cancellationSignal?.aborted ||
19557
- this.#abortAdmissionEpoch !== abortEpoch ||
20066
+ this.#isDisposed ||
20067
+ this.#sessionAdmissionClosing ||
20068
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
19558
20069
  this.#fallbackTransitionGeneration !== transitionGeneration;
19559
20070
  if (continuationCancelled) {
19560
20071
  await restoreOwnedTransition();
@@ -19579,7 +20090,7 @@ export class AgentSession {
19579
20090
  "escaped_non_ascii",
19580
20091
  1,
19581
20092
  cancellationSignal,
19582
- abortEpoch,
20093
+ attemptAbortEpoch,
19583
20094
  controllerStateBeforeFailure,
19584
20095
  previousModel,
19585
20096
  previousThinkingLevel,
@@ -19588,7 +20099,9 @@ export class AgentSession {
19588
20099
  );
19589
20100
  if (!switched) {
19590
20101
  if (
19591
- this.#abortAdmissionEpoch !== abortEpoch ||
20102
+ this.#isDisposed ||
20103
+ this.#sessionAdmissionClosing ||
20104
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
19592
20105
  cancellationSignal?.aborted ||
19593
20106
  this.#fallbackTransitionGeneration !== transitionGeneration
19594
20107
  ) {
@@ -19608,6 +20121,15 @@ export class AgentSession {
19608
20121
  });
19609
20122
  return;
19610
20123
  }
20124
+ if (
20125
+ attemptCancelled() ||
20126
+ !ownership.isCurrent() ||
20127
+ ownership.lease.signal.aborted ||
20128
+ this.#fallbackTransitionGeneration !== transitionGeneration
20129
+ ) {
20130
+ await restoreOwnedTransition();
20131
+ return;
20132
+ }
19611
20133
  const continuation = this.agent.continue({
19612
20134
  ...this.#managedFallbackPromptOptions(),
19613
20135
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
@@ -19617,7 +20139,9 @@ export class AgentSession {
19617
20139
  });
19618
20140
  if (
19619
20141
  cancellationSignal?.aborted ||
19620
- this.#abortAdmissionEpoch !== abortEpoch ||
20142
+ this.#isDisposed ||
20143
+ this.#sessionAdmissionClosing ||
20144
+ this.#abortAdmissionEpoch !== attemptAbortEpoch ||
19621
20145
  this.#fallbackTransitionGeneration !== transitionGeneration
19622
20146
  ) {
19623
20147
  await restoreOwnedTransition();
@@ -19636,7 +20160,7 @@ export class AgentSession {
19636
20160
  };
19637
20161
  }
19638
20162
  controller.advance();
19639
- if (this.#abortAdmissionEpoch !== abortEpoch || cancellationSignal?.aborted)
20163
+ if (attemptCancelled() || cancellationSignal?.aborted)
19640
20164
  return { type: "terminal", terminal: { stopReason: "cancelled" } };
19641
20165
  this.#defaultFallbackExhaustedLastTurn = true;
19642
20166
  return this.#managedFallbackExhaustionDecision(
@@ -19648,7 +20172,7 @@ export class AgentSession {
19648
20172
  return {
19649
20173
  type: "retry",
19650
20174
  continuation: async ownership => {
19651
- if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
20175
+ if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
19652
20176
  await this.agent.continue({
19653
20177
  ...this.#managedFallbackPromptOptions(),
19654
20178
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
@@ -19667,7 +20191,7 @@ export class AgentSession {
19667
20191
  return {
19668
20192
  type: "maintenance",
19669
20193
  continuation: async ownership => {
19670
- if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
20194
+ if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
19671
20195
  const resourceRunId = String(ownership.logicalRunId);
19672
20196
  const previousLease = this.#postPromptLeases.get(resourceRunId);
19673
20197
  this.#postPromptLeases.set(resourceRunId, ownership.lease);
@@ -19686,7 +20210,13 @@ export class AgentSession {
19686
20210
  resourceRunId,
19687
20211
  ownership.lease.signal,
19688
20212
  );
19689
- if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
20213
+ if (
20214
+ terminalized ||
20215
+ successorScheduled ||
20216
+ attemptCancelled() ||
20217
+ !ownership.isCurrent() ||
20218
+ ownership.lease.signal.aborted
20219
+ )
19690
20220
  return;
19691
20221
  this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
19692
20222
  stopReason: "error",
@@ -20129,6 +20659,9 @@ export class AgentSession {
20129
20659
  scope?: AttemptScope,
20130
20660
  scopeWasClean = this.#isRetryScopeClean(scope),
20131
20661
  ): Promise<boolean | ManagedAttemptDecision> {
20662
+ const retryAbortEpoch = this.#abortAdmissionEpoch;
20663
+ const retryCancelled = () =>
20664
+ this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
20132
20665
  const controller = this.#defaultFallbackChain();
20133
20666
  const managedFallback = controller.chain.entries.length > 1;
20134
20667
  const retrySettings = this.settings.getGroup("retry");
@@ -20140,6 +20673,9 @@ export class AgentSession {
20140
20673
  const classification = this.#classifyErrorForRetry(message);
20141
20674
  const localSnapshot = classification === "local_snapshot";
20142
20675
  const localBufferOverflow = classification === "local_buffer_overflow";
20676
+ if (retryCancelled()) {
20677
+ return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
20678
+ }
20143
20679
  // A local machinery failure must never stay charged against the provider
20144
20680
  // fallback budget, no matter which local exit follows (disabled retry,
20145
20681
  // visible-content surface, bounded retry, exhaustion, or the immediate
@@ -20402,7 +20938,7 @@ export class AgentSession {
20402
20938
  ownership?.domain.signal ??
20403
20939
  (activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
20404
20940
  if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
20405
- const abortEpoch = this.#abortAdmissionEpoch;
20941
+ if (retryCancelled()) return;
20406
20942
  let quotaPoolExhausted = false;
20407
20943
  if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
20408
20944
  const mark = await this.#markFailedCredential(trigger);
@@ -20436,7 +20972,7 @@ export class AgentSession {
20436
20972
  trigger.class,
20437
20973
  attemptsUsed,
20438
20974
  cancellationSignal,
20439
- abortEpoch,
20975
+ retryAbortEpoch,
20440
20976
  controllerStateBeforeAdvance,
20441
20977
  previousModel,
20442
20978
  previousThinkingLevel,
@@ -20446,7 +20982,7 @@ export class AgentSession {
20446
20982
  }
20447
20983
  }
20448
20984
  if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
20449
- if (this.#abortAdmissionEpoch === abortEpoch) {
20985
+ if (!retryCancelled()) {
20450
20986
  await this.#restoreDefaultFallbackTransition(
20451
20987
  controller,
20452
20988
  controllerStateBeforeAdvance,
@@ -20456,7 +20992,7 @@ export class AgentSession {
20456
20992
  }
20457
20993
  return;
20458
20994
  }
20459
- if (this.#abortAdmissionEpoch !== abortEpoch) {
20995
+ if (retryCancelled()) {
20460
20996
  controller.resetForNewTurn();
20461
20997
  return;
20462
20998
  }
@@ -20585,8 +21121,9 @@ export class AgentSession {
20585
21121
 
20586
21122
  if (managedOutcome) {
20587
21123
  try {
21124
+ if (retryCancelled() || ownershipCancelled()) return;
20588
21125
  await this.#checkEstimatedContextBeforePrompt();
20589
- if (ownershipCancelled()) {
21126
+ if (retryCancelled() || ownershipCancelled()) {
20590
21127
  const attempt = this.#retryAttempt;
20591
21128
  this.#retryAttempt = 0;
20592
21129
  await this.#emitSessionEvent({