@gajae-code/coding-agent 0.12.4 → 0.12.6

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 (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -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,
@@ -342,7 +349,12 @@ import {
342
349
  import { assertEditableFile } from "../tools/auto-generated-guard";
343
350
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
344
351
  import type { CheckpointState } from "../tools/checkpoint";
345
- import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
352
+ import {
353
+ createArtifactFailurePreview,
354
+ type OutputMeta,
355
+ outputMeta,
356
+ wrapToolWithMetaNotice,
357
+ } from "../tools/output-meta";
346
358
  import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
347
359
  import { registerResourceGcSession } from "../tools/resource-gc";
348
360
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
@@ -564,7 +576,11 @@ export interface AgentSessionConfig {
564
576
  /** Tool-session factory context used to lazily attach workflow-gate-only tools. */
565
577
  workflowGateToolSession?: ToolSession;
566
578
  /** Current session pre-LLM message transform pipeline */
567
- transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
579
+ transformContext?: (
580
+ messages: AgentMessage[],
581
+ signal?: AbortSignal,
582
+ scope?: AttemptScopeRef,
583
+ ) => AgentMessage[] | Promise<AgentMessage[]>;
568
584
  /** Provider payload hook used by the active session request path */
569
585
  onPayload?: SimpleStreamOptions["onPayload"];
570
586
  /** Provider response hook used by the active session request path */
@@ -869,14 +885,17 @@ type RetryErrorClassification =
869
885
 
870
886
  const BARE_DEFAULT_WATCHDOG_ERROR =
871
887
  /^(?:[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)$/;
888
+ const PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = "Provider stream timed out while waiting for the first event";
889
+ const WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR = `Error: ${PROVIDER_FIRST_EVENT_TIMEOUT_ERROR}`;
890
+ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream timed out while waiting for first event";
872
891
  const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
873
892
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
874
893
  "anthropic-messages": new Set([
875
- "Provider stream timed out while waiting for the first event",
894
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
876
895
  "Anthropic stream timed out while waiting for the first event",
877
896
  ]),
878
897
  "openai-completions": new Set([
879
- "Provider stream timed out while waiting for the first event",
898
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
880
899
  "OpenAI completions stream timed out while waiting for the first event",
881
900
  ]),
882
901
  } as const;
@@ -884,11 +903,11 @@ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
884
903
  const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
885
904
  const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
886
905
  "openai-responses": new Set([
887
- "Provider stream timed out while waiting for the first event",
906
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
888
907
  "OpenAI responses stream timed out while waiting for the first event",
889
908
  ]),
890
909
  "openai-completions": new Set([
891
- "Provider stream timed out while waiting for the first event",
910
+ PROVIDER_FIRST_EVENT_TIMEOUT_ERROR,
892
911
  "OpenAI completions stream timed out while waiting for the first event",
893
912
  ]),
894
913
  } as const;
@@ -906,6 +925,25 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
906
925
  (facts.headers !== undefined && Object.keys(facts.headers).length > 0)
907
926
  );
908
927
  }
928
+ function terminalProviderFirstEventTimeoutIdentity(errorMessage: string): string {
929
+ return errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
930
+ ? PROVIDER_FIRST_EVENT_TIMEOUT_ERROR
931
+ : errorMessage;
932
+ }
933
+ function isMessageOnlyFirstEventTimeout(message: AssistantMessage): boolean {
934
+ if (hasBareDefaultRetryDisqualifyingFacts(message)) return false;
935
+ return (
936
+ message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR ||
937
+ message.errorMessage === PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR
938
+ );
939
+ }
940
+
941
+ function isBareDefaultWrappedFirstEventTimeout(message: AssistantMessage): boolean {
942
+ return (
943
+ message.errorMessage === WRAPPED_PROVIDER_FIRST_EVENT_TIMEOUT_ERROR &&
944
+ !hasBareDefaultRetryDisqualifyingFacts(message)
945
+ );
946
+ }
909
947
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
910
948
  return (
911
949
  message.api === "openai-codex-responses" &&
@@ -1024,6 +1062,102 @@ function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
1024
1062
  return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
1025
1063
  }
1026
1064
 
1065
+ const PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS = 500;
1066
+ const PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES = 2;
1067
+
1068
+ type PreAdmissionArtifactSaveOutcome =
1069
+ | { kind: "saved"; saved: Awaited<ReturnType<SessionManager["saveArtifactReceipt"]>> }
1070
+ | { kind: "failed"; error: unknown };
1071
+
1072
+ interface PreAdmissionArtifactSaveAdmission {
1073
+ readonly pending: Set<Promise<PreAdmissionArtifactSaveOutcome>>;
1074
+ }
1075
+
1076
+ /**
1077
+ * Pre-admission spills are owned by their concrete SessionManager. A stalled
1078
+ * save remains admitted until its promise settles, so timeout does not turn
1079
+ * each subsequent tool result into another retained full payload.
1080
+ */
1081
+ const preAdmissionArtifactSaveAdmissions = new WeakMap<SessionManager, PreAdmissionArtifactSaveAdmission>();
1082
+
1083
+ function preAdmissionArtifactSaveAdmissionFor(sessionManager: SessionManager): PreAdmissionArtifactSaveAdmission {
1084
+ let admission = preAdmissionArtifactSaveAdmissions.get(sessionManager);
1085
+ if (!admission) {
1086
+ admission = { pending: new Set() };
1087
+ preAdmissionArtifactSaveAdmissions.set(sessionManager, admission);
1088
+ }
1089
+ return admission;
1090
+ }
1091
+
1092
+ function beginPreAdmissionArtifactSave(
1093
+ sessionManager: SessionManager,
1094
+ content: string,
1095
+ toolType: string,
1096
+ ): { kind: "capacity" } | { kind: "started"; outcome: Promise<PreAdmissionArtifactSaveOutcome> } {
1097
+ const admission = preAdmissionArtifactSaveAdmissionFor(sessionManager);
1098
+ if (admission.pending.size >= PRE_ADMISSION_ARTIFACT_MAX_PENDING_SAVES) return { kind: "capacity" };
1099
+
1100
+ let outcome: Promise<PreAdmissionArtifactSaveOutcome>;
1101
+ try {
1102
+ outcome = sessionManager.saveArtifactReceipt(content, toolType).then(
1103
+ saved => ({ kind: "saved" as const, saved }),
1104
+ error => ({ kind: "failed" as const, error }),
1105
+ );
1106
+ } catch (error) {
1107
+ return { kind: "started", outcome: Promise.resolve({ kind: "failed", error }) };
1108
+ }
1109
+ admission.pending.add(outcome);
1110
+ void outcome.then(
1111
+ () => admission.pending.delete(outcome),
1112
+ () => admission.pending.delete(outcome),
1113
+ );
1114
+ return { kind: "started", outcome };
1115
+ }
1116
+
1117
+ function replaceToolResultWithArtifactFailurePreview(
1118
+ message: ToolResultMessage,
1119
+ fullText: string,
1120
+ maxInlineBytes: number | undefined,
1121
+ diagnostic: string,
1122
+ ): void {
1123
+ const preview = createArtifactFailurePreview(fullText, maxInlineBytes, diagnostic);
1124
+ const existingDetails =
1125
+ message.details && typeof message.details === "object" ? (message.details as Record<string, unknown>) : {};
1126
+ const existingMeta =
1127
+ existingDetails.meta && typeof existingDetails.meta === "object" ? (existingDetails.meta as OutputMeta) : {};
1128
+ const existingTruncation = existingMeta.truncation;
1129
+ const totalLines = fullText.length > 0 ? fullText.split("\n").length : 0;
1130
+ const outputLines = preview.text.length > 0 ? preview.text.split("\n").length : 0;
1131
+ const outputBytes = Buffer.byteLength(preview.text, "utf-8");
1132
+ const truncation: NonNullable<OutputMeta["truncation"]> = {
1133
+ ...(existingTruncation ?? {}),
1134
+ direction: "tail",
1135
+ truncatedBy: "bytes",
1136
+ noticeOwner: undefined,
1137
+ totalLines,
1138
+ totalBytes: Buffer.byteLength(fullText, "utf-8"),
1139
+ outputLines,
1140
+ outputBytes,
1141
+ maxBytes: maxInlineBytes,
1142
+ shownRange: undefined,
1143
+ headRange: undefined,
1144
+ tailRange: undefined,
1145
+ nextOffset: undefined,
1146
+ artifactId: undefined,
1147
+ artifactVerified: false,
1148
+ sourceCaptureIncomplete: true,
1149
+ artifactFailureDiagnostic: diagnostic,
1150
+ };
1151
+ message.details = {
1152
+ ...existingDetails,
1153
+ meta: { ...existingMeta, truncation },
1154
+ };
1155
+ message.content = [
1156
+ ...message.content.filter((block): block is ImageContent => block.type === "image"),
1157
+ { type: "text", text: preview.text },
1158
+ ];
1159
+ }
1160
+
1027
1161
  function summarizeAgentBashArtifactSave(
1028
1162
  artifactId: string,
1029
1163
  originalText: string,
@@ -1629,6 +1763,49 @@ export class StreamingEditFileCache {
1629
1763
  }
1630
1764
  }
1631
1765
  type SessionAdmissionKind = "prompt" | "selection";
1766
+ class SessionRunCancellationDomainBridge implements RunCancellationDomainBridge {
1767
+ #domains = new Map<string, { domain: RunCancellationDomain; controller: AbortController }>();
1768
+ #released = new Set<string>();
1769
+ #quarantined = new Set<string>();
1770
+
1771
+ open(resourceRunId: string) {
1772
+ if (this.#quarantined.has(resourceRunId)) return { ok: false as const, reason: "quarantined" as const };
1773
+ const existing = this.#domains.get(resourceRunId);
1774
+ if (existing) return { ok: true as const, domain: existing.domain, created: false };
1775
+ if (this.#released.has(resourceRunId)) return { ok: false as const, reason: "duplicate_identity" as const };
1776
+ const controller = new AbortController();
1777
+ const domain: RunCancellationDomain = { resourceRunId, signal: controller.signal };
1778
+ this.#domains.set(resourceRunId, { domain, controller });
1779
+ return { ok: true as const, domain, created: true };
1780
+ }
1781
+
1782
+ lookup(resourceRunId: string): RunCancellationDomain | undefined {
1783
+ return this.#domains.get(resourceRunId)?.domain;
1784
+ }
1785
+
1786
+ abort(resourceRunId: string, reason?: unknown) {
1787
+ const record = this.#domains.get(resourceRunId);
1788
+ if (!record)
1789
+ return {
1790
+ ok: false as const,
1791
+ reason: this.#quarantined.has(resourceRunId) ? ("quarantined" as const) : ("unknown_run" as const),
1792
+ };
1793
+ const newlyAborted = !record.controller.signal.aborted;
1794
+ if (newlyAborted) record.controller.abort(reason);
1795
+ return { ok: true as const, newlyAborted };
1796
+ }
1797
+
1798
+ release(resourceRunId: string, disposition: "settled" | "quarantined"): void {
1799
+ const record = this.#domains.get(resourceRunId);
1800
+ if (!record) return;
1801
+ if (disposition === "quarantined") {
1802
+ this.#quarantined.add(resourceRunId);
1803
+ if (!record.controller.signal.aborted) record.controller.abort();
1804
+ }
1805
+ this.#domains.delete(resourceRunId);
1806
+ this.#released.add(resourceRunId);
1807
+ }
1808
+ }
1632
1809
 
1633
1810
  type SessionAdmissionEntry = {
1634
1811
  kind: SessionAdmissionKind;
@@ -1888,6 +2065,15 @@ export class AgentSession {
1888
2065
  #newSessionTransition: Promise<boolean> | undefined;
1889
2066
  // Extension system
1890
2067
  #extensionRunner: ExtensionRunner | undefined = undefined;
2068
+ #attemptAuthority!: AttemptScopeAuthority;
2069
+ #attemptRecordStore!: AttemptRecordStore;
2070
+ #activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
2071
+ #acceptRunHandle(handle: AttemptRunHandle | undefined): void {
2072
+ // Integration doubles can accept a run without minting a handle; keep the
2073
+ // previously recorded run id rather than throwing inside the callback.
2074
+ if (!handle) return;
2075
+ this.#activeLogicalRunId = handle.logicalRunId;
2076
+ }
1891
2077
 
1892
2078
  #turnIndex = 0;
1893
2079
  #workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
@@ -1917,7 +2103,11 @@ export class AgentSession {
1917
2103
  // Tool registry and prompt builder for extensions
1918
2104
  #toolRegistry: Map<string, AgentTool>;
1919
2105
  #workflowGateToolSession: ToolSession | undefined;
1920
- #transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
2106
+ #transformContext: (
2107
+ messages: AgentMessage[],
2108
+ signal?: AbortSignal,
2109
+ scope?: AttemptScopeRef,
2110
+ ) => AgentMessage[] | Promise<AgentMessage[]>;
1921
2111
  #onPayload: SimpleStreamOptions["onPayload"] | undefined;
1922
2112
  #onResponse: SimpleStreamOptions["onResponse"] | undefined;
1923
2113
  #onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
@@ -2009,6 +2199,10 @@ export class AgentSession {
2009
2199
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
2010
2200
  #postPromptTasksResolve: (() => void) | undefined = undefined;
2011
2201
  #postPromptTasksAbortController = new AbortController();
2202
+ #runCancellationDomains = new SessionRunCancellationDomainBridge();
2203
+ #postPromptLeases = new Map<string, RunResourceProducerLease>();
2204
+ #runResourceLeaseContext = new AsyncLocalStorage<RunResourceProducerLease>();
2205
+ #agentSessionClaimKey = {};
2012
2206
 
2013
2207
  #streamingEditAbortTriggered = false;
2014
2208
  #streamingEditCheckedLineCounts = new Map<string, number>();
@@ -2051,6 +2245,7 @@ export class AgentSession {
2051
2245
  // the successor or proves it cannot. Holds prevent a false idle event while
2052
2246
  // preserving the predecessor for cancellation and preflight failures.
2053
2247
  #pendingAgentEndContinuationHolds = new Map<symbol, AgentSessionEvent>();
2248
+ #deferredAgentEndLeases = new WeakMap<AgentSessionEvent, RunResourceProducerLease>();
2054
2249
  #sessionSettlementPromise: Promise<void> | undefined;
2055
2250
  #sessionSettlementResolve: (() => void) | undefined;
2056
2251
  #agentEndPublicationInFlight = 0;
@@ -2334,6 +2529,17 @@ export class AgentSession {
2334
2529
  this.#restoreDeferredAgentEndAfterContinuationFailure(pending);
2335
2530
  }
2336
2531
 
2532
+ #releaseDeferredAgentEndLease(pending: AgentSessionEvent | undefined): void {
2533
+ if (!pending) return;
2534
+ const lease = this.#deferredAgentEndLeases.get(pending);
2535
+ if (!lease) return;
2536
+ this.#deferredAgentEndLeases.delete(pending);
2537
+ if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
2538
+ this.#postPromptLeases.delete(lease.resourceRunId);
2539
+ }
2540
+ lease.closeDiscovery();
2541
+ }
2542
+
2337
2543
  #isSessionSettlementPending(): boolean {
2338
2544
  return (
2339
2545
  this.#promptInFlightCount > 0 ||
@@ -2397,13 +2603,26 @@ export class AgentSession {
2397
2603
  return;
2398
2604
  }
2399
2605
  this.#pendingAgentEndEmit = undefined;
2606
+ const lease = this.#deferredAgentEndLeases.get(pending);
2607
+ if (lease) this.#deferredAgentEndLeases.delete(pending);
2400
2608
  this.#agentEndPublicationInFlight++;
2401
- this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending);
2609
+ this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease);
2402
2610
  void this.#agentEndPublicationPromise;
2403
2611
  }
2404
2612
 
2405
- async #publishDeferredAgentEnd(pending: AgentSessionEvent): Promise<void> {
2406
- try {
2613
+ async #publishDeferredAgentEnd(
2614
+ pending: AgentSessionEvent,
2615
+ lease: RunResourceProducerLease | undefined,
2616
+ ): Promise<void> {
2617
+ let extensionDelivery: Promise<void> | undefined;
2618
+ const releaseLease = () => {
2619
+ if (!lease) return;
2620
+ if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
2621
+ this.#postPromptLeases.delete(lease.resourceRunId);
2622
+ }
2623
+ lease.closeDiscovery();
2624
+ };
2625
+ const publish = async () => {
2407
2626
  // Worker integration is first-party lifecycle persistence, not an extension
2408
2627
  // hook. Make it durable before publishing the terminal boundary while user
2409
2628
  // extension delivery remains asynchronous.
@@ -2411,10 +2630,21 @@ export class AgentSession {
2411
2630
  // Persist before notifying synchronous subscribers: a subscriber may start a
2412
2631
  // successor prompt from agent_end, whose running state must serialize after
2413
2632
  // this terminal boundary rather than be overwritten by it.
2414
- this.#persistRuntimeStateInBackground(pending);
2633
+ void this.#persistRuntimeStateInBackground(pending);
2415
2634
  this.#emit(pending);
2416
- void this.#queueExtensionEvent(pending, undefined, true);
2635
+ extensionDelivery = this.#queueExtensionEvent(
2636
+ pending,
2637
+ undefined,
2638
+ true,
2639
+ (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
2640
+ );
2641
+ };
2642
+ try {
2643
+ if (lease) await this.#runResourceLeaseContext.run(lease, publish);
2644
+ else await publish();
2417
2645
  } finally {
2646
+ if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
2647
+ else releaseLease();
2418
2648
  this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
2419
2649
  this.#resolveSessionSettlement();
2420
2650
  }
@@ -2422,6 +2652,7 @@ export class AgentSession {
2422
2652
 
2423
2653
  constructor(config: AgentSessionConfig) {
2424
2654
  this.agent = config.agent;
2655
+ this.agent.bindRunCancellationDomainBridge(this.#runCancellationDomains, this.#agentSessionClaimKey);
2425
2656
  this.sessionManager = config.sessionManager;
2426
2657
  this.settings = config.settings;
2427
2658
  this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
@@ -2471,6 +2702,11 @@ export class AgentSession {
2471
2702
  this.#promptTemplates = config.promptTemplates ?? [];
2472
2703
  this.#slashCommands = config.slashCommands ?? [];
2473
2704
  this.#extensionRunner = config.extensionRunner;
2705
+ this.#attemptAuthority = this.agent.getAttemptScopeAuthority();
2706
+ this.#attemptRecordStore = new AttemptRecordStore(this.#attemptAuthority);
2707
+ if (this.#extensionRunner && typeof this.#extensionRunner.setAttemptRecordStore === "function") {
2708
+ this.#extensionRunner.setAttemptRecordStore(this.#attemptRecordStore);
2709
+ }
2474
2710
  this.#skills = config.skills ?? [];
2475
2711
  this.#skillWarnings = config.skillWarnings ?? [];
2476
2712
  this.#customCommands = config.customCommands ?? [];
@@ -2483,7 +2719,10 @@ export class AgentSession {
2483
2719
  this.#workflowGateToolSession = config.workflowGateToolSession;
2484
2720
  this.#requestedToolNames = config.requestedToolNames;
2485
2721
  this.#transformContext = config.transformContext ?? (messages => messages);
2486
- this.#onPayload = config.onPayload;
2722
+ const configuredOnPayload = config.onPayload;
2723
+ this.#onPayload = configuredOnPayload
2724
+ ? (payload, model, scope) => configuredOnPayload(payload, model, scope)
2725
+ : undefined;
2487
2726
  this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
2488
2727
  // Avoid wrapping in an `async` closure when no user callback is configured: the
2489
2728
  // outer await on `#onResponse` (provider-response.ts) tolerates a sync void return,
@@ -2491,20 +2730,20 @@ export class AgentSession {
2491
2730
  // shows up as ~3.5% self time in streaming profiles.
2492
2731
  const configuredOnResponse = config.onResponse;
2493
2732
  this.#onResponse = configuredOnResponse
2494
- ? async (response, model) => {
2733
+ ? async (response, model, scope) => {
2495
2734
  this.rawSseDebugBuffer.recordResponse(response, model);
2496
- await configuredOnResponse(response, model);
2735
+ await configuredOnResponse(response, model, scope);
2497
2736
  }
2498
- : (response, model) => {
2737
+ : (response, model, _scope) => {
2499
2738
  this.rawSseDebugBuffer.recordResponse(response, model);
2500
2739
  };
2501
2740
  const configuredOnSseEvent = config.onSseEvent;
2502
2741
  this.#onSseEvent = configuredOnSseEvent
2503
- ? (event, model) => {
2742
+ ? (event, model, scope) => {
2504
2743
  this.rawSseDebugBuffer.recordEvent(event, model);
2505
- configuredOnSseEvent(event, model);
2744
+ configuredOnSseEvent(event, model, scope);
2506
2745
  }
2507
- : (event, model) => {
2746
+ : (event, model, _scope) => {
2508
2747
  this.rawSseDebugBuffer.recordEvent(event, model);
2509
2748
  };
2510
2749
  this.agent.setProviderResponseInterceptor(this.#onResponse);
@@ -3336,6 +3575,7 @@ export class AgentSession {
3336
3575
  event: AgentSessionEvent,
3337
3576
  turnGeneration?: number,
3338
3577
  workerIntegrationSettled = false,
3578
+ scope?: AttemptScopeRef,
3339
3579
  ): Promise<void> {
3340
3580
  // Streaming events observed after turn_end belong to no live extension turn.
3341
3581
  // Events already queued before that boundary must drain in FIFO order, unless
@@ -3351,7 +3591,7 @@ export class AgentSession {
3351
3591
  turnGeneration === undefined || turnGeneration === this.#extensionTurnGeneration;
3352
3592
  const emit = async () => {
3353
3593
  if (!belongsToCurrentTurn()) return;
3354
- await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled);
3594
+ await this.#emitExtensionEvent(event, belongsToCurrentTurn, workerIntegrationSettled, scope);
3355
3595
  };
3356
3596
  const queued = this.#queuedExtensionEvents.then(emit, emit);
3357
3597
  this.#queuedExtensionEvents = queued.catch(() => {});
@@ -3364,38 +3604,103 @@ export class AgentSession {
3364
3604
  }
3365
3605
 
3366
3606
  #trackAgentEvent = (event: AgentEvent): Promise<void> => {
3367
- const activePromptHandle = this.activePromptHandle;
3607
+ const terminalOwner = event.type === "agent_end" ? getAgentTerminalOwnerContext(event) : undefined;
3608
+ const maintenanceCheckpoint =
3609
+ event.type === "agent_end" && event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted";
3610
+ const terminalClaim =
3611
+ maintenanceCheckpoint || !terminalOwner
3612
+ ? undefined
3613
+ : this.agent.resourceLedger.claimProducer(
3614
+ terminalOwner.resourceRunId,
3615
+ terminalOwner.domain,
3616
+ this.#agentSessionClaimKey,
3617
+ );
3618
+ // A maintenance checkpoint is not a terminal publication. Capture the current
3619
+ // logical handle synchronously, then reserve its producer before this listener
3620
+ // creates any async work. True terminals must never fall back to this mutable
3621
+ // session state: their owner context is the only authority.
3622
+ const activePromptHandle = maintenanceCheckpoint
3623
+ ? this.activePromptHandle
3624
+ : event.type === "agent_end"
3625
+ ? terminalOwner?.resourceRunId
3626
+ : this.activePromptHandle;
3627
+ const domain = activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle) : undefined;
3628
+ const eventReservation = terminalClaim?.ok
3629
+ ? terminalClaim
3630
+ : activePromptHandle && domain
3631
+ ? this.agent.resourceLedger.reserveProducer(
3632
+ activePromptHandle,
3633
+ domain,
3634
+ "post_prompt",
3635
+ "agent-session-event",
3636
+ )
3637
+ : undefined;
3638
+ const eventLease = eventReservation?.ok ? eventReservation.lease : undefined;
3368
3639
  const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
3369
3640
  if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
3370
3641
  this.#agentEventHandlersInFlight++;
3371
3642
  const handler = (async (): Promise<void> => {
3372
3643
  try {
3373
- await this.#handleAgentEvent(event, activePromptHandle);
3644
+ // A terminal that carries owner context must win the exact producer claim;
3645
+ // a superseded run's late agent_end never falls back to a successor's
3646
+ // handle. Externally emitted terminals carry no owner context and own no
3647
+ // run resources, so they keep the ownerless handling path unless a
3648
+ // prompt-owned run is actually active.
3649
+ if (event.type === "agent_end" && !maintenanceCheckpoint) {
3650
+ if (terminalOwner ? !terminalClaim?.ok : this.activePromptHandle !== undefined) return;
3651
+ }
3652
+ // A prompt-owned event with a captured handle must have obtained an exact
3653
+ // producer lease before its handler was created. Failed ownership never
3654
+ // schedules prompt work or falls back to a successor's handle.
3655
+ if (activePromptHandle && !eventLease) return;
3656
+ if (eventLease && event.type === "agent_end")
3657
+ this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
3658
+ if (eventLease) {
3659
+ await this.#runResourceLeaseContext.run(eventLease, () =>
3660
+ this.#handleAgentEvent(event, activePromptHandle),
3661
+ );
3662
+ } else {
3663
+ await this.#handleAgentEvent(event, activePromptHandle);
3664
+ }
3374
3665
  } catch (error) {
3375
3666
  logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
3376
3667
  } finally {
3668
+ if (eventLease) {
3669
+ const pendingAgentEnd =
3670
+ event.type === "agent_end" && !maintenanceCheckpoint && this.#pendingAgentEndEmit === event
3671
+ ? this.#pendingAgentEndEmit
3672
+ : undefined;
3673
+ if (pendingAgentEnd) {
3674
+ this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
3675
+ } else {
3676
+ if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
3677
+ this.#postPromptLeases.delete(eventLease.resourceRunId);
3678
+ eventLease.closeDiscovery();
3679
+ }
3680
+ }
3377
3681
  this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
3378
3682
  this.#flushPendingAgentEnd();
3379
3683
  agentEndHandled?.resolve();
3380
3684
  }
3381
3685
  })();
3382
- if (activePromptHandle) {
3383
- this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
3384
- }
3686
+ if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
3385
3687
  return handler;
3386
3688
  };
3387
3689
 
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(() => {
3690
+ async #persistRuntimeStateInBackground(event: AgentSessionEvent): Promise<void> {
3691
+ try {
3692
+ await persistCoordinatorRuntimeStateFromEvent(event, {
3693
+ sessionId: this.sessionId,
3694
+ cwd: this.sessionManager.getCwd(),
3695
+ sessionFile: this.sessionManager.getSessionFile(),
3696
+ });
3697
+ } catch {
3394
3698
  logger.warn("Failed to persist coordinator runtime state", { event: event.type });
3395
- });
3699
+ }
3396
3700
  }
3397
3701
 
3398
3702
  async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
3703
+ const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
3399
3704
  if (event.type === "turn_start") {
3400
3705
  this.#extensionTurnGeneration++;
3401
3706
  this.#closedExtensionTurnGeneration = undefined;
@@ -3420,7 +3725,7 @@ export class AgentSession {
3420
3725
  this.#emit(event);
3421
3726
  if (this.#hasStreamingExtensionHandlers()) {
3422
3727
  __agentSessionPerfCounters.messageUpdateExtensionQueues += 1;
3423
- void this.#queueExtensionEvent(event, this.#extensionTurnGeneration);
3728
+ void this.#queueExtensionEvent(event, this.#extensionTurnGeneration, false, attemptScope);
3424
3729
  }
3425
3730
  return;
3426
3731
  }
@@ -3449,7 +3754,7 @@ export class AgentSession {
3449
3754
  if (event.type === "agent_end") {
3450
3755
  // Start the durable terminal write before synchronous subscribers can
3451
3756
  // re-enter prompt(), so a successor's running transition serializes after it.
3452
- void persistRuntimeState();
3757
+ await persistRuntimeState();
3453
3758
  this.#emit(event);
3454
3759
  await this.#emitExtensionEvent(event);
3455
3760
  return;
@@ -3477,39 +3782,101 @@ export class AgentSession {
3477
3782
  const textParts = message.content.flatMap(block => (block.type === "text" ? [block.text] : []));
3478
3783
  if (textParts.length === 0) return;
3479
3784
  const fullText = textParts.join("\n");
3785
+ const configuredInlineKiB = this.settings.get("tools.maxInlineResultBytes");
3786
+ const maxInlineBytes =
3787
+ configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : undefined;
3788
+ const totalBytes = Buffer.byteLength(fullText, "utf-8");
3789
+ const exceedsInlineCap = maxInlineBytes !== undefined && totalBytes > maxInlineBytes;
3480
3790
  const contextWindow = this.model?.contextWindow;
3481
3791
  const thresholdTokens = Math.min(
3482
3792
  8_000,
3483
3793
  contextWindow && contextWindow > 0 ? Math.floor(contextWindow * 0.05) : 8_000,
3484
3794
  );
3485
- if (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens) return;
3795
+ // The inline byte cap is an independent admission bound and must be checked
3796
+ // before token heuristics can opt this result out of spilling.
3797
+ if (!exceedsInlineCap && (thresholdTokens <= 0 || estimateTextTokensHeuristic(fullText) <= thresholdTokens))
3798
+ return;
3486
3799
 
3800
+ const saveAdmission = beginPreAdmissionArtifactSave(this.sessionManager, fullText, "tool-result");
3801
+ if (saveAdmission.kind === "capacity") {
3802
+ replaceToolResultWithArtifactFailurePreview(
3803
+ message,
3804
+ fullText,
3805
+ maxInlineBytes,
3806
+ "artifact save capacity exhausted; the previous save is still settling",
3807
+ );
3808
+ return;
3809
+ }
3487
3810
  try {
3488
- const artifactId = await this.sessionManager.saveArtifact(fullText, "tool-result");
3489
- if (!artifactId) return;
3811
+ const saveOutcome = await Promise.race([
3812
+ saveAdmission.outcome,
3813
+ Bun.sleep(PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS).then(() => ({ kind: "timeout" as const })),
3814
+ ]);
3815
+ if (saveOutcome.kind !== "saved" || !saveOutcome.saved) {
3816
+ const diagnostic =
3817
+ saveOutcome.kind === "timeout"
3818
+ ? `did not settle within ${PRE_ADMISSION_ARTIFACT_SAVE_WAIT_MS}ms`
3819
+ : saveOutcome.kind === "failed"
3820
+ ? boundAgentBashArtifactSaveDiagnostic(saveOutcome.error)
3821
+ : "artifact storage is unavailable";
3822
+ replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3823
+ return;
3824
+ }
3825
+ const saved = saveOutcome.saved;
3826
+ const existingDetails = message.details;
3827
+ const detailRecord =
3828
+ existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3829
+ const existingMeta =
3830
+ detailRecord.meta && typeof detailRecord.meta === "object" ? (detailRecord.meta as OutputMeta) : {};
3831
+ const existingTruncation = existingMeta.truncation;
3832
+ const priorLoss = existingTruncation !== undefined;
3490
3833
  const digest = crypto.createHash("sha256").update(fullText).digest("hex");
3491
- const preview = createPreAdmissionArtifactSpillPreview(fullText, artifactId, digest);
3834
+ const sourceCaptureIncomplete = priorLoss || !saved.complete || undefined;
3835
+ const preview = createPreAdmissionArtifactSpillPreview(fullText, saved.id, digest, {
3836
+ maxInlineBytes,
3837
+ fullOutput: saved.complete && !priorLoss,
3838
+ });
3492
3839
  const spillMeta = outputMeta()
3493
3840
  .truncationFromText(preview, {
3494
3841
  direction: "middle",
3495
3842
  totalLines: fullText.split("\n").length,
3496
- totalBytes: Buffer.byteLength(fullText, "utf-8"),
3497
- artifactId,
3843
+ totalBytes,
3844
+ artifactId: saved.id,
3845
+ artifactVerified: true,
3846
+ sourceCaptureIncomplete,
3847
+ ...(maxInlineBytes !== undefined ? { maxBytes: maxInlineBytes } : {}),
3498
3848
  })
3499
3849
  .get();
3500
- const existingDetails = message.details;
3501
- const detailRecord =
3502
- existingDetails && typeof existingDetails === "object" ? (existingDetails as Record<string, unknown>) : {};
3503
- const existingMeta =
3504
- detailRecord.meta && typeof detailRecord.meta === "object"
3505
- ? (detailRecord.meta as Record<string, unknown>)
3506
- : {};
3507
- message.details = { ...detailRecord, meta: { ...existingMeta, ...spillMeta } };
3850
+ const spillTruncation = spillMeta?.truncation;
3851
+ const artifactTruncatedBytes =
3852
+ Math.max(existingTruncation?.artifactTruncatedBytes ?? 0, saved.omittedBytes ?? 0) || undefined;
3853
+ const mergedTruncation = spillTruncation
3854
+ ? {
3855
+ ...existingTruncation,
3856
+ ...spillTruncation,
3857
+ artifactId: saved.id,
3858
+ artifactVerified: true,
3859
+ artifactTruncatedBytes,
3860
+ sourceTruncatedBytes: existingTruncation?.sourceTruncatedBytes,
3861
+ sourceCaptureIncomplete,
3862
+ columnDroppedBytes: existingTruncation?.columnDroppedBytes,
3863
+ shownRange: sourceCaptureIncomplete ? undefined : spillTruncation.shownRange,
3864
+ headRange: sourceCaptureIncomplete ? undefined : spillTruncation.headRange,
3865
+ tailRange: sourceCaptureIncomplete ? undefined : spillTruncation.tailRange,
3866
+ nextOffset: sourceCaptureIncomplete ? undefined : spillTruncation.nextOffset,
3867
+ }
3868
+ : existingTruncation;
3869
+ message.details = {
3870
+ ...detailRecord,
3871
+ meta: { ...existingMeta, ...spillMeta, truncation: mergedTruncation },
3872
+ };
3508
3873
  message.content = [
3509
3874
  ...message.content.filter((block): block is ImageContent => block.type === "image"),
3510
3875
  { type: "text", text: preview },
3511
3876
  ];
3512
3877
  } catch (error) {
3878
+ const diagnostic = boundAgentBashArtifactSaveDiagnostic(error);
3879
+ replaceToolResultWithArtifactFailurePreview(message, fullText, maxInlineBytes, diagnostic);
3513
3880
  logger.warn("Failed to spill oversized tool result before context admission", {
3514
3881
  toolName: message.toolName,
3515
3882
  error: error instanceof Error ? error.message : String(error),
@@ -3534,6 +3901,11 @@ export class AgentSession {
3534
3901
 
3535
3902
  /** Internal handler for agent events - shared by subscribe and reconnect */
3536
3903
  #handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
3904
+ const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
3905
+ if ((event.type === "agent_start" || event.type === "turn_start") && attemptScope) {
3906
+ this.#attemptRecordStore.register(attemptScope);
3907
+ this.#attemptRecordStore.establishClean(attemptScope);
3908
+ }
3537
3909
  if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
3538
3910
  if (
3539
3911
  event.type === "tool_execution_start" ||
@@ -4048,6 +4420,7 @@ export class AgentSession {
4048
4420
  generation: maintenanceGeneration,
4049
4421
  skipCompactionCheck: true,
4050
4422
  resourceRunId: activePromptHandle,
4423
+ maintenanceContinuation: true,
4051
4424
  });
4052
4425
  }
4053
4426
  return;
@@ -4128,10 +4501,11 @@ export class AgentSession {
4128
4501
  }
4129
4502
  this.#resolveRetry();
4130
4503
 
4131
- const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
4132
- this.#trackPostPromptTask(
4133
- compactionTask.then(() => undefined),
4134
- activePromptHandle,
4504
+ const compactionTask = this.#schedulePostPromptTask(
4505
+ async () => {
4506
+ await this.#checkCompaction(msg, true, undefined, activePromptHandle);
4507
+ },
4508
+ { resourceRunId: activePromptHandle },
4135
4509
  );
4136
4510
  await compactionTask;
4137
4511
  // Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
@@ -4196,29 +4570,52 @@ export class AgentSession {
4196
4570
  this.#postPromptTasksPromise = undefined;
4197
4571
  }
4198
4572
 
4199
- #trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
4573
+ #trackPostPromptTask(task: Promise<void>, lease?: RunResourceProducerLease, leaseTask: Promise<void> = task): void {
4200
4574
  this.#postPromptTasks.add(task);
4201
4575
  this.#ensurePostPromptTasksPromise();
4202
- if (resourceRunId) {
4203
- this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
4576
+ if (lease) {
4577
+ lease.track("post_prompt", "agent-session", leaseTask);
4578
+ void leaseTask.catch(() => {}).finally(() => lease.closeDiscovery());
4204
4579
  }
4205
4580
  void task
4206
4581
  .catch(() => {})
4207
4582
  .finally(() => {
4208
4583
  this.#postPromptTasks.delete(task);
4209
- if (this.#postPromptTasks.size === 0) {
4210
- this.#resolvePostPromptTasks();
4211
- }
4584
+ if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
4212
4585
  });
4213
4586
  }
4214
4587
 
4215
4588
  #schedulePostPromptTask(
4216
4589
  task: (signal: AbortSignal) => Promise<void>,
4217
- options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
4590
+ options?: {
4591
+ delayMs?: number;
4592
+ generation?: number;
4593
+ onSkip?: () => void;
4594
+ resourceRunId?: string;
4595
+ leaseTask?: Promise<void>;
4596
+ },
4218
4597
  ): Promise<void> {
4219
4598
  const delayMs = options?.delayMs ?? 0;
4220
- const signal = this.#postPromptTasksAbortController.signal;
4221
- const scheduled = (async () => {
4599
+ const resourceRunId = options?.resourceRunId;
4600
+ const contextualLease = this.#runResourceLeaseContext.getStore();
4601
+ const parentLease =
4602
+ resourceRunId && contextualLease?.resourceRunId === resourceRunId
4603
+ ? contextualLease
4604
+ : resourceRunId
4605
+ ? this.#postPromptLeases.get(resourceRunId)
4606
+ : undefined;
4607
+ const domain = resourceRunId ? this.#runCancellationDomains.lookup(resourceRunId) : undefined;
4608
+ const reservation = parentLease
4609
+ ? parentLease.fork(parentLease.domain, "post_prompt", "agent-session")
4610
+ : resourceRunId && domain
4611
+ ? this.agent.resourceLedger.reserveProducer(resourceRunId, domain, "post_prompt", "agent-session")
4612
+ : undefined;
4613
+ if (resourceRunId && !reservation?.ok) {
4614
+ options?.onSkip?.();
4615
+ return Promise.resolve();
4616
+ }
4617
+ const signal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
4618
+ const runScheduled = async () => {
4222
4619
  if (delayMs > 0) {
4223
4620
  try {
4224
4621
  await scheduler.wait(delayMs, { signal });
@@ -4236,8 +4633,11 @@ export class AgentSession {
4236
4633
  return;
4237
4634
  }
4238
4635
  await task(signal);
4239
- })();
4240
- this.#trackPostPromptTask(scheduled, options?.resourceRunId);
4636
+ };
4637
+ const scheduled = reservation?.ok
4638
+ ? this.#runResourceLeaseContext.run(reservation.lease, runScheduled)
4639
+ : runScheduled();
4640
+ this.#trackPostPromptTask(scheduled, reservation?.ok ? reservation.lease : undefined, options?.leaseTask);
4241
4641
  return scheduled;
4242
4642
  }
4243
4643
 
@@ -4256,6 +4656,7 @@ export class AgentSession {
4256
4656
  rescheduleOnBusy?: boolean;
4257
4657
  onError?: (error: unknown) => void;
4258
4658
  resourceRunId?: string;
4659
+ maintenanceContinuation?: boolean;
4259
4660
  }): Promise<void> {
4260
4661
  const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
4261
4662
  ? this.#reserveDeferredAgentEndForContinuation()
@@ -4277,12 +4678,13 @@ export class AgentSession {
4277
4678
  options?.onError?.(error);
4278
4679
  };
4279
4680
  const scheduledGeneration = options?.generation;
4280
- const signal = this.#postPromptTasksAbortController.signal;
4281
- const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
4282
- this.#schedulePostPromptTask(
4283
- async () => {
4681
+ const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> => {
4682
+ const leaseSettlement = Promise.withResolvers<void>();
4683
+ const settleLease = () => leaseSettlement.resolve();
4684
+ return this.#schedulePostPromptTask(
4685
+ async scheduledSignal => {
4284
4686
  const canContinue = (): boolean => {
4285
- if (signal.aborted || this.#isDisposed) {
4687
+ if (scheduledSignal.aborted || this.#isDisposed) {
4286
4688
  skip("aborted_signal");
4287
4689
  return false;
4288
4690
  }
@@ -4300,13 +4702,16 @@ export class AgentSession {
4300
4702
  }
4301
4703
  return true;
4302
4704
  };
4303
- if (!canContinue()) return;
4705
+ if (!canContinue()) {
4706
+ settleLease();
4707
+ return;
4708
+ }
4304
4709
  try {
4305
4710
  if (!options?.skipCompactionCheck) {
4306
4711
  await this.#checkEstimatedContextBeforePrompt();
4307
4712
  if (!canContinue()) return;
4308
4713
  }
4309
- if (signal.aborted) {
4714
+ if (scheduledSignal.aborted) {
4310
4715
  skip("aborted_signal");
4311
4716
  return;
4312
4717
  }
@@ -4326,23 +4731,40 @@ export class AgentSession {
4326
4731
  return;
4327
4732
  }
4328
4733
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4734
+ let predecessorAccepted = false;
4735
+ const releasePredecessor = () => {
4736
+ if (predecessorAccepted) return;
4737
+ predecessorAccepted = true;
4738
+ this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
4739
+ };
4329
4740
  const startsQueuedSuccessor =
4330
4741
  this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4331
4742
  try {
4332
4743
  await this.agent.continue({
4333
4744
  ...this.#managedFallbackPromptOptions(),
4745
+ maintenanceContinuation: options?.maintenanceContinuation,
4334
4746
  // Reset only after continue() has claimed the queued turn. Skipped or stale
4335
4747
  // continuations retain predecessor accounting, and resetAttemptBudget keeps
4336
4748
  // the sticky fallback cursor unchanged.
4337
- onRunAccepted: startsQueuedSuccessor
4338
- ? () => {
4339
- this.#defaultFallbackChain().resetAttemptBudget();
4340
- this.#overflowMaintenanceAttempts = 0;
4341
- }
4342
- : undefined,
4749
+ onRunAccepted: (handle: AttemptRunHandle) => {
4750
+ this.#acceptRunHandle(handle);
4751
+ settleLease();
4752
+ releasePredecessor();
4753
+ if (startsQueuedSuccessor) {
4754
+ this.#defaultFallbackChain().resetAttemptBudget();
4755
+ this.#overflowMaintenanceAttempts = 0;
4756
+ }
4757
+ },
4343
4758
  });
4759
+ releasePredecessor();
4344
4760
  } catch (error) {
4345
- if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
4761
+ if (
4762
+ options?.rescheduleOnBusy &&
4763
+ this.#isAgentBusyError(error) &&
4764
+ !predecessorAccepted &&
4765
+ !terminalized &&
4766
+ canContinue()
4767
+ ) {
4346
4768
  this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
4347
4769
  logger.debug("agent.continue busy after scheduling; rescheduling", {
4348
4770
  error: error.message,
@@ -4350,19 +4772,27 @@ export class AgentSession {
4350
4772
  void scheduleAttempt(100);
4351
4773
  return;
4352
4774
  }
4353
- this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4775
+ if (!predecessorAccepted)
4776
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4354
4777
  throw error;
4355
4778
  }
4356
4779
  } catch (error) {
4357
4780
  fail(error);
4781
+ } finally {
4782
+ settleLease();
4358
4783
  }
4359
4784
  },
4360
4785
  {
4361
4786
  delayMs,
4362
- onSkip: () => skip("aborted_signal"),
4787
+ onSkip: () => {
4788
+ settleLease();
4789
+ skip("aborted_signal");
4790
+ },
4363
4791
  resourceRunId: options?.resourceRunId,
4792
+ leaseTask: leaseSettlement.promise,
4364
4793
  },
4365
4794
  );
4795
+ };
4366
4796
  return scheduleAttempt();
4367
4797
  }
4368
4798
 
@@ -4433,7 +4863,7 @@ export class AgentSession {
4433
4863
 
4434
4864
  const compactionSettings = this.settings.getGroup("compaction");
4435
4865
  if (compactionSettings.autoContinue !== false) {
4436
- this.#scheduleAutoContinuePrompt(generation, false);
4866
+ this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
4437
4867
  return true;
4438
4868
  }
4439
4869
 
@@ -4441,11 +4871,13 @@ export class AgentSession {
4441
4871
  return false;
4442
4872
  }
4443
4873
 
4444
- #scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
4874
+ #scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true, resourceRunId?: string): void {
4445
4875
  const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
4446
4876
  const scheduledGeneration = generation;
4447
- const signal = this.#postPromptTasksAbortController.signal;
4448
- const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
4877
+ const continuationAuthorized = async (
4878
+ signal: AbortSignal,
4879
+ hasPendingNextTurnMessages = false,
4880
+ ): Promise<boolean> => {
4449
4881
  const snapshot = await this.#compactionStateSnapshot();
4450
4882
  if (signal.aborted) {
4451
4883
  this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
@@ -4467,61 +4899,68 @@ export class AgentSession {
4467
4899
  if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
4468
4900
  return authorized;
4469
4901
  };
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(),
4902
+ const scheduleAttempt = (delayMs = 0) => {
4903
+ let rescheduled = false;
4904
+ void this.#schedulePostPromptTask(
4905
+ async signal => {
4906
+ try {
4907
+ await Promise.resolve();
4908
+ if (signal.aborted) {
4909
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4910
+ return;
4911
+ }
4912
+ if (this.#promptGeneration !== scheduledGeneration) {
4913
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
4914
+ return;
4915
+ }
4916
+ if (!(await continuationAuthorized(signal))) return;
4917
+ await this.#promptWithMessage(
4918
+ {
4919
+ role: "developer",
4920
+ content: [{ type: "text", text: autoContinuePrompt }],
4921
+ attribution: "agent",
4922
+ timestamp: Date.now(),
4923
+ },
4924
+ autoContinuePrompt,
4925
+ {
4926
+ skipPostPromptRecoveryWait: true,
4927
+ skipCompactionCheck: true,
4928
+ predecessorAgentEndHold,
4929
+ onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
4930
+ continuationAuthorized(signal, hasPendingNextTurnMessages),
4931
+ },
4932
+ );
4933
+ } catch (error) {
4934
+ if (signal.aborted) {
4935
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4936
+ return;
4937
+ }
4938
+ if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
4939
+ logger.debug("Auto-compaction continuation busy; rescheduling", {
4940
+ source: "auto_continue_prompt",
4941
+ reason: "queue_drained",
4942
+ error: error.message,
4943
+ });
4944
+ rescheduled = true;
4945
+ scheduleAttempt(100);
4946
+ return;
4947
+ }
4948
+ this.#logCompactionContinuationError("auto_continue_prompt", error);
4949
+ } finally {
4950
+ if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4951
+ }
4477
4952
  },
4478
- autoContinuePrompt,
4479
4953
  {
4480
- skipPostPromptRecoveryWait: true,
4481
- skipCompactionCheck: true,
4482
- predecessorAgentEndHold,
4483
- onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
4954
+ delayMs,
4955
+ generation: scheduledGeneration,
4956
+ resourceRunId,
4957
+ onSkip: () => {
4958
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4959
+ this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4960
+ },
4484
4961
  },
4485
4962
  );
4486
4963
  };
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
4964
  scheduleAttempt();
4526
4965
  }
4527
4966
 
@@ -5266,163 +5705,200 @@ export class AgentSession {
5266
5705
  event: AgentSessionEvent,
5267
5706
  continueWhile?: () => boolean,
5268
5707
  workerIntegrationSettled = false,
5708
+ scope?: AttemptScopeRef,
5269
5709
  ): Promise<void> {
5270
5710
  if (event.type === "agent_end" && !workerIntegrationSettled) {
5271
5711
  await this.#flushWorkerIntegrationForAgentEnd();
5272
5712
  }
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",
5713
+ const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
5714
+ const isTerminalAgentEnd =
5715
+ event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
5716
+ const finishAttempt = () => {
5717
+ if (!isTerminalAgentEnd) return;
5718
+ if (deliveryScope) this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
5719
+ this.#activeLogicalRunId = undefined;
5720
+ };
5721
+ if (!this.#extensionRunner) {
5722
+ finishAttempt();
5723
+ return;
5724
+ }
5725
+
5726
+ try {
5727
+ if (event.type === "agent_start") {
5728
+ this.#turnIndex = 0;
5729
+ await this.#extensionRunner.emit({ type: "agent_start" }, undefined, deliveryScope);
5730
+ } else if (event.type === "agent_end") {
5731
+ await this.#extensionRunner.emit(
5732
+ {
5733
+ type: "agent_end",
5734
+ messages: event.messages,
5735
+ stopReason: event.stopReason,
5736
+ },
5737
+ undefined,
5738
+ deliveryScope,
5739
+ );
5740
+ } else if (event.type === "turn_start") {
5741
+ const hookEvent: TurnStartEvent = {
5742
+ type: "turn_start",
5743
+ turnIndex: this.#turnIndex,
5744
+ timestamp: Date.now(),
5745
+ };
5746
+ await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
5747
+ } else if (event.type === "turn_end") {
5748
+ const hookEvent: TurnEndEvent = {
5749
+ type: "turn_end",
5750
+ turnIndex: this.#turnIndex,
5316
5751
  message: event.message,
5317
- contentIndex: event.assistantMessageEvent.contentIndex,
5752
+ toolResults: event.toolResults,
5318
5753
  };
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",
5754
+ await this.#extensionRunner.emit(hookEvent, undefined, deliveryScope);
5755
+ this.#turnIndex++;
5756
+ } else if (event.type === "message_start") {
5757
+ const extensionEvent: MessageStartEvent = {
5758
+ type: "message_start",
5324
5759
  message: event.message,
5325
- contentIndex: event.assistantMessageEvent.contentIndex,
5326
- delta: event.assistantMessageEvent.delta,
5327
5760
  };
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",
5761
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5762
+ } else if (event.type === "message_update") {
5763
+ const extensionEvent: MessageUpdateEvent = {
5764
+ type: "message_update",
5333
5765
  message: event.message,
5334
- contentIndex: event.assistantMessageEvent.contentIndex,
5335
- content: event.assistantMessageEvent.content,
5766
+ assistantMessageEvent: event.assistantMessageEvent,
5336
5767
  };
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) });
5768
+ await this.#extensionRunner.emit(extensionEvent, continueWhile, deliveryScope);
5769
+ if (continueWhile && !continueWhile()) return;
5770
+ if (event.assistantMessageEvent.type === "reasoning_summary_start") {
5771
+ const reasoningEvent: ReasoningSummaryStartEvent = {
5772
+ type: "reasoning_summary_start",
5773
+ message: event.message,
5774
+ contentIndex: event.assistantMessageEvent.contentIndex,
5775
+ };
5776
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_start")) this.#markRetryReplayUnsafe();
5777
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5778
+ } else if (event.assistantMessageEvent.type === "reasoning_summary_delta") {
5779
+ const reasoningEvent: ReasoningSummaryDeltaEvent = {
5780
+ type: "reasoning_summary_delta",
5781
+ message: event.message,
5782
+ contentIndex: event.assistantMessageEvent.contentIndex,
5783
+ delta: event.assistantMessageEvent.delta,
5784
+ };
5785
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_delta")) this.#markRetryReplayUnsafe();
5786
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5787
+ } else if (event.assistantMessageEvent.type === "reasoning_summary_end") {
5788
+ const reasoningEvent: ReasoningSummaryEndEvent = {
5789
+ type: "reasoning_summary_end",
5790
+ message: event.message,
5791
+ contentIndex: event.assistantMessageEvent.contentIndex,
5792
+ content: event.assistantMessageEvent.content,
5793
+ };
5794
+ if (this.#extensionRunner.hasHandlers("reasoning_summary_end")) this.#markRetryReplayUnsafe();
5795
+ await this.#extensionRunner.emit(reasoningEvent, continueWhile, deliveryScope);
5796
+ }
5797
+ } else if (event.type === "message_end") {
5798
+ const extensionEvent: MessageEndEvent = {
5799
+ type: "message_end",
5800
+ message: event.message,
5801
+ };
5802
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5803
+ } else if (event.type === "tool_execution_start") {
5804
+ const extensionEvent: ToolExecutionStartEvent = {
5805
+ type: "tool_execution_start",
5806
+ toolCallId: event.toolCallId,
5807
+ toolName: event.toolName,
5808
+ args: event.args,
5809
+ intent: event.intent,
5810
+ };
5811
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5812
+ } else if (event.type === "tool_execution_update") {
5813
+ const extensionEvent: ToolExecutionUpdateEvent = {
5814
+ type: "tool_execution_update",
5815
+ toolCallId: event.toolCallId,
5816
+ toolName: event.toolName,
5817
+ args: event.args,
5818
+ partialResult: event.partialResult,
5819
+ };
5820
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5821
+ } else if (event.type === "tool_execution_end") {
5822
+ const extensionEvent: ToolExecutionEndEvent = {
5823
+ type: "tool_execution_end",
5824
+ toolCallId: event.toolCallId,
5825
+ toolName: event.toolName,
5826
+ result: event.result,
5827
+ isError: event.isError ?? false,
5828
+ };
5829
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
5830
+ } else if (event.type === "auto_compaction_start") {
5831
+ await this.#extensionRunner.emit(
5832
+ { type: "auto_compaction_start", reason: event.reason, action: event.action },
5833
+ undefined,
5834
+ deliveryScope,
5835
+ );
5836
+ } else if (event.type === "auto_compaction_end") {
5837
+ await this.#extensionRunner.emit(
5838
+ {
5839
+ type: "auto_compaction_end",
5840
+ action: event.action,
5841
+ result: event.result,
5842
+ aborted: event.aborted,
5843
+ willRetry: event.willRetry,
5844
+ errorMessage: event.errorMessage,
5845
+ skipped: event.skipped,
5846
+ continuationSkipReason: event.continuationSkipReason,
5847
+ },
5848
+ undefined,
5849
+ deliveryScope,
5850
+ );
5851
+ } else if (event.type === "auto_retry_start") {
5852
+ if (this.#extensionRunner.hasHandlers("auto_retry_start")) this.#markRetryReplayUnsafe();
5853
+ await this.#extensionRunner.emit(
5854
+ {
5855
+ type: "auto_retry_start",
5856
+ attempt: event.attempt,
5857
+ maxAttempts: event.maxAttempts,
5858
+ delayMs: event.delayMs,
5859
+ errorMessage: event.errorMessage,
5860
+ unbounded: event.unbounded,
5861
+ },
5862
+ undefined,
5863
+ deliveryScope,
5864
+ );
5865
+ } else if (event.type === "auto_retry_end") {
5866
+ await this.#extensionRunner.emit(
5867
+ {
5868
+ type: "auto_retry_end",
5869
+ success: event.success,
5870
+ attempt: event.attempt,
5871
+ finalError: event.finalError,
5872
+ },
5873
+ undefined,
5874
+ deliveryScope,
5875
+ );
5876
+ } else if (event.type === "ttsr_triggered") {
5877
+ await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules }, undefined, deliveryScope);
5878
+ } else if (event.type === "todo_reminder") {
5879
+ await this.#extensionRunner.emit(
5880
+ {
5881
+ type: "todo_reminder",
5882
+ todos: event.todos,
5883
+ attempt: event.attempt,
5884
+ maxAttempts: event.maxAttempts,
5885
+ },
5886
+ undefined,
5887
+ deliveryScope,
5888
+ );
5889
+ } else if (event.type === "goal_updated") {
5890
+ try {
5891
+ await this.#extensionRunner.emit(
5892
+ { type: "goal_updated", goal: event.goal, state: event.state },
5893
+ undefined,
5894
+ deliveryScope,
5895
+ );
5896
+ } catch (error) {
5897
+ logger.warn("Goal updated extension hook failed", { error: String(error) });
5898
+ }
5425
5899
  }
5900
+ } finally {
5901
+ finishAttempt();
5426
5902
  }
5427
5903
  }
5428
5904
 
@@ -5545,7 +6021,15 @@ export class AgentSession {
5545
6021
  ),
5546
6022
  Bun.sleep(2_000).then(() => false),
5547
6023
  ]);
5548
- if (!disposeIdleSettled) this.agent.forceAbort("Session disposed");
6024
+ if (!disposeIdleSettled) {
6025
+ try {
6026
+ this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
6027
+ } catch {
6028
+ // AttemptScope handle may not be registered for sessions
6029
+ // that don't participate in the facility (e.g. test mocks).
6030
+ this.agent.forceAbort("Session disposed");
6031
+ }
6032
+ }
5549
6033
  await admissionClosed;
5550
6034
  await this.#agentEndPublicationPromise;
5551
6035
  await this.#queuedExtensionEvents;
@@ -6929,7 +7413,8 @@ export class AgentSession {
6929
7413
  this.#assertNoHandoffTransition();
6930
7414
  await this.agent.continue({
6931
7415
  ...this.#managedFallbackPromptOptions(),
6932
- onRunAccepted: () => {
7416
+ onRunAccepted: (handle: AttemptRunHandle) => {
7417
+ this.#acceptRunHandle(handle);
6933
7418
  if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
6934
7419
  },
6935
7420
  });
@@ -6956,20 +7441,32 @@ export class AgentSession {
6956
7441
  }
6957
7442
 
6958
7443
  /** 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);
7444
+ async convertMessagesToLlm(
7445
+ messages: AgentMessage[],
7446
+ signal?: AbortSignal,
7447
+ scope?: AttemptScope,
7448
+ ): Promise<Message[]> {
7449
+ const transformedMessages =
7450
+ scope === undefined
7451
+ ? await this.#transformContext(messages, signal)
7452
+ : await this.#transformContext(messages, signal, scope);
6961
7453
  return await this.#convertToLlm(transformedMessages);
6962
7454
  }
6963
7455
 
6964
7456
  /** Apply session-level stream hooks to a direct side request. */
6965
- prepareSimpleStreamOptions(options: SimpleStreamOptions, provider = "anthropic"): SimpleStreamOptions {
7457
+ prepareSimpleStreamOptions(
7458
+ options: SimpleStreamOptions,
7459
+ provider = "anthropic",
7460
+ scope?: AttemptScope,
7461
+ ): SimpleStreamOptions {
6966
7462
  const sessionOnPayload = this.#onPayload;
6967
7463
  const sessionOnResponse = this.#onResponse;
6968
7464
  const sessionMetadata = this.agent.metadataForProvider(provider);
6969
7465
  const sessionOnSseEvent = this.#onSseEvent;
6970
- if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
7466
+ if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent && !scope) return options;
6971
7467
 
6972
7468
  const preparedOptions: SimpleStreamOptions = { ...options };
7469
+ if (scope) preparedOptions.attemptScope = scope;
6973
7470
 
6974
7471
  // Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
6975
7472
  // they share the same session bucket as Agent.prompt-routed requests on Anthropic
@@ -6983,10 +7480,10 @@ export class AgentSession {
6983
7480
  preparedOptions.onPayload = sessionOnPayload;
6984
7481
  } else {
6985
7482
  const requestOnPayload = options.onPayload;
6986
- preparedOptions.onPayload = async (payload, model) => {
6987
- const sessionPayload = await sessionOnPayload(payload, model);
7483
+ preparedOptions.onPayload = async (payload, model, callbackScope) => {
7484
+ const sessionPayload = await sessionOnPayload(payload, model, callbackScope);
6988
7485
  const sessionResolvedPayload = sessionPayload ?? payload;
6989
- const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
7486
+ const requestPayload = await requestOnPayload(sessionResolvedPayload, model, callbackScope);
6990
7487
  return requestPayload ?? sessionResolvedPayload;
6991
7488
  };
6992
7489
  }
@@ -6997,9 +7494,9 @@ export class AgentSession {
6997
7494
  preparedOptions.onResponse = sessionOnResponse;
6998
7495
  } else {
6999
7496
  const requestOnResponse = options.onResponse;
7000
- preparedOptions.onResponse = async (response, model) => {
7001
- await sessionOnResponse(response, model);
7002
- await requestOnResponse(response, model);
7497
+ preparedOptions.onResponse = async (response, model, callbackScope) => {
7498
+ await sessionOnResponse(response, model, callbackScope);
7499
+ await requestOnResponse(response, model, callbackScope);
7003
7500
  };
7004
7501
  }
7005
7502
  }
@@ -7009,9 +7506,9 @@ export class AgentSession {
7009
7506
  preparedOptions.onSseEvent = sessionOnSseEvent;
7010
7507
  } else {
7011
7508
  const requestOnSseEvent = options.onSseEvent;
7012
- preparedOptions.onSseEvent = (event, model) => {
7013
- sessionOnSseEvent(event, model);
7014
- requestOnSseEvent(event, model);
7509
+ preparedOptions.onSseEvent = (event, model, callbackScope) => {
7510
+ sessionOnSseEvent(event, model, callbackScope);
7511
+ requestOnSseEvent(event, model, callbackScope);
7015
7512
  };
7016
7513
  }
7017
7514
  }
@@ -8058,7 +8555,7 @@ export class AgentSession {
8058
8555
  skipPostPromptRecoveryWait?: boolean;
8059
8556
  predecessorAgentEndHold?: symbol;
8060
8557
  admissionLease?: SessionAdmissionLease;
8061
- onRunAccepted?: () => void;
8558
+ onRunAccepted?: (handle: AttemptRunHandle) => void;
8062
8559
  onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
8063
8560
  resetRetryReplaySafety?: boolean;
8064
8561
  },
@@ -8284,8 +8781,9 @@ export class AgentSession {
8284
8781
  const agentPromptOptions = {
8285
8782
  ...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
8286
8783
  ...this.#managedFallbackPromptOptions(),
8287
- onRunAccepted: () => {
8288
- options?.onRunAccepted?.();
8784
+ onRunAccepted: (handle: AttemptRunHandle) => {
8785
+ this.#acceptRunHandle(handle);
8786
+ options?.onRunAccepted?.(handle);
8289
8787
  options?.admissionLease?.release();
8290
8788
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
8291
8789
  },
@@ -9408,7 +9906,14 @@ export class AgentSession {
9408
9906
  ]);
9409
9907
  if (outcome.kind === "timeout") {
9410
9908
  this.#abandonPostPromptTasks();
9411
- this.agent.forceAbort("Abort cleanup timed out");
9909
+ const forceAbortLogicalRunId = this.agent.currentManagedLogicalRunId ?? this.#activeLogicalRunId;
9910
+ try {
9911
+ if (forceAbortLogicalRunId !== undefined)
9912
+ this.agent.forceAbort("Abort cleanup timed out", forceAbortLogicalRunId);
9913
+ else this.agent.forceAbort("Abort cleanup timed out");
9914
+ } catch {
9915
+ this.agent.forceAbort("Abort cleanup timed out");
9916
+ }
9412
9917
  this.emitNotice(
9413
9918
  "warning",
9414
9919
  "Abort cleanup timed out; forced session recovery. The previous provider stream or tool may still be unwinding in the background.",
@@ -9465,20 +9970,16 @@ export class AgentSession {
9465
9970
  * Abort a specific active prompt and prove whether its tracked resources settled.
9466
9971
  */
9467
9972
  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);
9973
+ const aborted = this.#runCancellationDomains.abort(handle);
9974
+ if (!aborted.ok) {
9975
+ if (aborted.reason === "quarantined") {
9976
+ return await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 });
9977
+ }
9978
+ return { status: "unfenced", reason: "unknown_run", pending: [] };
9481
9979
  }
9980
+ if (handle === this.agent.activeResourceRunId) this.agent.abort();
9981
+ const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
9982
+ if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
9482
9983
  return proof;
9483
9984
  }
9484
9985
 
@@ -11870,6 +12371,7 @@ export class AgentSession {
11870
12371
  skipAbortedCheck = true,
11871
12372
  onTerminalOverflowNoop?: () => void,
11872
12373
  resourceRunId?: string,
12374
+ ownershipSignal?: AbortSignal,
11873
12375
  ): Promise<boolean> {
11874
12376
  // Safety stops are terminal and must not trigger context maintenance.
11875
12377
  if (
@@ -11913,7 +12415,8 @@ export class AgentSession {
11913
12415
  }
11914
12416
 
11915
12417
  // Try context promotion first - switch to a larger model and retry without compacting
11916
- const promoted = await this.#tryContextPromotion(assistantMessage);
12418
+ const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
12419
+ if (ownershipSignal?.aborted) return false;
11917
12420
  if (promoted) {
11918
12421
  // Retry on the promoted (larger) model without compacting
11919
12422
  this.#scheduleAgentContinue({
@@ -11938,6 +12441,7 @@ export class AgentSession {
11938
12441
  }
11939
12442
  },
11940
12443
  resourceRunId,
12444
+ signal: ownershipSignal,
11941
12445
  });
11942
12446
  return "continuationScheduled" in status && status.continuationScheduled === true;
11943
12447
  }
@@ -11972,14 +12476,16 @@ export class AgentSession {
11972
12476
  autoCompactionOutputReserveTokens,
11973
12477
  )
11974
12478
  ) {
11975
- const pruneResult = await this.#pruneToolOutputs(undefined, true);
12479
+ const pruneResult = await this.#pruneToolOutputs(ownershipSignal, true);
12480
+ if (ownershipSignal?.aborted) return false;
11976
12481
  if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
11977
12482
  }
11978
12483
  if (shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
11979
12484
  // Try promotion first — if a larger model is available, switch instead of compacting
11980
- const promoted = await this.#tryContextPromotion(assistantMessage);
12485
+ const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
12486
+ if (ownershipSignal?.aborted) return false;
11981
12487
  if (!promoted) {
11982
- await this.#runAutoCompaction("threshold", false);
12488
+ await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
11983
12489
  }
11984
12490
  }
11985
12491
  return true;
@@ -13358,7 +13864,7 @@ export class AgentSession {
13358
13864
  });
13359
13865
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
13360
13866
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13361
- this.#scheduleAutoContinuePrompt(generation);
13867
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13362
13868
  }
13363
13869
 
13364
13870
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
@@ -13422,7 +13928,7 @@ export class AgentSession {
13422
13928
  }
13423
13929
  const overflowContinuationScheduled =
13424
13930
  !overflowNoopWouldReplay && willRetry && !continuationSkipReason
13425
- ? await this.#scheduleOverflowRetryContinuation(generation)
13931
+ ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
13426
13932
  : false;
13427
13933
  await this.#emitSessionEvent({
13428
13934
  type: "auto_compaction_end",
@@ -13445,6 +13951,7 @@ export class AgentSession {
13445
13951
  rescheduleOnBusy: true,
13446
13952
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
13447
13953
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
13954
+ resourceRunId: options?.resourceRunId,
13448
13955
  });
13449
13956
  return { kind: "skipped", continuationScheduled: true };
13450
13957
  }
@@ -13465,9 +13972,10 @@ export class AgentSession {
13465
13972
  rescheduleOnBusy: true,
13466
13973
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
13467
13974
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
13975
+ resourceRunId: options?.resourceRunId,
13468
13976
  });
13469
13977
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13470
- this.#scheduleAutoContinuePrompt(generation);
13978
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13471
13979
  }
13472
13980
  return { kind: "skipped" };
13473
13981
  }
@@ -13694,7 +14202,9 @@ export class AgentSession {
13694
14202
  this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
13695
14203
  }
13696
14204
  const overflowContinuationScheduled =
13697
- willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
14205
+ willRetry && !continuationSkipReason
14206
+ ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
14207
+ : false;
13698
14208
  await this.#emitSessionEvent({
13699
14209
  type: "auto_compaction_end",
13700
14210
  action,
@@ -13721,9 +14231,10 @@ export class AgentSession {
13721
14231
  rescheduleOnBusy: true,
13722
14232
  onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
13723
14233
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
14234
+ resourceRunId: options?.resourceRunId,
13724
14235
  });
13725
14236
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
13726
- this.#scheduleAutoContinuePrompt(generation);
14237
+ this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
13727
14238
  }
13728
14239
  return { kind: "compacted" };
13729
14240
  } catch (error) {
@@ -13788,7 +14299,7 @@ export class AgentSession {
13788
14299
 
13789
14300
  #isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
13790
14301
  if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
13791
- const errorMessage = message.errorMessage ?? "";
14302
+ const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
13792
14303
  if (message.api === "anthropic-messages") {
13793
14304
  return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
13794
14305
  }
@@ -13815,7 +14326,8 @@ export class AgentSession {
13815
14326
  #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
13816
14327
  if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
13817
14328
  const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
13818
- return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
14329
+ const errorMessage = terminalProviderFirstEventTimeoutIdentity(message.errorMessage ?? "");
14330
+ return timeoutMessages?.has(errorMessage) ?? false;
13819
14331
  }
13820
14332
 
13821
14333
  #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
@@ -14003,6 +14515,7 @@ export class AgentSession {
14003
14515
  if (this.#isTerminalProviderFirstEventTimeout(message)) {
14004
14516
  return "terminal";
14005
14517
  }
14518
+ if (isMessageOnlyFirstEventTimeout(message)) return "first_event_timeout";
14006
14519
  // Legacy providers that have not yet stamped the typed timeout fact retain
14007
14520
  // their existing bounded ollama-cloud behavior.
14008
14521
  if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
@@ -14173,20 +14686,37 @@ export class AgentSession {
14173
14686
  return {
14174
14687
  type: "maintenance",
14175
14688
  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, {
14689
+ if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
14690
+ const resourceRunId = String(ownership.logicalRunId);
14691
+ const previousLease = this.#postPromptLeases.get(resourceRunId);
14692
+ this.#postPromptLeases.set(resourceRunId, ownership.lease);
14693
+ try {
14694
+ let terminalized = false;
14695
+ const successorScheduled = await this.#checkCompaction(
14696
+ outcome.message,
14697
+ true,
14698
+ () => {
14699
+ terminalized = true;
14700
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14701
+ stopReason: "error",
14702
+ messages: [outcome.message],
14703
+ });
14704
+ },
14705
+ resourceRunId,
14706
+ ownership.lease.signal,
14707
+ );
14708
+ if (terminalized || successorScheduled || !ownership.isCurrent() || ownership.lease.signal.aborted)
14709
+ return;
14710
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14181
14711
  stopReason: "error",
14182
14712
  messages: [outcome.message],
14183
14713
  });
14184
- });
14185
- if (terminalized || successorScheduled || !ownership.isCurrent()) return;
14186
- this.agent.requestRunTerminal(ownership.logicalRunId, {
14187
- stopReason: "error",
14188
- messages: [outcome.message],
14189
- });
14714
+ } finally {
14715
+ if (this.#postPromptLeases.get(resourceRunId) === ownership.lease) {
14716
+ if (previousLease) this.#postPromptLeases.set(resourceRunId, previousLease);
14717
+ else this.#postPromptLeases.delete(resourceRunId);
14718
+ }
14719
+ }
14190
14720
  },
14191
14721
  };
14192
14722
  }
@@ -14378,17 +14908,20 @@ export class AgentSession {
14378
14908
  // retry.enabled=false always surfaces immediately, matching the explicit
14379
14909
  // user opt-out.
14380
14910
  if (!managedFallback && !retrySettings.enabled) return false;
14381
- const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
14382
- if (classification === "first_event_timeout") {
14911
+ const classification = this.#classifyErrorForRetry(message);
14912
+ const firstEventTimeout = classification === "first_event_timeout";
14913
+ if (!managedFallback && firstEventTimeout) {
14383
14914
  this.#firstEventTimeoutRetryStartedAt ??= Date.now();
14384
14915
  }
14385
14916
  // First-event timeouts are replay-safe only before any model progress,
14386
14917
  // abort, or extension/provider lifecycle participation.
14387
- if (
14388
- classification === "first_event_timeout" &&
14389
- (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
14390
- ) {
14391
- return false;
14918
+ if (firstEventTimeout && (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))) {
14919
+ return managedOutcome
14920
+ ? {
14921
+ type: "terminal",
14922
+ terminal: { stopReason: "error", messages: [message] },
14923
+ }
14924
+ : false;
14392
14925
  }
14393
14926
  const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
14394
14927
  if (!trigger) {
@@ -14415,12 +14948,14 @@ export class AgentSession {
14415
14948
  // capacity-overload event.
14416
14949
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
14417
14950
  const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
14951
+ const bareDefaultWrappedFirstEventTimeout = isBareDefaultWrappedFirstEventTimeout(message);
14418
14952
  // Content-free Codex overload is replay-safe by the same reasoning as
14419
14953
  // credential rotation: no partial output means no observable state.
14420
14954
  const canReplayCodexOverload = bareDefaultCodexOverload;
14421
14955
  if (
14422
14956
  (!canReplayCodexOverload &&
14423
14957
  !this.#isTypedFirstEventTimeout(message) &&
14958
+ !bareDefaultWrappedFirstEventTimeout &&
14424
14959
  (hasBareDefaultRetryDisqualifyingFacts(message) ||
14425
14960
  (classification !== "transient" && classification !== "first_event_timeout") ||
14426
14961
  !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
@@ -14429,7 +14964,7 @@ export class AgentSession {
14429
14964
  return false;
14430
14965
  }
14431
14966
  }
14432
- const legacyUnbounded = classification === "transient";
14967
+ const legacyUnbounded = !managedFallback && classification === "transient";
14433
14968
  const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
14434
14969
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
14435
14970
  let outcome = managedFallback
@@ -14496,7 +15031,7 @@ export class AgentSession {
14496
15031
  : this.#fallbackExhaustionError(controller);
14497
15032
  this.emitNotice("error", errorMessage, "fallback");
14498
15033
  if (managedOutcome && ownership) {
14499
- this.agent.requestRunTerminal(ownership.logicalRunId, {
15034
+ this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
14500
15035
  stopReason: "exhausted",
14501
15036
  messages: [this.#managedFallbackExhaustionMessage(message, errorMessage)],
14502
15037
  });
@@ -14522,12 +15057,11 @@ export class AgentSession {
14522
15057
  await this.#emitSessionEvent({
14523
15058
  type: "auto_retry_start",
14524
15059
  attempt: this.#retryAttempt,
14525
- maxAttempts:
14526
- classification === "first_event_timeout"
15060
+ maxAttempts: managedFallback
15061
+ ? controller.maxAttempts
15062
+ : firstEventTimeout
14527
15063
  ? retrySettings.maxRetries + 1
14528
- : managedFallback
14529
- ? controller.maxAttempts
14530
- : retrySettings.maxRetries,
15064
+ : retrySettings.maxRetries,
14531
15065
  delayMs,
14532
15066
  errorMessage,
14533
15067
  unbounded: managedFallback ? false : legacyUnbounded,
@@ -14548,8 +15082,13 @@ export class AgentSession {
14548
15082
  }
14549
15083
  this.agent.replaceMessages(messages.slice(0, end));
14550
15084
  }
15085
+ const retrySignal = ownership
15086
+ ? AbortSignal.any([retryAbortController.signal, ownership.lease.signal])
15087
+ : retryAbortController.signal;
15088
+ const ownershipCancelled = () =>
15089
+ Boolean(ownership && (!ownership.isCurrent() || ownership.lease.signal.aborted));
14551
15090
  try {
14552
- await scheduler.wait(delayMs, { signal: retryAbortController.signal });
15091
+ await scheduler.wait(delayMs, { signal: retrySignal });
14553
15092
  } catch {
14554
15093
  if (this.#retryAbortController !== retryAbortController) return;
14555
15094
  this.#retryAbortController = undefined;
@@ -14568,7 +15107,10 @@ export class AgentSession {
14568
15107
  return;
14569
15108
  }
14570
15109
  }
14571
- if (retryAbortController.signal.aborted && !this.#retryNowRequested) {
15110
+ if (
15111
+ (retryAbortController.signal.aborted || ownershipCancelled()) &&
15112
+ !(this.#retryNowRequested && !ownershipCancelled())
15113
+ ) {
14572
15114
  if (this.#retryAbortController !== retryAbortController) return;
14573
15115
  this.#retryAbortController = undefined;
14574
15116
  const attempt = this.#retryAttempt;
@@ -14588,7 +15130,7 @@ export class AgentSession {
14588
15130
  if (managedOutcome) {
14589
15131
  try {
14590
15132
  await this.#checkEstimatedContextBeforePrompt();
14591
- if (!ownership?.isCurrent()) {
15133
+ if (ownershipCancelled()) {
14592
15134
  const attempt = this.#retryAttempt;
14593
15135
  this.#retryAttempt = 0;
14594
15136
  await this.#emitSessionEvent({
@@ -14619,10 +15161,13 @@ export class AgentSession {
14619
15161
  }
14620
15162
  }
14621
15163
 
15164
+ const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
14622
15165
  this.#scheduleAgentContinue({
14623
15166
  delayMs: 1,
14624
15167
  generation,
14625
15168
  allowDuringCancelAndSubmit: true,
15169
+ suppressPredecessorAgentEnd: resourceRunId !== undefined,
15170
+ resourceRunId,
14626
15171
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
14627
15172
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
14628
15173
  });
@@ -14677,7 +15222,11 @@ export class AgentSession {
14677
15222
 
14678
15223
  async #promptAgentWithIdleRetry(
14679
15224
  messages: AgentMessage[],
14680
- options?: { toolChoice?: ToolChoice; fallbackManaged?: boolean; onRunAccepted?: () => void },
15225
+ options?: {
15226
+ toolChoice?: ToolChoice;
15227
+ fallbackManaged?: boolean;
15228
+ onRunAccepted?: (handle: AttemptRunHandle) => void;
15229
+ },
14681
15230
  predecessorAgentEndHold?: symbol,
14682
15231
  ): Promise<void> {
14683
15232
  const deadline = Date.now() + 30_000;
@@ -14690,6 +15239,7 @@ export class AgentSession {
14690
15239
  continuationHold = undefined;
14691
15240
  try {
14692
15241
  await this.agent.prompt(messages, options);
15242
+ this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
14693
15243
  return;
14694
15244
  } catch (error) {
14695
15245
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
@@ -15441,19 +15991,30 @@ export class AgentSession {
15441
15991
  !rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
15442
15992
  const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
15443
15993
  const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
15994
+ let sideAttempt: { scope: AttemptScope; dispose: () => void } | undefined;
15444
15995
  try {
15445
15996
  const model = this.model;
15446
15997
  if (!model) throw new Error("No active model on session");
15447
15998
  const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
15448
15999
  if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
16000
+ sideAttempt = this.agent.mintSideAttemptScope();
16001
+ const sideScope = sideAttempt.scope;
16002
+ this.#attemptRecordStore.register(sideScope);
16003
+ this.#attemptRecordStore.establishClean(sideScope);
15449
16004
  const prependMessages = prependMessagesValid()
15450
16005
  ? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
15451
16006
  : undefined;
15452
16007
  let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
15453
- let llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
16008
+ let llmMessages = await awaitEphemeralAbort(
16009
+ this.convertMessagesToLlm(snapshot, args.signal, sideScope),
16010
+ args.signal,
16011
+ );
15454
16012
  if (prependMessages && !prependMessagesValid()) {
15455
16013
  snapshot = this.#buildEphemeralSnapshot(args.promptText);
15456
- llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
16014
+ llmMessages = await awaitEphemeralAbort(
16015
+ this.convertMessagesToLlm(snapshot, args.signal, sideScope),
16016
+ args.signal,
16017
+ );
15457
16018
  }
15458
16019
  const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
15459
16020
  const ephemeralSessionId = crypto.randomUUID();
@@ -15474,6 +16035,7 @@ export class AgentSession {
15474
16035
  toolChoice: "none",
15475
16036
  },
15476
16037
  model.provider,
16038
+ sideScope,
15477
16039
  );
15478
16040
  args.signal?.throwIfAborted();
15479
16041
  let replyText = "";
@@ -15501,6 +16063,10 @@ export class AgentSession {
15501
16063
  }
15502
16064
  return { replyText: replyText.trim(), assistantMessage };
15503
16065
  } finally {
16066
+ if (sideAttempt) {
16067
+ sideAttempt.dispose();
16068
+ this.#attemptRecordStore.retire(sideAttempt.scope);
16069
+ }
15504
16070
  if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
15505
16071
  }
15506
16072
  }
@@ -15514,8 +16080,15 @@ export class AgentSession {
15514
16080
  }
15515
16081
  const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
15516
16082
  if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
16083
+ const sideAttempt = this.agent.mintSideAttemptScope();
16084
+ this.#attemptRecordStore.register(sideAttempt.scope);
16085
+ this.#attemptRecordStore.establishClean(sideAttempt.scope);
15517
16086
  const scope = args.turn.scope;
15518
- if (!scope) throw new Error("The /btw conversation scope was scrubbed.");
16087
+ if (!scope) {
16088
+ sideAttempt.dispose();
16089
+ this.#attemptRecordStore.retire(sideAttempt.scope);
16090
+ throw new Error("The /btw conversation scope was scrubbed.");
16091
+ }
15519
16092
  const messages = scope.messages.map(message => ({
15520
16093
  role: message.role,
15521
16094
  content: [{ type: "text" as const, text: message.text }],
@@ -15548,6 +16121,7 @@ export class AgentSession {
15548
16121
  requestMaxRetries: 0,
15549
16122
  streamMaxRetries: 0,
15550
16123
  streamFirstEventTimeoutMs: 0,
16124
+ attemptScope: sideAttempt.scope,
15551
16125
  };
15552
16126
  const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
15553
16127
  let replyText = "";
@@ -15607,6 +16181,8 @@ export class AgentSession {
15607
16181
  if (idleTimer) clearTimeout(idleTimer);
15608
16182
  clearTimeout(totalTimer);
15609
16183
  void iterator.return?.().catch(() => undefined);
16184
+ sideAttempt.dispose();
16185
+ this.#attemptRecordStore.retire(sideAttempt.scope);
15610
16186
  }
15611
16187
  }
15612
16188