@gajae-code/coding-agent 0.12.5 → 0.12.7

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 (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -0
@@ -32,16 +32,21 @@ import {
32
32
  type AgentTool,
33
33
  assertImagePlaceholdersHavePayload,
34
34
  canContinuePersistedHistory,
35
+ getAgentTerminalOwnerContext,
35
36
  type ManagedAttemptContinuationOwnership,
36
37
  type ManagedAttemptDecision,
37
38
  type ManagedAttemptOutcome,
38
39
  type MidRunMaintenanceOutcome,
40
+ type RunCancellationDomain,
41
+ type RunCancellationDomainBridge,
42
+ type RunResourceProducerLease,
39
43
  type RunSettlementProof,
40
44
  resolveTelemetry,
41
45
  type StablePrefixSnapshot,
42
46
  ThinkingLevel,
43
47
  } from "@gajae-code/agent-core";
44
48
  import { normalizeMessagesForProvider } from "@gajae-code/agent-core/agent-loop";
49
+ import type { AttemptRunHandle, AttemptScope, AttemptScopeAuthority } from "@gajae-code/agent-core/attempt-scope";
45
50
  import {
46
51
  AUTO_HANDOFF_THRESHOLD_FOCUS,
47
52
  CompactionCancelledError,
@@ -73,6 +78,7 @@ import {
73
78
  } from "@gajae-code/agent-core/compaction/pruning";
74
79
  import type {
75
80
  AssistantMessage,
81
+ AttemptScopeRef,
76
82
  Context,
77
83
  DeveloperMessage,
78
84
  Effort,
@@ -109,6 +115,7 @@ import {
109
115
  type FallbackTriggerClass,
110
116
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
111
117
  } from "@gajae-code/ai/utils/fallback-transport";
118
+ import { AttemptRecordStore } from "./attempt-record-store";
112
119
  import {
113
120
  BTW_MAX_ANSWER_UTF8_BYTES,
114
121
  BTW_MAX_QUESTION_UTF8_BYTES,
@@ -564,7 +571,11 @@ export interface AgentSessionConfig {
564
571
  /** Tool-session factory context used to lazily attach workflow-gate-only tools. */
565
572
  workflowGateToolSession?: ToolSession;
566
573
  /** Current session pre-LLM message transform pipeline */
567
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
574
+ transformContext?: (
575
+ messages: AgentMessage[],
576
+ signal?: AbortSignal,
577
+ scope?: AttemptScopeRef,
578
+ ) => AgentMessage[] | Promise<AgentMessage[]>;
568
579
  /** Provider payload hook used by the active session request path */
569
580
  onPayload?: SimpleStreamOptions["onPayload"];
570
581
  /** Provider response hook used by the active session request path */
@@ -869,14 +880,17 @@ type RetryErrorClassification =
869
880
 
870
881
  const BARE_DEFAULT_WATCHDOG_ERROR =
871
882
  /^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
883
+ const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
884
+ const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
885
+ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
872
886
  const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
873
887
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
874
888
  "anthropic-messages": new Set([
875
- "Provider stream timed out while waiting for the first event",
889
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
876
890
  "Anthropic stream timed out while waiting for the first event",
877
891
  ]),
878
892
  "openai-completions": new Set([
879
- "Provider stream timed out while waiting for the first event",
893
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
880
894
  "OpenAI completions stream timed out while waiting for the first event",
881
895
  ]),
882
896
  } as const;
@@ -884,11 +898,11 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
884
898
  const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
885
899
  const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
886
900
  "openai-responses": new Set([
887
- "Provider stream timed out while waiting for the first event",
901
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
888
902
  "OpenAI responses stream timed out while waiting for the first event",
889
903
  ]),
890
904
  "openai-completions": new Set([
891
- "Provider stream timed out while waiting for the first event",
905
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
892
906
  "OpenAI completions stream timed out while waiting for the first event",
893
907
  ]),
894
908
  } as const;
@@ -906,6 +920,25 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
906
920
  (facts.headers !== undefined && Object.keys(facts.headers).length > 0)
907
921
  );
908
922
  }
923
+ function terminalProviderFirstEventTimeoutIdentity(errorMessage: string): string {
924
+ return errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
925
+ ? PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
926
+ : errorMessage;
927
+ }
928
+ function isMessageOnlyFirstEventTimeout(message: AssistantMessage): boolean {
929
+ if (hasBareDefaultRetryDisqualifyingFacts(message)) return false;
930
+ return (
931
+ message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR ||
932
+ message.errorMessage === PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR
933
+ );
934
+ }
935
+
936
+ function isBareDefaultWrappedFirstEventTimeout(message: AssistantMessage): boolean {
937
+ return (
938
+ message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR &&
939
+ !hasBareDefaultRetryDisqualifyingFacts(message)
940
+ );
941
+ }
909
942
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
910
943
  return (
911
944
  message.api === "openai-codex-responses" &&
@@ -1629,6 +1662,49 @@ export class StreamingEditFileCache {
1629
1662
  }
1630
1663
  }
1631
1664
  type SessionAdmissionKind = "prompt" | "selection";
1665
+ class SessionRunCancellationDomainBridge implements RunCancellationDomainBridge {
1666
+ #domains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
1667
+ #released = new Set<string>();
1668
+ #quarantined = new Set<string>();
1669
+
1670
+ open(resourceRunId: string) {
1671
+ if (this.#quarantined.has(resourceRunId)) return { ok: false as const, reason: "quarantined" as const };
1672
+ const existing = this.#domains.get(resourceRunId);
1673
+ if (existing) return { ok: true as const, domain: existing.domain, created: false };
1674
+ if (this.#released.has(resourceRunId)) return { ok: false as const, reason: "duplicate_identity" as const };
1675
+ const controller = new AbortController();
1676
+ const domain: RunCancellationDomain = { resourceRunId, signal: controller.signal };
1677
+ this.#domains.set(resourceRunId, { domain, controller });
1678
+ return { ok: true as const, domain, created: true };
1679
+ }
1680
+
1681
+ lookup(resourceRunId: string): RunCancellationDomain | undefined {
1682
+ return this.#domains.get(resourceRunId)?.domain;
1683
+ }
1684
+
1685
+ abort(resourceRunId: string, reason?: unknown) {
1686
+ const record = this.#domains.get(resourceRunId);
1687
+ if (!record)
1688
+ return {
1689
+ ok: false as const,
1690
+ reason: this.#quarantined.has(resourceRunId) ? ("quarantined" as const) : ("unknown_run" as const),
1691
+ };
1692
+ const newlyAborted = !record.controller.signal.aborted;
1693
+ if (newlyAborted) record.controller.abort(reason);
1694
+ return { ok: true as const, newlyAborted };
1695
+ }
1696
+
1697
+ release(resourceRunId: string, disposition: "settled" | "quarantined"): void {
1698
+ const record = this.#domains.get(resourceRunId);
1699
+ if (!record) return;
1700
+ if (disposition === "quarantined") {
1701
+ this.#quarantined.add(resourceRunId);
1702
+ if (!record.controller.signal.aborted) record.controller.abort();
1703
+ }
1704
+ this.#domains.delete(resourceRunId);
1705
+ this.#released.add(resourceRunId);
1706
+ }
1707
+ }
1632
1708
 
1633
1709
  type SessionAdmissionEntry = {
1634
1710
  kind: SessionAdmissionKind;
@@ -1888,6 +1964,15 @@ export class AgentSession {
1888
1964
  #newSessionTransition: Promise<boolean> | undefined;
1889
1965
  // Extension system
1890
1966
  #extensionRunner: ExtensionRunner | undefined = undefined;
1967
+ #attemptAuthority!: AttemptScopeAuthority;
1968
+ #attemptRecordStore!: AttemptRecordStore;
1969
+ #activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
1970
+ #acceptRunHandle(handle: AttemptRunHandle | undefined): void {
1971
+ // Integration doubles can accept a run without minting a handle; keep the
1972
+ // previously recorded run id rather than throwing inside the callback.
1973
+ if (!handle) return;
1974
+ this.#activeLogicalRunId = handle.logicalRunId;
1975
+ }
1891
1976
 
1892
1977
  #turnIndex = 0;
1893
1978
  #workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
@@ -1917,7 +2002,11 @@ export class AgentSession {
1917
2002
  // Tool registry and prompt builder for extensions
1918
2003
  #toolRegistry: Map<string, AgentTool>;
1919
2004
  #workflowGateToolSession: ToolSession | undefined;
1920
- #transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
2005
+ #transformContext: (
2006
+ messages: AgentMessage[],
2007
+ signal?: AbortSignal,
2008
+ scope?: AttemptScopeRef,
2009
+ ) => AgentMessage[] | Promise<AgentMessage[]>;
1921
2010
  #onPayload: SimpleStreamOptions["onPayload"] | undefined;
1922
2011
  #onResponse: SimpleStreamOptions["onResponse"] | undefined;
1923
2012
  #onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
@@ -2009,6 +2098,10 @@ export class AgentSession {
2009
2098
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
2010
2099
  #postPromptTasksResolve: (() => void) | undefined = undefined;
2011
2100
  #postPromptTasksAbortController = new AbortController();
2101
+ #runCancellationDomains = new SessionRunCancellationDomainBridge();
2102
+ #postPromptLeases = new Map<string, RunResourceProducerLease>();
2103
+ #runResourceLeaseContext = new AsyncLocalStorage<RunResourceProducerLease>();
2104
+ #agentSessionClaimKey = {};
2012
2105
 
2013
2106
  #streamingEditAbortTriggered = false;
2014
2107
  #streamingEditCheckedLineCounts = new Map<string, number>();
@@ -2051,6 +2144,7 @@ export class AgentSession {
2051
2144
  // the successor or proves it cannot. Holds prevent a false idle event while
2052
2145
  // preserving the predecessor for cancellation and preflight failures.
2053
2146
  #pendingAgentEndContinuationHolds = new Map<symbol, AgentSessionEvent>();
2147
+ #deferredAgentEndLeases = new WeakMap<AgentSessionEvent, RunResourceProducerLease>();
2054
2148
  #sessionSettlementPromise: Promise<void> | undefined;
2055
2149
  #sessionSettlementResolve: (() => void) | undefined;
2056
2150
  #agentEndPublicationInFlight = 0;
@@ -2334,6 +2428,17 @@ export class AgentSession {
2334
2428
  this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
2335
2429
  }
2336
2430
 
2431
+ #releaseDeferredAgentEndLease(pending: AgentSessionEvent | undefined): void {
2432
+ if (!pending) return;
2433
+ const lease = this.#deferredAgentEndLeases.get(pending);
2434
+ if (!lease) return;
2435
+ this.#deferredAgentEndLeases.delete(pending);
2436
+ if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
2437
+ this.#postPromptLeases.delete(lease.resourceRunId);
2438
+ }
2439
+ lease.closeDiscovery();
2440
+ }
2441
+
2337
2442
  #isSessionSettlementPending(): boolean {
2338
2443
  return (
2339
2444
  this.#promptInFlightCount > 0 ||
@@ -2397,13 +2502,26 @@ export class AgentSession {
2397
2502
  return;
2398
2503
  }
2399
2504
  this.#pendingAgentEndEmit = undefined;
2505
+ const lease = this.#deferredAgentEndLeases.get(pending);
2506
+ if (lease) this.#deferredAgentEndLeases.delete(pending);
2400
2507
  this.#agentEndPublicationInFlight++;
2401
- this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending);
2508
+ this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
2402
2509
  void this.#agentEndPublicationPromise;
2403
2510
  }
2404
2511
 
2405
- async #publishDeferredAgentEnd(pending: AgentSessionEvent): Promise<void> {
2406
- try {
2512
+ async #publishDeferredAgentEnd(
2513
+ pending: AgentSessionEvent,
2514
+ lease: RunResourceProducerLease | undefined,
2515
+ ): Promise<void> {
2516
+ let extensionDelivery: Promise<void> | undefined;
2517
+ const releaseLease = () => {
2518
+ if (!lease) return;
2519
+ if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
2520
+ this.#postPromptLeases.delete(lease.resourceRunId);
2521
+ }
2522
+ lease.closeDiscovery();
2523
+ };
2524
+ const publish = async () => {
2407
2525
  // Worker integration is first-party lifecycle persistence, not an extension
2408
2526
  // hook. Make it durable before publishing the terminal boundary while user
2409
2527
  // extension delivery remains asynchronous.
@@ -2411,10 +2529,21 @@ export class AgentSession {
2411
2529
  // Persist before notifying synchronous subscribers: a subscriber may start a
2412
2530
  // successor prompt from agent_end, whose running state must serialize after
2413
2531
  // this terminal boundary rather than be overwritten by it.
2414
- this.#persistRuntimeStateInBackground(pending);
2532
+ void this.#persistRuntimeStateInBackground(pending);
2415
2533
  this.#emit(pending);
2416
- void this.#queueExtensionEvent(pending, undefined, true);
2534
+ extensionDelivery = this.#queueExtensionEvent(
2535
+ pending,
2536
+ undefined,
2537
+ true,
2538
+ (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
2539
+ );
2540
+ };
2541
+ try {
2542
+ if (lease) await this.#runResourceLeaseContext.run(lease, publish);
2543
+ else await publish();
2417
2544
  } finally {
2545
+ if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
2546
+ else releaseLease();
2418
2547
  this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
2419
2548
  this.#resolveSessionSettlement();
2420
2549
  }
@@ -2422,6 +2551,7 @@ export class AgentSession {
2422
2551
 
2423
2552
  constructor(config: AgentSessionConfig) {
2424
2553
  this.agent = config.agent;
2554
+ this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
2425
2555
  this.sessionManager = config.sessionManager;
2426
2556
  this.settings = config.settings;
2427
2557
  this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
@@ -2471,6 +2601,11 @@ export class AgentSession {
2471
2601
  this.#promptTemplates = config.promptTemplates ?? [];
2472
2602
  this.#slashCommands = config.slashCommands ?? [];
2473
2603
  this.#extensionRunner = config.extensionRunner;
2604
+ this.#attemptAuthority = this.agent.getAttemptScopeAuthority();
2605
+ this.#attemptRecordStore = new AttemptRecordStore(this.#attemptAuthority);
2606
+ if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
2607
+ this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
2608
+ }
2474
2609
  this.#skills = config.skills ?? [];
2475
2610
  this.#skillWarnings = config.skillWarnings ?? [];
2476
2611
  this.#customCommands = config.customCommands ?? [];
@@ -2483,7 +2618,10 @@ export class AgentSession {
2483
2618
  this.#workflowGateToolSession = config.workflowGateToolSession;
2484
2619
  this.#requestedToolNames = config.requestedToolNames;
2485
2620
  this.#transformContext = config.transformContext ?? (messages => messages);
2486
- this.#onPayload = config.onPayload;
2621
+ const configuredOnPayload = config.onPayload;
2622
+ this.#onPayload = configuredOnPayload
2623
+ ? (payload, model, scope) => configuredOnPayload(payload, model, scope)
2624
+ : undefined;
2487
2625
  this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
2488
2626
  // Avoid wrapping in an `async` closure when no user callback is configured: the
2489
2627
  // outer await on `#onResponse` (provider-response.ts) tolerates a sync void return,
@@ -2491,20 +2629,20 @@ export class AgentSession {
2491
2629
  // shows up as ~3.5% self time in streaming profiles.
2492
2630
  const configuredOnResponse = config.onResponse;
2493
2631
  this.#onResponse = configuredOnResponse
2494
- ? async (response, model) => {
2632
+ ? async (response, model, scope) => {
2495
2633
  this.rawSseDebugBuffer.recordResponse(response, model);
2496
- await configuredOnResponse(response, model);
2634
+ await configuredOnResponse(response, model, scope);
2497
2635
  }
2498
- : (response, model) => {
2636
+ : (response, model, _scope) => {
2499
2637
  this.rawSseDebugBuffer.recordResponse(response, model);
2500
2638
  };
2501
2639
  const configuredOnSseEvent = config.onSseEvent;
2502
2640
  this.#onSseEvent = configuredOnSseEvent
2503
- ? (event, model) => {
2641
+ ? (event, model, scope) => {
2504
2642
  this.rawSseDebugBuffer.recordEvent(event, model);
2505
- configuredOnSseEvent(event, model);
2643
+ configuredOnSseEvent(event, model, scope);
2506
2644
  }
2507
- : (event, model) => {
2645
+ : (event, model, _scope) => {
2508
2646
  this.rawSseDebugBuffer.recordEvent(event, model);
2509
2647
  };
2510
2648
  this.agent.setProviderResponseInterceptor(this.#onResponse);
@@ -3336,6 +3474,7 @@ export class AgentSession {
3336
3474
  event: AgentSessionEvent,
3337
3475
  turnGeneration?: number,
3338
3476
  workerIntegrationSettled = false,
3477
+ scope?: AttemptScopeRef,
3339
3478
  ): Promise<void> {
3340
3479
  // Streaming events observed after turn_end belong to no live extension turn.
3341
3480
  // Events already queued before that boundary must drain in FIFO order, unless
@@ -3351,7 +3490,7 @@ export class AgentSession {
3351
3490
  turnGeneration === undefined || turnGeneration === this.#extensionTurnGeneration;
3352
3491
  const emit = async () => {
3353
3492
  if (!belongsToCurrentTurn()) return;
3354
- await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled);
3493
+ await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled, scope);
3355
3494
  };
3356
3495
  const queued = this.#queuedExtensionEvents.then(emit, emit);
3357
3496
  this.#queuedExtensionEvents = queued.catch(() => {});
@@ -3364,38 +3503,103 @@ export class AgentSession {
3364
3503
  }
3365
3504
 
3366
3505
  #trackAgentEvent = (event: AgentEvent): Promise<void> => {
3367
- const activePromptHandle = this.activePromptHandle;
3506
+ const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
3507
+ const maintenanceCheckpoint =
3508
+ event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
3509
+ const terminalClaim =
3510
+ maintenanceCheckpoint || !terminalOwner
3511
+ ? undefined
3512
+ : this.agent.resourceLedger.claimProducer(
3513
+ terminalOwner.resourceRunId,
3514
+ terminalOwner.domain,
3515
+ this.#agentSessionClaimKey,
3516
+ );
3517
+ // A maintenance checkpoint is not a terminal publication. Capture the current
3518
+ // logical handle synchronously, then reserve its producer before this listener
3519
+ // creates any async work. True terminals must never fall back to this mutable
3520
+ // session state: their owner context is the only authority.
3521
+ const activePromptHandle = maintenanceCheckpoint
3522
+ ? this.activePromptHandle
3523
+ : event.type === "agent_end"
3524
+ ? terminalOwner?.resourceRunId
3525
+ : this.activePromptHandle;
3526
+ const domain = activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle) : undefined;
3527
+ const eventReservation = terminalClaim?.ok
3528
+ ? terminalClaim
3529
+ : activePromptHandle && domain
3530
+ ? this.agent.resourceLedger.reserveProducer(
3531
+ activePromptHandle,
3532
+ domain,
3533
+ "post_prompt",
3534
+ "agent-session-event",
3535
+ )
3536
+ : undefined;
3537
+ const eventLease = eventReservation?.ok ? eventReservation.lease : undefined;
3368
3538
  const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
3369
3539
  if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
3370
3540
  this.#agentEventHandlersInFlight++;
3371
3541
  const handler = (async (): Promise<void> => {
3372
3542
  try {
3373
- await this.#handleAgentEvent(event, activePromptHandle);
3543
+ // A terminal that carries owner context must win the exact producer claim;
3544
+ // a superseded run's late agent_end never falls back to a successor's
3545
+ // handle. Externally emitted terminals carry no owner context and own no
3546
+ // run resources, so they keep the ownerless handling path unless a
3547
+ // prompt-owned run is actually active.
3548
+ if (event.type === "agent_end" && !maintenanceCheckpoint) {
3549
+ if (terminalOwner ? !terminalClaim?.ok : this.activePromptHandle !== undefined) return;
3550
+ }
3551
+ // A prompt-owned event with a captured handle must have obtained an exact
3552
+ // producer lease before its handler was created. Failed ownership never
3553
+ // schedules prompt work or falls back to a successor's handle.
3554
+ if (activePromptHandle && !eventLease) return;
3555
+ if (eventLease && event.type === "agent_end")
3556
+ this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
3557
+ if (eventLease) {
3558
+ await this.#runResourceLeaseContext.run(eventLease, () =>
3559
+ this.#handleAgentEvent(event, activePromptHandle),
3560
+ );
3561
+ } else {
3562
+ await this.#handleAgentEvent(event, activePromptHandle);
3563
+ }
3374
3564
  } catch (error) {
3375
3565
  logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
3376
3566
  } finally {
3567
+ if (eventLease) {
3568
+ const pendingAgentEnd =
3569
+ event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
3570
+ ? this.#pendingAgentEndEmit
3571
+ : undefined;
3572
+ if (pendingAgentEnd) {
3573
+ this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
3574
+ } else {
3575
+ if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
3576
+ this.#postPromptLeases.delete(eventLease.resourceRunId);
3577
+ eventLease.closeDiscovery();
3578
+ }
3579
+ }
3377
3580
  this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
3378
3581
  this.#flushPendingAgentEnd();
3379
3582
  agentEndHandled?.resolve();
3380
3583
  }
3381
3584
  })();
3382
- if (activePromptHandle) {
3383
- this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
3384
- }
3585
+ if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
3385
3586
  return handler;
3386
3587
  };
3387
3588
 
3388
- #persistRuntimeStateInBackground(event: AgentSessionEvent): void {
3389
- void persistCoordinatorRuntimeStateFromEvent(event, {
3390
- sessionId: this.sessionId,
3391
- cwd: this.sessionManager.getCwd(),
3392
- sessionFile: this.sessionManager.getSessionFile(),
3393
- }).catch(() => {
3589
+ async #persistRuntimeStateInBackground(event: AgentSessionEvent): Promise<void> {
3590
+ try {
3591
+ await persistCoordinatorRuntimeStateFromEvent(event, {
3592
+ sessionId: this.sessionId,
3593
+ cwd: this.sessionManager.getCwd(),
3594
+ sessionFile: this.sessionManager.getSessionFile(),
3595
+ });
3596
+ } catch {
3394
3597
  logger.warn("Failed to persist coordinator runtime state", { event: event.type });
3395
- });
3598
+ }
3396
3599
  }
3397
3600
 
3398
3601
  async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
3602
+ const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
3399
3603
  if (event.type === "turn_start") {
3400
3604
  this.#extensionTurnGeneration++;
3401
3605
  this.#closedExtensionTurnGeneration = undefined;
@@ -3420,7 +3624,7 @@ export class AgentSession {
3420
3624
  this.#emit(event);
3421
3625
  if (this.#hasStreamingExtensionHandlers()) {
3422
3626
  __agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
3423
- void this.#queueExtensionEvent(event, this.#extensionTurnGeneration);
3627
+ void this.#queueExtensionEvent(event, this.#extensionTurnGeneration, false, attemptScope);
3424
3628
  }
3425
3629
  return;
3426
3630
  }
@@ -3449,7 +3653,7 @@ export class AgentSession {
3449
3653
  if (event.type === "agent_end") {
3450
3654
  // Start the durable terminal write before synchronous subscribers can
3451
3655
  // re-enter prompt(), so a successor's running transition serializes after it.
3452
- void persistRuntimeState();
3656
+ await persistRuntimeState();
3453
3657
  this.#emit(event);
3454
3658
  await this.#emitExtensionEvent(event);
3455
3659
  return;
@@ -3534,6 +3738,11 @@ export class AgentSession {
3534
3738
 
3535
3739
  /** Internal handler for agent events - shared by subscribe and reconnect */
3536
3740
  #handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
3741
+ const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
3742
+ if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
3743
+ this.#attemptRecordStore.register(attemptScope);
3744
+ this.#attemptRecordStore.establishClean(attemptScope);
3745
+ }
3537
3746
  if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
3538
3747
  if (
3539
3748
  event.type === "tool_execution_start" ||
@@ -4048,6 +4257,7 @@ export class AgentSession {
4048
4257
  generation: maintenanceGeneration,
4049
4258
  skipCompactionCheck: true,
4050
4259
  resourceRunId: activePromptHandle,
4260
+ maintenanceContinuation: true,
4051
4261
  });
4052
4262
  }
4053
4263
  return;
@@ -4128,10 +4338,11 @@ export class AgentSession {
4128
4338
  }
4129
4339
  this.#resolveRetry();
4130
4340
 
4131
- const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
4132
- this.#trackPostPromptTask(
4133
- compactionTask.then(() => undefined),
4134
- activePromptHandle,
4341
+ const compactionTask = this.#schedulePostPromptTask(
4342
+ async () => {
4343
+ await this.#checkCompaction(msg, true, undefined, activePromptHandle);
4344
+ },
4345
+ { resourceRunId: activePromptHandle },
4135
4346
  );
4136
4347
  await compactionTask;
4137
4348
  // Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
@@ -4196,29 +4407,52 @@ export class AgentSession {
4196
4407
  this.#postPromptTasksPromise = undefined;
4197
4408
  }
4198
4409
 
4199
- #trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
4410
+ #trackPostPromptTask(task: Promise<void>, lease?: RunResourceProducerLease, leaseTask: Promise<void> = task): void {
4200
4411
  this.#postPromptTasks.add(task);
4201
4412
  this.#ensurePostPromptTasksPromise();
4202
- if (resourceRunId) {
4203
- this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
4413
+ if (lease) {
4414
+ lease.track("post_prompt", "agent-session", leaseTask);
4415
+ void leaseTask.catch(() => {}).finally(() => lease.closeDiscovery());
4204
4416
  }
4205
4417
  void task
4206
4418
  .catch(() => {})
4207
4419
  .finally(() => {
4208
4420
  this.#postPromptTasks.delete(task);
4209
- if (this.#postPromptTasks.size === 0) {
4210
- this.#resolvePostPromptTasks();
4211
- }
4421
+ if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
4212
4422
  });
4213
4423
  }
4214
4424
 
4215
4425
  #schedulePostPromptTask(
4216
4426
  task: (signal: AbortSignal) => Promise<void>,
4217
- options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
4427
+ options?: {
4428
+ delayMs?: number;
4429
+ generation?: number;
4430
+ onSkip?: () => void;
4431
+ resourceRunId?: string;
4432
+ leaseTask?: Promise<void>;
4433
+ },
4218
4434
  ): Promise<void> {
4219
4435
  const delayMs = options?.delayMs ?? 0;
4220
- const signal = this.#postPromptTasksAbortController.signal;
4221
- const scheduled = (async () => {
4436
+ const resourceRunId = options?.resourceRunId;
4437
+ const contextualLease = this.#runResourceLeaseContext.getStore();
4438
+ const parentLease =
4439
+ resourceRunId && contextualLease?.resourceRunId === resourceRunId
4440
+ ? contextualLease
4441
+ : resourceRunId
4442
+ ? this.#postPromptLeases.get(resourceRunId)
4443
+ : undefined;
4444
+ const domain = resourceRunId ? this.#runCancellationDomains.lookup(resourceRunId) : undefined;
4445
+ const reservation = parentLease
4446
+ ? parentLease.fork(parentLease.domain, "post_prompt", "agent-session")
4447
+ : resourceRunId && domain
4448
+ ? this.agent.resourceLedger.reserveProducer(resourceRunId, domain, "post_prompt", "agent-session")
4449
+ : undefined;
4450
+ if (resourceRunId && !reservation?.ok) {
4451
+ options?.onSkip?.();
4452
+ return Promise.resolve();
4453
+ }
4454
+ const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
4455
+ const runScheduled = async () => {
4222
4456
  if (delayMs > 0) {
4223
4457
  try {
4224
4458
  await scheduler.wait(delayMs, { signal });
@@ -4236,8 +4470,11 @@ export class AgentSession {
4236
4470
  return;
4237
4471
  }
4238
4472
  await task(signal);
4239
- })();
4240
- this.#trackPostPromptTask(scheduled, options?.resourceRunId);
4473
+ };
4474
+ const scheduled = reservation?.ok
4475
+ ? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
4476
+ : runScheduled();
4477
+ this.#trackPostPromptTask(scheduled, reservation?.ok ? reservation.lease : undefined, options?.leaseTask);
4241
4478
  return scheduled;
4242
4479
  }
4243
4480
 
@@ -4256,6 +4493,7 @@ export class AgentSession {
4256
4493
  rescheduleOnBusy?: boolean;
4257
4494
  onError?: (error: unknown) => void;
4258
4495
  resourceRunId?: string;
4496
+ maintenanceContinuation?: boolean;
4259
4497
  }): Promise<void> {
4260
4498
  const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
4261
4499
  ? this.#reserveDeferredAgentEndForContinuation()
@@ -4277,12 +4515,13 @@ export class AgentSession {
4277
4515
  options?.onError?.(error);
4278
4516
  };
4279
4517
  const scheduledGeneration = options?.generation;
4280
- const signal = this.#postPromptTasksAbortController.signal;
4281
- const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
4282
- this.#schedulePostPromptTask(
4283
- async () => {
4518
+ const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> => {
4519
+ const leaseSettlement = Promise.withResolvers<void>();
4520
+ const settleLease = () => leaseSettlement.resolve();
4521
+ return this.#schedulePostPromptTask(
4522
+ async scheduledSignal => {
4284
4523
  const canContinue = (): boolean => {
4285
- if (signal.aborted || this.#isDisposed) {
4524
+ if (scheduledSignal.aborted || this.#isDisposed) {
4286
4525
  skip("aborted_signal");
4287
4526
  return false;
4288
4527
  }
@@ -4300,13 +4539,16 @@ export class AgentSession {
4300
4539
  }
4301
4540
  return true;
4302
4541
  };
4303
- if (!canContinue()) return;
4542
+ if (!canContinue()) {
4543
+ settleLease();
4544
+ return;
4545
+ }
4304
4546
  try {
4305
4547
  if (!options?.skipCompactionCheck) {
4306
4548
  await this.#checkEstimatedContextBeforePrompt();
4307
4549
  if (!canContinue()) return;
4308
4550
  }
4309
- if (signal.aborted) {
4551
+ if (scheduledSignal.aborted) {
4310
4552
  skip("aborted_signal");
4311
4553
  return;
4312
4554
  }
@@ -4326,23 +4568,40 @@ export class AgentSession {
4326
4568
  return;
4327
4569
  }
4328
4570
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4571
+ let predecessorAccepted = false;
4572
+ const releasePredecessor = () => {
4573
+ if (predecessorAccepted) return;
4574
+ predecessorAccepted = true;
4575
+ this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
4576
+ };
4329
4577
  const startsQueuedSuccessor =
4330
4578
  this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4331
4579
  try {
4332
4580
  await this.agent.continue({
4333
4581
  ...this.#managedFallbackPromptOptions(),
4582
+ maintenanceContinuation: options?.maintenanceContinuation,
4334
4583
  // Reset only after continue() has claimed the queued turn. Skipped or stale
4335
4584
  // continuations retain predecessor accounting, and resetAttemptBudget keeps
4336
4585
  // the sticky fallback cursor unchanged.
4337
- onRunAccepted: startsQueuedSuccessor
4338
- ? () => {
4339
- this.#defaultFallbackChain().resetAttemptBudget();
4340
- this.#overflowMaintenanceAttempts = 0;
4341
- }
4342
- : undefined,
4586
+ onRunAccepted: (handle: AttemptRunHandle) => {
4587
+ this.#acceptRunHandle(handle);
4588
+ settleLease();
4589
+ releasePredecessor();
4590
+ if (startsQueuedSuccessor) {
4591
+ this.#defaultFallbackChain().resetAttemptBudget();
4592
+ this.#overflowMaintenanceAttempts = 0;
4593
+ }
4594
+ },
4343
4595
  });
4596
+ releasePredecessor();
4344
4597
  } catch (error) {
4345
- if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
4598
+ if (
4599
+ options?.rescheduleOnBusy &&
4600
+ this.#isAgentBusyError(error) &&
4601
+ !predecessorAccepted &&
4602
+ !terminalized &&
4603
+ canContinue()
4604
+ ) {
4346
4605
  this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
4347
4606
  logger.debug("agent.continue busy after scheduling; rescheduling", {
4348
4607
  error: error.message,
@@ -4350,19 +4609,27 @@ export class AgentSession {
4350
4609
  void scheduleAttempt(100);
4351
4610
  return;
4352
4611
  }
4353
- this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4612
+ if (!predecessorAccepted)
4613
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4354
4614
  throw error;
4355
4615
  }
4356
4616
  } catch (error) {
4357
4617
  fail(error);
4618
+ } finally {
4619
+ settleLease();
4358
4620
  }
4359
4621
  },
4360
4622
  {
4361
4623
  delayMs,
4362
- onSkip: () => skip("aborted_signal"),
4624
+ onSkip: () => {
4625
+ settleLease();
4626
+ skip("aborted_signal");
4627
+ },
4363
4628
  resourceRunId: options?.resourceRunId,
4629
+ leaseTask: leaseSettlement.promise,
4364
4630
  },
4365
4631
  );
4632
+ };
4366
4633
  return scheduleAttempt();
4367
4634
  }
4368
4635
 
@@ -4433,7 +4700,7 @@ export class AgentSession {
4433
4700
 
4434
4701
  const compactionSettings = this.settings.getGroup("compaction");
4435
4702
  if (compactionSettings.autoContinue !== false) {
4436
- this.#scheduleAutoContinuePrompt(generation, false);
4703
+ this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
4437
4704
  return true;
4438
4705
  }
4439
4706
 
@@ -4441,11 +4708,13 @@ export class AgentSession {
4441
4708
  return false;
4442
4709
  }
4443
4710
 
4444
- #scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
4711
+ #scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true, resourceRunId?: string): void {
4445
4712
  const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
4446
4713
  const scheduledGeneration = generation;
4447
- const signal = this.#postPromptTasksAbortController.signal;
4448
- const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
4714
+ const continuationAuthorized = async (
4715
+ signal: AbortSignal,
4716
+ hasPendingNextTurnMessages = false,
4717
+ ): Promise<boolean> => {
4449
4718
  const snapshot = await this.#compactionStateSnapshot();
4450
4719
  if (signal.aborted) {
4451
4720
  this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
@@ -4467,61 +4736,68 @@ export class AgentSession {
4467
4736
  if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
4468
4737
  return authorized;
4469
4738
  };
4470
- const continuePrompt = async () => {
4471
- await this.#promptWithMessage(
4472
- {
4473
- role: "developer",
4474
- content: [{ type: "text", text: autoContinuePrompt }],
4475
- attribution: "agent",
4476
- timestamp: Date.now(),
4739
+ const scheduleAttempt = (delayMs = 0) => {
4740
+ let rescheduled = false;
4741
+ void this.#schedulePostPromptTask(
4742
+ async signal => {
4743
+ try {
4744
+ await Promise.resolve();
4745
+ if (signal.aborted) {
4746
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4747
+ return;
4748
+ }
4749
+ if (this.#promptGeneration !== scheduledGeneration) {
4750
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
4751
+ return;
4752
+ }
4753
+ if (!(await continuationAuthorized(signal))) return;
4754
+ await this.#promptWithMessage(
4755
+ {
4756
+ role: "developer",
4757
+ content: [{ type: "text", text: autoContinuePrompt }],
4758
+ attribution: "agent",
4759
+ timestamp: Date.now(),
4760
+ },
4761
+ autoContinuePrompt,
4762
+ {
4763
+ skipPostPromptRecoveryWait: true,
4764
+ skipCompactionCheck: true,
4765
+ predecessorAgentEndHold,
4766
+ onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
4767
+ continuationAuthorized(signal, hasPendingNextTurnMessages),
4768
+ },
4769
+ );
4770
+ } catch (error) {
4771
+ if (signal.aborted) {
4772
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4773
+ return;
4774
+ }
4775
+ if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
4776
+ logger.debug("Auto-compaction continuation busy; rescheduling", {
4777
+ source: "auto_continue_prompt",
4778
+ reason: "queue_drained",
4779
+ error: error.message,
4780
+ });
4781
+ rescheduled = true;
4782
+ scheduleAttempt(100);
4783
+ return;
4784
+ }
4785
+ this.#logCompactionContinuationError("auto_continue_prompt", error);
4786
+ } finally {
4787
+ if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4788
+ }
4477
4789
  },
4478
- autoContinuePrompt,
4479
4790
  {
4480
- skipPostPromptRecoveryWait: true,
4481
- skipCompactionCheck: true,
4482
- predecessorAgentEndHold,
4483
- onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
4791
+ delayMs,
4792
+ generation: scheduledGeneration,
4793
+ resourceRunId,
4794
+ onSkip: () => {
4795
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4796
+ this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4797
+ },
4484
4798
  },
4485
4799
  );
4486
4800
  };
4487
- const scheduleAttempt = (delayMs = 0) => {
4488
- const attempt = (async () => {
4489
- let rescheduled = false;
4490
- try {
4491
- if (delayMs > 0) await scheduler.wait(delayMs, { signal });
4492
- await Promise.resolve();
4493
- if (signal.aborted) {
4494
- this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4495
- return;
4496
- }
4497
- if (this.#promptGeneration !== scheduledGeneration) {
4498
- this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
4499
- return;
4500
- }
4501
- if (!(await continuationAuthorized())) return;
4502
- await continuePrompt();
4503
- } catch (error) {
4504
- if (signal.aborted) {
4505
- this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4506
- return;
4507
- }
4508
- if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
4509
- logger.debug("Auto-compaction continuation busy; rescheduling", {
4510
- source: "auto_continue_prompt",
4511
- reason: "queue_drained",
4512
- error: error.message,
4513
- });
4514
- rescheduled = true;
4515
- scheduleAttempt(100);
4516
- return;
4517
- }
4518
- this.#logCompactionContinuationError("auto_continue_prompt", error);
4519
- } finally {
4520
- if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4521
- }
4522
- })();
4523
- this.#trackPostPromptTask(attempt);
4524
- };
4525
4801
  scheduleAttempt();
4526
4802
  }
4527
4803
 
@@ -5266,163 +5542,200 @@ export class AgentSession {
5266
5542
  event: AgentSessionEvent,
5267
5543
  continueWhile?: () => boolean,
5268
5544
  workerIntegrationSettled = false,
5545
+ scope?: AttemptScopeRef,
5269
5546
  ): Promise<void> {
5270
5547
  if (event.type === "agent_end" && !workerIntegrationSettled) {
5271
5548
  await this.#flushWorkerIntegrationForAgentEnd();
5272
5549
  }
5273
- if (!this.#extensionRunner) return;
5274
- if (event.type === "agent_start") {
5275
- this.#turnIndex = 0;
5276
- await this.#extensionRunner.emit({ type: "agent_start" });
5277
- } else if (event.type === "agent_end") {
5278
- await this.#extensionRunner.emit({
5279
- type: "agent_end",
5280
- messages: event.messages,
5281
- stopReason: event.stopReason,
5282
- });
5283
- } else if (event.type === "turn_start") {
5284
- const hookEvent: TurnStartEvent = {
5285
- type: "turn_start",
5286
- turnIndex: this.#turnIndex,
5287
- timestamp: Date.now(),
5288
- };
5289
- await this.#extensionRunner.emit(hookEvent);
5290
- } else if (event.type === "turn_end") {
5291
- const hookEvent: TurnEndEvent = {
5292
- type: "turn_end",
5293
- turnIndex: this.#turnIndex,
5294
- message: event.message,
5295
- toolResults: event.toolResults,
5296
- };
5297
- await this.#extensionRunner.emit(hookEvent);
5298
- this.#turnIndex++;
5299
- } else if (event.type === "message_start") {
5300
- const extensionEvent: MessageStartEvent = {
5301
- type: "message_start",
5302
- message: event.message,
5303
- };
5304
- await this.#extensionRunner.emit(extensionEvent);
5305
- } else if (event.type === "message_update") {
5306
- const extensionEvent: MessageUpdateEvent = {
5307
- type: "message_update",
5308
- message: event.message,
5309
- assistantMessageEvent: event.assistantMessageEvent,
5310
- };
5311
- await this.#extensionRunner.emit(extensionEvent, continueWhile);
5312
- if (continueWhile && !continueWhile()) return;
5313
- if (event.assistantMessageEvent.type === "reasoning_summary_start") {
5314
- const reasoningEvent: ReasoningSummaryStartEvent = {
5315
- type: "reasoning_summary_start",
5550
+ const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
5551
+ const isTerminalAgentEnd =
5552
+ event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
5553
+ const finishAttempt = () => {
5554
+ if (!isTerminalAgentEnd) return;
5555
+ if (deliveryScope) this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
5556
+ this.#activeLogicalRunId = undefined;
5557
+ };
5558
+ if (!this.#extensionRunner) {
5559
+ finishAttempt();
5560
+ return;
5561
+ }
5562
+
5563
+ try {
5564
+ if (event.type === "agent_start") {
5565
+ this.#turnIndex = 0;
5566
+ await this.#extensionRunner.emit({ type: "agent_start" }, undefined, deliveryScope);
5567
+ } else if (event.type === "agent_end") {
5568
+ await this.#extensionRunner.emit(
5569
+ {
5570
+ type: "agent_end",
5571
+ messages: event.messages,
5572
+ stopReason: event.stopReason,
5573
+ },
5574
+ undefined,
5575
+ deliveryScope,
5576
+ );
5577
+ } else if (event.type === "turn_start") {
5578
+ const hookEvent: TurnStartEvent = {
5579
+ type: "turn_start",
5580
+ turnIndex: this.#turnIndex,
5581
+ timestamp: Date.now(),
5582
+ };
5583
+ await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
5584
+ } else if (event.type === "turn_end") {
5585
+ const hookEvent: TurnEndEvent = {
5586
+ type: "turn_end",
5587
+ turnIndex: this.#turnIndex,
5316
5588
  message: event.message,
5317
- contentIndex: event.assistantMessageEvent.contentIndex,
5589
+ toolResults: event.toolResults,
5318
5590
  };
5319
- if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
5320
- await this.#extensionRunner.emit(reasoningEvent, continueWhile);
5321
- } else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
5322
- const reasoningEvent: ReasoningSummaryDeltaEvent = {
5323
- type: "reasoning_summary_delta",
5591
+ await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
5592
+ this.#turnIndex++;
5593
+ } else if (event.type === "message_start") {
5594
+ const extensionEvent: MessageStartEvent = {
5595
+ type: "message_start",
5324
5596
  message: event.message,
5325
- contentIndex: event.assistantMessageEvent.contentIndex,
5326
- delta: event.assistantMessageEvent.delta,
5327
5597
  };
5328
- if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
5329
- await this.#extensionRunner.emit(reasoningEvent, continueWhile);
5330
- } else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
5331
- const reasoningEvent: ReasoningSummaryEndEvent = {
5332
- type: "reasoning_summary_end",
5598
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5599
+ } else if (event.type === "message_update") {
5600
+ const extensionEvent: MessageUpdateEvent = {
5601
+ type: "message_update",
5333
5602
  message: event.message,
5334
- contentIndex: event.assistantMessageEvent.contentIndex,
5335
- content: event.assistantMessageEvent.content,
5603
+ assistantMessageEvent: event.assistantMessageEvent,
5336
5604
  };
5337
- if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
5338
- await this.#extensionRunner.emit(reasoningEvent, continueWhile);
5339
- }
5340
- } else if (event.type === "message_end") {
5341
- const extensionEvent: MessageEndEvent = {
5342
- type: "message_end",
5343
- message: event.message,
5344
- };
5345
- await this.#extensionRunner.emit(extensionEvent);
5346
- } else if (event.type === "tool_execution_start") {
5347
- const extensionEvent: ToolExecutionStartEvent = {
5348
- type: "tool_execution_start",
5349
- toolCallId: event.toolCallId,
5350
- toolName: event.toolName,
5351
- args: event.args,
5352
- intent: event.intent,
5353
- };
5354
- await this.#extensionRunner.emit(extensionEvent);
5355
- } else if (event.type === "tool_execution_update") {
5356
- const extensionEvent: ToolExecutionUpdateEvent = {
5357
- type: "tool_execution_update",
5358
- toolCallId: event.toolCallId,
5359
- toolName: event.toolName,
5360
- args: event.args,
5361
- partialResult: event.partialResult,
5362
- };
5363
- await this.#extensionRunner.emit(extensionEvent);
5364
- } else if (event.type === "tool_execution_end") {
5365
- const extensionEvent: ToolExecutionEndEvent = {
5366
- type: "tool_execution_end",
5367
- toolCallId: event.toolCallId,
5368
- toolName: event.toolName,
5369
- result: event.result,
5370
- isError: event.isError ?? false,
5371
- };
5372
- await this.#extensionRunner.emit(extensionEvent);
5373
- } else if (event.type === "auto_compaction_start") {
5374
- await this.#extensionRunner.emit({
5375
- type: "auto_compaction_start",
5376
- reason: event.reason,
5377
- action: event.action,
5378
- });
5379
- } else if (event.type === "auto_compaction_end") {
5380
- await this.#extensionRunner.emit({
5381
- type: "auto_compaction_end",
5382
- action: event.action,
5383
- result: event.result,
5384
- aborted: event.aborted,
5385
- willRetry: event.willRetry,
5386
- errorMessage: event.errorMessage,
5387
- skipped: event.skipped,
5388
- continuationSkipReason: event.continuationSkipReason,
5389
- });
5390
- } else if (event.type === "auto_retry_start") {
5391
- if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
5392
- await this.#extensionRunner.emit({
5393
- type: "auto_retry_start",
5394
- attempt: event.attempt,
5395
- maxAttempts: event.maxAttempts,
5396
- delayMs: event.delayMs,
5397
- errorMessage: event.errorMessage,
5398
- unbounded: event.unbounded,
5399
- });
5400
- } else if (event.type === "auto_retry_end") {
5401
- await this.#extensionRunner.emit({
5402
- type: "auto_retry_end",
5403
- success: event.success,
5404
- attempt: event.attempt,
5405
- finalError: event.finalError,
5406
- });
5407
- } else if (event.type === "ttsr_triggered") {
5408
- await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
5409
- } else if (event.type === "todo_reminder") {
5410
- await this.#extensionRunner.emit({
5411
- type: "todo_reminder",
5412
- todos: event.todos,
5413
- attempt: event.attempt,
5414
- maxAttempts: event.maxAttempts,
5415
- });
5416
- } else if (event.type === "goal_updated") {
5417
- try {
5418
- await this.#extensionRunner.emit({
5419
- type: "goal_updated",
5420
- goal: event.goal,
5421
- state: event.state,
5422
- });
5423
- } catch (error) {
5424
- logger.warn("Goal updated extension hook failed", { error: String(error) });
5605
+ await this.#extensionRunner.emit(extensionEvent, continueWhile, deliveryScope);
5606
+ if (continueWhile && !continueWhile()) return;
5607
+ if (event.assistantMessageEvent.type === "reasoning_summary_start") {
5608
+ const reasoningEvent: ReasoningSummaryStartEvent = {
5609
+ type: "reasoning_summary_start",
5610
+ message: event.message,
5611
+ contentIndex: event.assistantMessageEvent.contentIndex,
5612
+ };
5613
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
5614
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5615
+ } else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
5616
+ const reasoningEvent: ReasoningSummaryDeltaEvent = {
5617
+ type: "reasoning_summary_delta",
5618
+ message: event.message,
5619
+ contentIndex: event.assistantMessageEvent.contentIndex,
5620
+ delta: event.assistantMessageEvent.delta,
5621
+ };
5622
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
5623
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5624
+ } else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
5625
+ const reasoningEvent: ReasoningSummaryEndEvent = {
5626
+ type: "reasoning_summary_end",
5627
+ message: event.message,
5628
+ contentIndex: event.assistantMessageEvent.contentIndex,
5629
+ content: event.assistantMessageEvent.content,
5630
+ };
5631
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
5632
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5633
+ }
5634
+ } else if (event.type === "message_end") {
5635
+ const extensionEvent: MessageEndEvent = {
5636
+ type: "message_end",
5637
+ message: event.message,
5638
+ };
5639
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5640
+ } else if (event.type === "tool_execution_start") {
5641
+ const extensionEvent: ToolExecutionStartEvent = {
5642
+ type: "tool_execution_start",
5643
+ toolCallId: event.toolCallId,
5644
+ toolName: event.toolName,
5645
+ args: event.args,
5646
+ intent: event.intent,
5647
+ };
5648
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5649
+ } else if (event.type === "tool_execution_update") {
5650
+ const extensionEvent: ToolExecutionUpdateEvent = {
5651
+ type: "tool_execution_update",
5652
+ toolCallId: event.toolCallId,
5653
+ toolName: event.toolName,
5654
+ args: event.args,
5655
+ partialResult: event.partialResult,
5656
+ };
5657
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5658
+ } else if (event.type === "tool_execution_end") {
5659
+ const extensionEvent: ToolExecutionEndEvent = {
5660
+ type: "tool_execution_end",
5661
+ toolCallId: event.toolCallId,
5662
+ toolName: event.toolName,
5663
+ result: event.result,
5664
+ isError: event.isError ?? false,
5665
+ };
5666
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5667
+ } else if (event.type === "auto_compaction_start") {
5668
+ await this.#extensionRunner.emit(
5669
+ { type: "auto_compaction_start", reason: event.reason, action: event.action },
5670
+ undefined,
5671
+ deliveryScope,
5672
+ );
5673
+ } else if (event.type === "auto_compaction_end") {
5674
+ await this.#extensionRunner.emit(
5675
+ {
5676
+ type: "auto_compaction_end",
5677
+ action: event.action,
5678
+ result: event.result,
5679
+ aborted: event.aborted,
5680
+ willRetry: event.willRetry,
5681
+ errorMessage: event.errorMessage,
5682
+ skipped: event.skipped,
5683
+ continuationSkipReason: event.continuationSkipReason,
5684
+ },
5685
+ undefined,
5686
+ deliveryScope,
5687
+ );
5688
+ } else if (event.type === "auto_retry_start") {
5689
+ if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
5690
+ await this.#extensionRunner.emit(
5691
+ {
5692
+ type: "auto_retry_start",
5693
+ attempt: event.attempt,
5694
+ maxAttempts: event.maxAttempts,
5695
+ delayMs: event.delayMs,
5696
+ errorMessage: event.errorMessage,
5697
+ unbounded: event.unbounded,
5698
+ },
5699
+ undefined,
5700
+ deliveryScope,
5701
+ );
5702
+ } else if (event.type === "auto_retry_end") {
5703
+ await this.#extensionRunner.emit(
5704
+ {
5705
+ type: "auto_retry_end",
5706
+ success: event.success,
5707
+ attempt: event.attempt,
5708
+ finalError: event.finalError,
5709
+ },
5710
+ undefined,
5711
+ deliveryScope,
5712
+ );
5713
+ } else if (event.type === "ttsr_triggered") {
5714
+ await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules }, undefined, deliveryScope);
5715
+ } else if (event.type === "todo_reminder") {
5716
+ await this.#extensionRunner.emit(
5717
+ {
5718
+ type: "todo_reminder",
5719
+ todos: event.todos,
5720
+ attempt: event.attempt,
5721
+ maxAttempts: event.maxAttempts,
5722
+ },
5723
+ undefined,
5724
+ deliveryScope,
5725
+ );
5726
+ } else if (event.type === "goal_updated") {
5727
+ try {
5728
+ await this.#extensionRunner.emit(
5729
+ { type: "goal_updated", goal: event.goal, state: event.state },
5730
+ undefined,
5731
+ deliveryScope,
5732
+ );
5733
+ } catch (error) {
5734
+ logger.warn("Goal updated extension hook failed", { error: String(error) });
5735
+ }
5425
5736
  }
5737
+ } finally {
5738
+ finishAttempt();
5426
5739
  }
5427
5740
  }
5428
5741
 
@@ -5545,7 +5858,15 @@ export class AgentSession {
5545
5858
  ),
5546
5859
  Bun.sleep(2_000).then(() => false),
5547
5860
  ]);
5548
- if (!disposeIdleSettled) this.agent.forceAbort("Session disposed");
5861
+ if (!disposeIdleSettled) {
5862
+ try {
5863
+ this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
5864
+ } catch {
5865
+ // AttemptScope handle may not be registered for sessions
5866
+ // that don't participate in the facility (e.g. test mocks).
5867
+ this.agent.forceAbort("Session disposed");
5868
+ }
5869
+ }
5549
5870
  await admissionClosed;
5550
5871
  await this.#agentEndPublicationPromise;
5551
5872
  await this.#queuedExtensionEvents;
@@ -6929,7 +7250,8 @@ export class AgentSession {
6929
7250
  this.#assertNoHandoffTransition();
6930
7251
  await this.agent.continue({
6931
7252
  ...this.#managedFallbackPromptOptions(),
6932
- onRunAccepted: () => {
7253
+ onRunAccepted: (handle: AttemptRunHandle) => {
7254
+ this.#acceptRunHandle(handle);
6933
7255
  if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
6934
7256
  },
6935
7257
  });
@@ -6956,20 +7278,32 @@ export class AgentSession {
6956
7278
  }
6957
7279
 
6958
7280
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
6959
- async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
6960
- const transformedMessages = await this.#transformContext(messages, signal);
7281
+ async convertMessagesToLlm(
7282
+ messages: AgentMessage[],
7283
+ signal?: AbortSignal,
7284
+ scope?: AttemptScope,
7285
+ ): Promise<Message[]> {
7286
+ const transformedMessages =
7287
+ scope === undefined
7288
+ ? await this.#transformContext(messages, signal)
7289
+ : await this.#transformContext(messages, signal, scope);
6961
7290
  return await this.#convertToLlm(transformedMessages);
6962
7291
  }
6963
7292
 
6964
7293
  /** Apply session-level stream hooks to a direct side request. */
6965
- prepareSimpleStreamOptions(options: SimpleStreamOptions, provider = "anthropic"): SimpleStreamOptions {
7294
+ prepareSimpleStreamOptions(
7295
+ options: SimpleStreamOptions,
7296
+ provider = "anthropic",
7297
+ scope?: AttemptScope,
7298
+ ): SimpleStreamOptions {
6966
7299
  const sessionOnPayload = this.#onPayload;
6967
7300
  const sessionOnResponse = this.#onResponse;
6968
7301
  const sessionMetadata = this.agent.metadataForProvider(provider);
6969
7302
  const sessionOnSseEvent = this.#onSseEvent;
6970
- if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
7303
+ if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent && !scope) return options;
6971
7304
 
6972
7305
  const preparedOptions: SimpleStreamOptions = { ...options };
7306
+ if (scope) preparedOptions.attemptScope = scope;
6973
7307
 
6974
7308
  // Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
6975
7309
  // they share the same session bucket as Agent.prompt-routed requests on Anthropic
@@ -6983,10 +7317,10 @@ export class AgentSession {
6983
7317
  preparedOptions.onPayload = sessionOnPayload;
6984
7318
  } else {
6985
7319
  const requestOnPayload = options.onPayload;
6986
- preparedOptions.onPayload = async (payload, model) => {
6987
- const sessionPayload = await sessionOnPayload(payload, model);
7320
+ preparedOptions.onPayload = async (payload, model, callbackScope) => {
7321
+ const sessionPayload = await sessionOnPayload(payload, model, callbackScope);
6988
7322
  const sessionResolvedPayload = sessionPayload ?? payload;
6989
- const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
7323
+ const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope);
6990
7324
  return requestPayload ?? sessionResolvedPayload;
6991
7325
  };
6992
7326
  }
@@ -6997,9 +7331,9 @@ export class AgentSession {
6997
7331
  preparedOptions.onResponse = sessionOnResponse;
6998
7332
  } else {
6999
7333
  const requestOnResponse = options.onResponse;
7000
- preparedOptions.onResponse = async (response, model) => {
7001
- await sessionOnResponse(response, model);
7002
- await requestOnResponse(response, model);
7334
+ preparedOptions.onResponse = async (response, model, callbackScope) => {
7335
+ await sessionOnResponse(response, model, callbackScope);
7336
+ await requestOnResponse(response, model, callbackScope);
7003
7337
  };
7004
7338
  }
7005
7339
  }
@@ -7009,9 +7343,9 @@ export class AgentSession {
7009
7343
  preparedOptions.onSseEvent = sessionOnSseEvent;
7010
7344
  } else {
7011
7345
  const requestOnSseEvent = options.onSseEvent;
7012
- preparedOptions.onSseEvent = (event, model) => {
7013
- sessionOnSseEvent(event, model);
7014
- requestOnSseEvent(event, model);
7346
+ preparedOptions.onSseEvent = (event, model, callbackScope) => {
7347
+ sessionOnSseEvent(event, model, callbackScope);
7348
+ requestOnSseEvent(event, model, callbackScope);
7015
7349
  };
7016
7350
  }
7017
7351
  }
@@ -8058,7 +8392,7 @@ export class AgentSession {
8058
8392
  skipPostPromptRecoveryWait?: boolean;
8059
8393
  predecessorAgentEndHold?: symbol;
8060
8394
  admissionLease?: SessionAdmissionLease;
8061
- onRunAccepted?: () => void;
8395
+ onRunAccepted?: (handle: AttemptRunHandle) => void;
8062
8396
  onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
8063
8397
  resetRetryReplaySafety?: boolean;
8064
8398
  },
@@ -8284,8 +8618,9 @@ export class AgentSession {
8284
8618
  const agentPromptOptions = {
8285
8619
  ...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
8286
8620
  ...this.#managedFallbackPromptOptions(),
8287
- onRunAccepted: () => {
8288
- options?.onRunAccepted?.();
8621
+ onRunAccepted: (handle: AttemptRunHandle) => {
8622
+ this.#acceptRunHandle(handle);
8623
+ options?.onRunAccepted?.(handle);
8289
8624
  options?.admissionLease?.release();
8290
8625
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
8291
8626
  },
@@ -9408,7 +9743,14 @@ export class AgentSession {
9408
9743
  ]);
9409
9744
  if (outcome.kind === "timeout") {
9410
9745
  this.#abandonPostPromptTasks();
9411
- this.agent.forceAbort("Abort cleanup timed out");
9746
+ const forceAbortLogicalRunId = this.agent.currentManagedLogicalRunId ?? this.#activeLogicalRunId;
9747
+ try {
9748
+ if (forceAbortLogicalRunId !== undefined)
9749
+ this.agent.forceAbort("Abort cleanup timed out", forceAbortLogicalRunId);
9750
+ else this.agent.forceAbort("Abort cleanup timed out");
9751
+ } catch {
9752
+ this.agent.forceAbort("Abort cleanup timed out");
9753
+ }
9412
9754
  this.emitNotice(
9413
9755
  "warning",
9414
9756
  "Abort cleanup timed out; forced session recovery. The previous provider stream or tool may still be unwinding in the background.",
@@ -9465,20 +9807,16 @@ export class AgentSession {
9465
9807
  * Abort a specific active prompt and prove whether its tracked resources settled.
9466
9808
  */
9467
9809
  async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
9468
- // Only the abort trigger is gated on active ownership; settlement is always proven
9469
- // from the ledger, because a run can go inactive while its resources still run.
9470
- if (handle === this.agent.activeResourceRunId)
9471
- // Awaiting the abort unboundedly would let a provider or tool that ignores
9472
- // cancellation defer the grace forever, so the proof races the fixed grace
9473
- // independently of abort completion.
9474
- void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
9475
-
9476
- const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
9477
- if (proof.status === "unfenced") {
9478
- // Detach the unsettled resources so they can never re-enter this run; their real
9479
- // promises still release them inside the ledger.
9480
- this.agent.resourceLedger.quarantine(handle);
9810
+ const aborted = this.#runCancellationDomains.abort(handle);
9811
+ if (!aborted.ok) {
9812
+ if (aborted.reason === "quarantined") {
9813
+ return await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 });
9814
+ }
9815
+ return { status: "unfenced", reason: "unknown_run", pending: [] };
9481
9816
  }
9817
+ if (handle === this.agent.activeResourceRunId) this.agent.abort();
9818
+ const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
9819
+ if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
9482
9820
  return proof;
9483
9821
  }
9484
9822
 
@@ -11870,6 +12208,7 @@ export class AgentSession {
11870
12208
  skipAbortedCheck = true,
11871
12209
  onTerminalOverflowNoop?: () => void,
11872
12210
  resourceRunId?: string,
12211
+ ownershipSignal?: AbortSignal,
11873
12212
  ): Promise<boolean> {
11874
12213
  // Safety stops are terminal and must not trigger context maintenance.
11875
12214
  if (
@@ -11913,7 +12252,8 @@ export class AgentSession {
11913
12252
  }
11914
12253
 
11915
12254
  // Try context promotion first - switch to a larger model and retry without compacting
11916
- const promoted = await this.#tryContextPromotion(assistantMessage);
12255
+ const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
12256
+ if (ownershipSignal?.aborted) return false;
11917
12257
  if (promoted) {
11918
12258
  // Retry on the promoted (larger) model without compacting
11919
12259
  this.#scheduleAgentContinue({
@@ -11938,6 +12278,7 @@ export class AgentSession {
11938
12278
  }
11939
12279
  },
11940
12280
  resourceRunId,
12281
+ signal: ownershipSignal,
11941
12282
  });
11942
12283
  return "continuationScheduled" in status && status.continuationScheduled === true;
11943
12284
  }
@@ -11972,14 +12313,16 @@ export class AgentSession {
11972
12313
  autoCompactionOutputReserveTokens,
11973
12314
  )
11974
12315
  ) {
11975
- const pruneResult = await this.#pruneToolOutputs(undefined, true);
12316
+ const pruneResult = await this.#pruneToolOutputs(ownershipSignal, true);
12317
+ if (ownershipSignal?.aborted) return false;
11976
12318
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
11977
12319
  }
11978
12320
  if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
11979
12321
  // Try promotion first — if a larger model is available, switch instead of compacting
11980
- const promoted = await this.#tryContextPromotion(assistantMessage);
12322
+ const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
12323
+ if (ownershipSignal?.aborted) return false;
11981
12324
  if (!promoted) {
11982
- await this.#runAutoCompaction("threshold", false);
12325
+ await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
11983
12326
  }
11984
12327
  }
11985
12328
  return true;
@@ -13358,7 +13701,7 @@ export class AgentSession {
13358
13701
  });
13359
13702
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
13360
13703
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13361
- this.#scheduleAutoContinuePrompt(generation);
13704
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13362
13705
  }
13363
13706
 
13364
13707
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
@@ -13422,7 +13765,7 @@ export class AgentSession {
13422
13765
  }
13423
13766
  const overflowContinuationScheduled =
13424
13767
  !overflowNoopWouldReplay && willRetry && !continuationSkipReason
13425
- ? await this.#scheduleOverflowRetryContinuation(generation)
13768
+ ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
13426
13769
  : false;
13427
13770
  await this.#emitSessionEvent({
13428
13771
  type: "auto_compaction_end",
@@ -13445,6 +13788,7 @@ export class AgentSession {
13445
13788
  rescheduleOnBusy: true,
13446
13789
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
13447
13790
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
13791
+ resourceRunId: options?.resourceRunId,
13448
13792
  });
13449
13793
  return { kind: "skipped", continuationScheduled: true };
13450
13794
  }
@@ -13465,9 +13809,10 @@ export class AgentSession {
13465
13809
  rescheduleOnBusy: true,
13466
13810
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
13467
13811
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
13812
+ resourceRunId: options?.resourceRunId,
13468
13813
  });
13469
13814
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13470
- this.#scheduleAutoContinuePrompt(generation);
13815
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13471
13816
  }
13472
13817
  return { kind: "skipped" };
13473
13818
  }
@@ -13694,7 +14039,9 @@ export class AgentSession {
13694
14039
  this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
13695
14040
  }
13696
14041
  const overflowContinuationScheduled =
13697
- willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
14042
+ willRetry && !continuationSkipReason
14043
+ ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
14044
+ : false;
13698
14045
  await this.#emitSessionEvent({
13699
14046
  type: "auto_compaction_end",
13700
14047
  action,
@@ -13721,9 +14068,10 @@ export class AgentSession {
13721
14068
  rescheduleOnBusy: true,
13722
14069
  onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
13723
14070
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
14071
+ resourceRunId: options?.resourceRunId,
13724
14072
  });
13725
14073
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13726
- this.#scheduleAutoContinuePrompt(generation);
14074
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13727
14075
  }
13728
14076
  return { kind: "compacted" };
13729
14077
  } catch (error) {
@@ -13788,7 +14136,7 @@ export class AgentSession {
13788
14136
 
13789
14137
  #isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
13790
14138
  if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
13791
- const errorMessage = message.errorMessage ?? "";
14139
+ const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
13792
14140
  if (message.api === "anthropic-messages") {
13793
14141
  return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
13794
14142
  }
@@ -13815,7 +14163,8 @@ export class AgentSession {
13815
14163
  #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
13816
14164
  if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
13817
14165
  const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
13818
- return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
14166
+ const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
14167
+ return timeoutMessages?.has(errorMessage) ?? false;
13819
14168
  }
13820
14169
 
13821
14170
  #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
@@ -14003,6 +14352,7 @@ export class AgentSession {
14003
14352
  if (this.#isTerminalProviderFirstEventTimeout(message)) {
14004
14353
  return "terminal";
14005
14354
  }
14355
+ if (isMessageOnlyFirstEventTimeout(message)) return "first_event_timeout";
14006
14356
  // Legacy providers that have not yet stamped the typed timeout fact retain
14007
14357
  // their existing bounded ollama-cloud behavior.
14008
14358
  if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
@@ -14173,20 +14523,37 @@ export class AgentSession {
14173
14523
  return {
14174
14524
  type: "maintenance",
14175
14525
  continuation: async ownership => {
14176
- if (!ownership.isCurrent()) return;
14177
- let terminalized = false;
14178
- const successorScheduled = await this.#checkCompaction(outcome.message, true, () => {
14179
- terminalized = true;
14180
- this.agent.requestRunTerminal(ownership.logicalRunId, {
14526
+ if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
14527
+ const resourceRunId = String(ownership.logicalRunId);
14528
+ const previousLease = this.#postPromptLeases.get(resourceRunId);
14529
+ this.#postPromptLeases.set(resourceRunId, ownership.lease);
14530
+ try {
14531
+ let terminalized = false;
14532
+ const successorScheduled = await this.#checkCompaction(
14533
+ outcome.message,
14534
+ true,
14535
+ () => {
14536
+ terminalized = true;
14537
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14538
+ stopReason: "error",
14539
+ messages: [outcome.message],
14540
+ });
14541
+ },
14542
+ resourceRunId,
14543
+ ownership.lease.signal,
14544
+ );
14545
+ if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
14546
+ return;
14547
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14181
14548
  stopReason: "error",
14182
14549
  messages: [outcome.message],
14183
14550
  });
14184
- });
14185
- if (terminalized || successorScheduled || !ownership.isCurrent()) return;
14186
- this.agent.requestRunTerminal(ownership.logicalRunId, {
14187
- stopReason: "error",
14188
- messages: [outcome.message],
14189
- });
14551
+ } finally {
14552
+ if (this.#postPromptLeases.get(resourceRunId) === ownership.lease) {
14553
+ if (previousLease) this.#postPromptLeases.set(resourceRunId, previousLease);
14554
+ else this.#postPromptLeases.delete(resourceRunId);
14555
+ }
14556
+ }
14190
14557
  },
14191
14558
  };
14192
14559
  }
@@ -14378,17 +14745,20 @@ export class AgentSession {
14378
14745
  // retry.enabled=false always surfaces immediately, matching the explicit
14379
14746
  // user opt-out.
14380
14747
  if (!managedFallback && !retrySettings.enabled) return false;
14381
- const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
14382
- if (classification === "first_event_timeout") {
14748
+ const classification = this.#classifyErrorForRetry(message);
14749
+ const firstEventTimeout = classification === "first_event_timeout";
14750
+ if (!managedFallback && firstEventTimeout) {
14383
14751
  this.#firstEventTimeoutRetryStartedAt ??= Date.now();
14384
14752
  }
14385
14753
  // First-event timeouts are replay-safe only before any model progress,
14386
14754
  // abort, or extension/provider lifecycle participation.
14387
- if (
14388
- classification === "first_event_timeout" &&
14389
- (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
14390
- ) {
14391
- return false;
14755
+ if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
14756
+ return managedOutcome
14757
+ ? {
14758
+ type: "terminal",
14759
+ terminal: { stopReason: "error", messages: [message] },
14760
+ }
14761
+ : false;
14392
14762
  }
14393
14763
  const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
14394
14764
  if (!trigger) {
@@ -14415,12 +14785,14 @@ export class AgentSession {
14415
14785
  // capacity-overload event.
14416
14786
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
14417
14787
  const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
14788
+ const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
14418
14789
  // Content-free Codex overload is replay-safe by the same reasoning as
14419
14790
  // credential rotation: no partial output means no observable state.
14420
14791
  const canReplayCodexOverload = bareDefaultCodexOverload;
14421
14792
  if (
14422
14793
  (!canReplayCodexOverload &&
14423
14794
  !this.#isTypedFirstEventTimeout(message) &&
14795
+ !bareDefaultWrappedFirstEventTimeout &&
14424
14796
  (hasBareDefaultRetryDisqualifyingFacts(message) ||
14425
14797
  (classification !== "transient" && classification !== "first_event_timeout") ||
14426
14798
  !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
@@ -14429,7 +14801,7 @@ export class AgentSession {
14429
14801
  return false;
14430
14802
  }
14431
14803
  }
14432
- const legacyUnbounded = classification === "transient";
14804
+ const legacyUnbounded = !managedFallback && classification === "transient";
14433
14805
  const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
14434
14806
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
14435
14807
  let outcome = managedFallback
@@ -14496,7 +14868,7 @@ export class AgentSession {
14496
14868
  : this.#fallbackExhaustionError(controller);
14497
14869
  this.emitNotice("error", errorMessage, "fallback");
14498
14870
  if (managedOutcome && ownership) {
14499
- this.agent.requestRunTerminal(ownership.logicalRunId, {
14871
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14500
14872
  stopReason: "exhausted",
14501
14873
  messages: [this.#managedFallbackExhaustionMessage(message, errorMessage)],
14502
14874
  });
@@ -14522,12 +14894,11 @@ export class AgentSession {
14522
14894
  await this.#emitSessionEvent({
14523
14895
  type: "auto_retry_start",
14524
14896
  attempt: this.#retryAttempt,
14525
- maxAttempts:
14526
- classification === "first_event_timeout"
14897
+ maxAttempts: managedFallback
14898
+ ? controller.maxAttempts
14899
+ : firstEventTimeout
14527
14900
  ? retrySettings.maxRetries + 1
14528
- : managedFallback
14529
- ? controller.maxAttempts
14530
- : retrySettings.maxRetries,
14901
+ : retrySettings.maxRetries,
14531
14902
  delayMs,
14532
14903
  errorMessage,
14533
14904
  unbounded: managedFallback ? false : legacyUnbounded,
@@ -14548,8 +14919,13 @@ export class AgentSession {
14548
14919
  }
14549
14920
  this.agent.replaceMessages(messages.slice(0, end));
14550
14921
  }
14922
+ const retrySignal = ownership
14923
+ ? AbortSignal.any([retryAbortController.signal, ownership.lease.signal])
14924
+ : retryAbortController.signal;
14925
+ const ownershipCancelled = () =>
14926
+ Boolean(ownership && (!ownership.isCurrent() || ownership.lease.signal.aborted));
14551
14927
  try {
14552
- await scheduler.wait(delayMs, { signal: retryAbortController.signal });
14928
+ await scheduler.wait(delayMs, { signal: retrySignal });
14553
14929
  } catch {
14554
14930
  if (this.#retryAbortController !== retryAbortController) return;
14555
14931
  this.#retryAbortController = undefined;
@@ -14568,7 +14944,10 @@ export class AgentSession {
14568
14944
  return;
14569
14945
  }
14570
14946
  }
14571
- if (retryAbortController.signal.aborted && !this.#retryNowRequested) {
14947
+ if (
14948
+ (retryAbortController.signal.aborted || ownershipCancelled()) &&
14949
+ !(this.#retryNowRequested && !ownershipCancelled())
14950
+ ) {
14572
14951
  if (this.#retryAbortController !== retryAbortController) return;
14573
14952
  this.#retryAbortController = undefined;
14574
14953
  const attempt = this.#retryAttempt;
@@ -14588,7 +14967,7 @@ export class AgentSession {
14588
14967
  if (managedOutcome) {
14589
14968
  try {
14590
14969
  await this.#checkEstimatedContextBeforePrompt();
14591
- if (!ownership?.isCurrent()) {
14970
+ if (ownershipCancelled()) {
14592
14971
  const attempt = this.#retryAttempt;
14593
14972
  this.#retryAttempt = 0;
14594
14973
  await this.#emitSessionEvent({
@@ -14619,10 +14998,13 @@ export class AgentSession {
14619
14998
  }
14620
14999
  }
14621
15000
 
15001
+ const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
14622
15002
  this.#scheduleAgentContinue({
14623
15003
  delayMs: 1,
14624
15004
  generation,
14625
15005
  allowDuringCancelAndSubmit: true,
15006
+ suppressPredecessorAgentEnd: resourceRunId !== undefined,
15007
+ resourceRunId,
14626
15008
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
14627
15009
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
14628
15010
  });
@@ -14677,7 +15059,11 @@ export class AgentSession {
14677
15059
 
14678
15060
  async #promptAgentWithIdleRetry(
14679
15061
  messages: AgentMessage[],
14680
- options?: { toolChoice?: ToolChoice; fallbackManaged?: boolean; onRunAccepted?: () => void },
15062
+ options?: {
15063
+ toolChoice?: ToolChoice;
15064
+ fallbackManaged?: boolean;
15065
+ onRunAccepted?: (handle: AttemptRunHandle) => void;
15066
+ },
14681
15067
  predecessorAgentEndHold?: symbol,
14682
15068
  ): Promise<void> {
14683
15069
  const deadline = Date.now() + 30_000;
@@ -14690,6 +15076,7 @@ export class AgentSession {
14690
15076
  continuationHold = undefined;
14691
15077
  try {
14692
15078
  await this.agent.prompt(messages, options);
15079
+ this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
14693
15080
  return;
14694
15081
  } catch (error) {
14695
15082
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
@@ -15441,19 +15828,30 @@ export class AgentSession {
15441
15828
  !rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
15442
15829
  const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
15443
15830
  const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
15831
+ let sideAttempt: { scope: AttemptScope; dispose: () => void } | undefined;
15444
15832
  try {
15445
15833
  const model = this.model;
15446
15834
  if (!model) throw new Error("No active model on session");
15447
15835
  const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
15448
15836
  if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
15837
+ sideAttempt = this.agent.mintSideAttemptScope();
15838
+ const sideScope = sideAttempt.scope;
15839
+ this.#attemptRecordStore.register(sideScope);
15840
+ this.#attemptRecordStore.establishClean(sideScope);
15449
15841
  const prependMessages = prependMessagesValid()
15450
15842
  ? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
15451
15843
  : undefined;
15452
15844
  let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
15453
- let llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
15845
+ let llmMessages = await awaitEphemeralAbort(
15846
+ this.convertMessagesToLlm(snapshot, args.signal, sideScope),
15847
+ args.signal,
15848
+ );
15454
15849
  if (prependMessages && !prependMessagesValid()) {
15455
15850
  snapshot = this.#buildEphemeralSnapshot(args.promptText);
15456
- llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
15851
+ llmMessages = await awaitEphemeralAbort(
15852
+ this.convertMessagesToLlm(snapshot, args.signal, sideScope),
15853
+ args.signal,
15854
+ );
15457
15855
  }
15458
15856
  const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
15459
15857
  const ephemeralSessionId = crypto.randomUUID();
@@ -15474,6 +15872,7 @@ export class AgentSession {
15474
15872
  toolChoice: "none",
15475
15873
  },
15476
15874
  model.provider,
15875
+ sideScope,
15477
15876
  );
15478
15877
  args.signal?.throwIfAborted();
15479
15878
  let replyText = "";
@@ -15501,6 +15900,10 @@ export class AgentSession {
15501
15900
  }
15502
15901
  return { replyText: replyText.trim(), assistantMessage };
15503
15902
  } finally {
15903
+ if (sideAttempt) {
15904
+ sideAttempt.dispose();
15905
+ this.#attemptRecordStore.retire(sideAttempt.scope);
15906
+ }
15504
15907
  if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
15505
15908
  }
15506
15909
  }
@@ -15514,8 +15917,15 @@ export class AgentSession {
15514
15917
  }
15515
15918
  const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
15516
15919
  if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
15920
+ const sideAttempt = this.agent.mintSideAttemptScope();
15921
+ this.#attemptRecordStore.register(sideAttempt.scope);
15922
+ this.#attemptRecordStore.establishClean(sideAttempt.scope);
15517
15923
  const scope = args.turn.scope;
15518
- if (!scope) throw new Error("The /btw conversation scope was scrubbed.");
15924
+ if (!scope) {
15925
+ sideAttempt.dispose();
15926
+ this.#attemptRecordStore.retire(sideAttempt.scope);
15927
+ throw new Error("The /btw conversation scope was scrubbed.");
15928
+ }
15519
15929
  const messages = scope.messages.map(message => ({
15520
15930
  role: message.role,
15521
15931
  content: [{ type: "text" as const, text: message.text }],
@@ -15548,6 +15958,7 @@ export class AgentSession {
15548
15958
  requestMaxRetries: 0,
15549
15959
  streamMaxRetries: 0,
15550
15960
  streamFirstEventTimeoutMs: 0,
15961
+ attemptScope: sideAttempt.scope,
15551
15962
  };
15552
15963
  const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
15553
15964
  let replyText = "";
@@ -15607,6 +16018,8 @@ export class AgentSession {
15607
16018
  if (idleTimer) clearTimeout(idleTimer);
15608
16019
  clearTimeout(totalTimer);
15609
16020
  void iterator.return?.().catch(() => undefined);
16021
+ sideAttempt.dispose();
16022
+ this.#attemptRecordStore.retire(sideAttempt.scope);
15610
16023
  }
15611
16024
  }
15612
16025