@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -37,8 +37,15 @@ import {
37
37
  settleOwnedWork,
38
38
  } from "../../session/terminal-abort";
39
39
  import { parseThinkingLevel } from "../../thinking";
40
+ import { readEndpointFile } from "../broker/endpoint-authority";
40
41
  import { ensureBroker } from "../broker/ensure";
41
- import { SessionIndex } from "../broker/session-index";
42
+ import { processIncarnation } from "../broker/process-incarnation";
43
+ import {
44
+ type MasterRoleAttestationV2,
45
+ resolveSessionLocator,
46
+ SessionIndex,
47
+ type SessionLocatorV2,
48
+ } from "../broker/session-index";
42
49
  import {
43
50
  parseSyntheticModelId,
44
51
  resolveSyntheticModelSelection,
@@ -49,17 +56,28 @@ import {
49
56
  import { projectQ10Models } from "../models.js";
50
57
  import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
51
58
  import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
59
+ import type { SdkPromptTerminalOutcome } from "../prompt-status";
52
60
  import { validateRequiredPromptText } from "../protocol/adapter-validation";
53
61
  import { OPERATIONS } from "../protocol/operation-registry";
62
+ import { type ReceiptState, reduceReceiptState } from "../receipt-state";
54
63
  import {
55
64
  createKindAwareReconciliation,
56
65
  createReconciliationStore,
57
66
  type KindAwareReconciliation,
58
67
  resolveReconciliationSessionFile,
59
68
  } from "../reconciliation-extensions";
60
- import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
61
- import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
62
- import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
69
+ import {
70
+ reduceTurnResultContent,
71
+ reportableTurnResultContent,
72
+ sanitizeTurnResultContent,
73
+ type TurnResultContent,
74
+ } from "../turn-result";
75
+ import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
76
+ import {
77
+ BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
78
+ BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
79
+ hasBrokerRuntimeAbortCapability,
80
+ } from "./control/runtime-gate";
63
81
  import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
64
82
  import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
65
83
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
@@ -103,6 +121,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
103
121
  * after this bound are abandoned — their durable broker state is the recovery
104
122
  * authority, and outcomes are inherently uncertain. */
105
123
  const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
124
+ /** Master verification fields are ephemeral protocol values, not unbounded input. */
125
+ const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
126
+ const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
127
+ /** Keep replay protection finite even when an authenticated client floods nonces. */
128
+ const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
129
+ /** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
130
+ const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
131
+
132
+ export type MasterCapabilityReplayState = Map<string, number>;
133
+
134
+ export function verifyMasterCapabilityFrame(input: {
135
+ frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
136
+ expectedCapability: string | undefined;
137
+ expectedEpoch: string | undefined;
138
+ replay: MasterCapabilityReplayState;
139
+ now?: number;
140
+ }): { ok: boolean; nonce: string; attestationEpoch: string } {
141
+ const now = input.now ?? Date.now();
142
+ for (const [consumedNonce, expiresAt] of input.replay) {
143
+ if (expiresAt <= now) input.replay.delete(consumedNonce);
144
+ }
145
+ const bounded = (value: unknown): value is string =>
146
+ typeof value === "string" &&
147
+ value.length > 0 &&
148
+ value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
149
+ MASTER_AUTH_VALUE_PATTERN.test(value);
150
+ const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
151
+ const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
152
+ const capability = bounded(input.frame.capability) ? input.frame.capability : "";
153
+ const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
154
+ const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
155
+ const authenticatedFields =
156
+ bounded(input.frame.nonce) &&
157
+ bounded(input.frame.attestationEpoch) &&
158
+ bounded(input.frame.capability) &&
159
+ bounded(input.expectedCapability) &&
160
+ bounded(input.expectedEpoch) &&
161
+ epoch === expectedEpoch;
162
+ const expectedBytes = Buffer.from(expectedCapability);
163
+ const capabilityBytes = Buffer.from(capability);
164
+ const capabilityMatches =
165
+ authenticatedFields &&
166
+ expectedBytes.length === capabilityBytes.length &&
167
+ crypto.timingSafeEqual(expectedBytes, capabilityBytes);
168
+ const reusable = capabilityMatches && !input.replay.has(nonce);
169
+ if (reusable) {
170
+ if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
171
+ const oldest = input.replay.keys().next().value;
172
+ if (typeof oldest === "string") input.replay.delete(oldest);
173
+ }
174
+ input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
175
+ }
176
+ return {
177
+ ok: reusable,
178
+ nonce,
179
+ attestationEpoch: authenticatedFields ? epoch : "",
180
+ };
181
+ }
182
+
106
183
  /** Maximum time a replaced runtime may retain a lifecycle persistence task. */
107
184
  const LIFECYCLE_QUIESCENCE_MS = 1_000;
108
185
 
@@ -142,6 +219,8 @@ export interface SessionSdkRuntimeOptions
142
219
  settings?: Settings;
143
220
  /** Mutable shadow of patched config values merged into query readback. */
144
221
  configOverrides?: Map<string, unknown>;
222
+ /** Stable master lineage identity retained across session switches/branches. */
223
+ masterOwnerSessionId?: string;
145
224
  }
146
225
 
147
226
  export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
@@ -152,6 +231,8 @@ export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
152
231
  startedAt?: number;
153
232
  terminalAt?: number;
154
233
  error?: { code: string; message: string };
234
+ content?: TurnResultContent;
235
+ receiptState?: Exclude<ReceiptState, "absent">;
155
236
  outcome?: unknown;
156
237
  pendingOutcome?: unknown;
157
238
  skillName?: string;
@@ -400,6 +481,8 @@ export interface CreateSdkSessionRuntimeOptions {
400
481
  agentDir: string;
401
482
  /** Lifecycle-owned sessions require broker publication before they become usable. */
402
483
  brokerRegistrationRequired?: boolean;
484
+ /** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
485
+ lifecycleRequestId?: string;
403
486
  createTransport(input: {
404
487
  sessionId: string;
405
488
  stateRoot: string;
@@ -411,6 +494,12 @@ export interface CreateSdkSessionRuntimeOptions {
411
494
  onSdkRequest?: SessionSdkHostOptions["onRequest"];
412
495
  /** Mutable shadow of patched config values merged into query readback. */
413
496
  configOverrides?: Map<string, unknown>;
497
+ /** In-memory master capability for private broker verification only. */
498
+ masterCapability?: string;
499
+ /** Opaque direct-role epoch this effective host may adopt. */
500
+ masterAttestationEpoch?: string;
501
+ /** Stable master lineage identity retained across session switches/branches. */
502
+ masterOwnerSessionId?: string;
414
503
  /** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
415
504
  terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
416
505
  /** Callback when a frame is admitted to the runtime (test harness). */
@@ -419,6 +508,8 @@ export interface CreateSdkSessionRuntimeOptions {
419
508
  onLifecycleDrainTimeoutForTests?: () => void;
420
509
  /** Test-only observation of bounded failure-diagnostic deduplication state. */
421
510
  onFailureDiagnosticKeyCountForTests?: (count: number) => void;
511
+ /** Test-only observation after a resolved invocation completion is reconciled. */
512
+ onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
422
513
  }
423
514
 
424
515
  function unavailable(operation: string): () => never {
@@ -436,7 +527,67 @@ export interface InvocationCorrelation {
436
527
 
437
528
  export type InvocationKind = "prompt" | "skill" | "steer";
438
529
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
530
+ type InvocationOutcome = SdkPromptTerminalOutcome;
531
+ type InvocationOutcomeInput = {
532
+ kind: string;
533
+ reason?: unknown;
534
+ code?: unknown;
535
+ message?: unknown;
536
+ provenance?: unknown;
537
+ };
439
538
  const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
539
+ const PRESERVED_PROVIDER_FAILURE_CODES = new Set([
540
+ "provider_down",
541
+ "provider_unavailable",
542
+ "provider_rejected",
543
+ "upstream_stream_interrupted",
544
+ "argument_validation",
545
+ "execution",
546
+ "upstream_error",
547
+ "transport_reset",
548
+ ]);
549
+ const isPreservedProviderFailure = (code: string | undefined): boolean =>
550
+ code !== undefined && (PRESERVED_PROVIDER_FAILURE_CODES.has(code) || code.startsWith("provider_http_"));
551
+
552
+ /**
553
+ * Durable reconciliation accepts only the bounded outcome vocabulary from
554
+ * reconciliation-store.ts. Provider diagnostics keep their specific safe code
555
+ * in `error`, while the durable outcome is normalized to the public failure
556
+ * classifier so a failed terminal can never be quarantined on reload.
557
+ */
558
+ function canonicalFailedOutcome(
559
+ failure?: { code?: unknown; message?: unknown },
560
+ provenance: "agent_failed" | "deadline" = "agent_failed",
561
+ ): InvocationOutcome {
562
+ const deadline = provenance === "deadline" || failure?.code === "prompt_deadline_exceeded";
563
+ return {
564
+ kind: "failed",
565
+ code: deadline ? "prompt_deadline_exceeded" : "prompt_failed",
566
+ message: deadline ? "Prompt deadline exceeded." : EMPTY_PROMPT_FAILURE.message,
567
+ provenance: deadline ? "deadline" : "agent_failed",
568
+ };
569
+ }
570
+
571
+ function canonicalTerminalOutcome(
572
+ outcome: unknown,
573
+ failure?: { code?: unknown; message?: unknown },
574
+ ): InvocationOutcome | undefined {
575
+ if (outcome && typeof outcome === "object") {
576
+ const candidate = outcome as { kind?: unknown; reason?: unknown; provenance?: unknown; code?: unknown };
577
+ if (candidate.kind === "stopped") {
578
+ if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
579
+ return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
580
+ return undefined;
581
+ }
582
+ if (candidate.kind === "failed")
583
+ return canonicalFailedOutcome(
584
+ { code: candidate.code, message: (outcome as { message?: unknown }).message },
585
+ candidate.provenance === "deadline" ? "deadline" : "agent_failed",
586
+ );
587
+ }
588
+ if (failure !== undefined) return canonicalFailedOutcome(failure);
589
+ return undefined;
590
+ }
440
591
  interface InvocationRecord extends InvocationCorrelation {
441
592
  kind: InvocationKind;
442
593
  revision: number;
@@ -447,6 +598,9 @@ interface InvocationRecord extends InvocationCorrelation {
447
598
  startedAt?: number;
448
599
  terminalAt?: number;
449
600
  error?: { code: string; message: string };
601
+ content?: TurnResultContent;
602
+ receiptState?: Exclude<ReceiptState, "absent">;
603
+ outcome?: unknown;
450
604
  }
451
605
  export interface InvocationReconciliation {
452
606
  /** Shared v2 reconciliation owner; present for durable terminal admission. */
@@ -462,7 +616,7 @@ export interface InvocationReconciliation {
462
616
  type: "agent_start" | "agent_end";
463
617
  content?: TurnResultContent;
464
618
  hasActivity?: boolean;
465
- outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
619
+ outcome?: InvocationOutcome;
466
620
  }
467
621
  | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
468
622
  ): Promise<void>;
@@ -477,8 +631,8 @@ export interface InvocationReconciliation {
477
631
  claimPendingOutcome(
478
632
  kind: InvocationKind,
479
633
  correlation: InvocationCorrelation,
480
- outcome: { kind: string; code: string; message: string; provenance?: string },
481
- ): Promise<unknown>;
634
+ outcome: InvocationOutcomeInput,
635
+ ): Promise<InvocationOutcome>;
482
636
  // Positional compatibility (exact-head review P1): the 4th argument is either
483
637
  // the generation-fence isCurrent callback or the legacy recordError object;
484
638
  // both shapes are normalized in the implementation so a legacy object can
@@ -486,7 +640,7 @@ export interface InvocationReconciliation {
486
640
  finalizeOutcome(
487
641
  kind: InvocationKind,
488
642
  correlation: InvocationCorrelation,
489
- outcome?: { kind: string; code: string; message: string; provenance?: string },
643
+ outcome?: InvocationOutcomeInput,
490
644
  arg4?: (() => boolean) | { code: string; message: string },
491
645
  // Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
492
646
  // when an explicit outcome is absent, a content-bearing, activity-bearing,
@@ -729,25 +883,71 @@ export function createInvocationReconciliation(
729
883
  // so a real lifecycle event is never swallowed.
730
884
  }
731
885
  const current = records.get(recordKey);
886
+ const incomingOutcome = frame.type === "agent_end" ? canonicalTerminalOutcome(frame.outcome) : undefined;
887
+ const providerFailure =
888
+ current?.error !== undefined && current.error.code !== "prompt_deadline_exceeded";
889
+ if (
890
+ current !== undefined &&
891
+ frame.type === "agent_end" &&
892
+ incomingOutcome?.kind === "failed" &&
893
+ incomingOutcome.provenance !== "deadline"
894
+ ) {
895
+ const content = reduceTurnResultContent(current.content, frame.content);
896
+ const failed = {
897
+ ...current,
898
+ revision: ++mutationRevision,
899
+ status: "failed" as const,
900
+ outcome: incomingOutcome,
901
+ error:
902
+ current.error?.code !== "prompt_deadline_exceeded" && current.error !== undefined
903
+ ? current.error
904
+ : { code: incomingOutcome.code, message: incomingOutcome.message },
905
+ content,
906
+ receiptState: reduceReceiptState(
907
+ current.receiptState,
908
+ reportableTurnResultContent(content) ? "present" : undefined,
909
+ ),
910
+ };
911
+ delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
912
+ delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
913
+ records.set(recordKey, failed);
914
+ try {
915
+ await persist();
916
+ } catch (error) {
917
+ if (records.get(recordKey) === failed) records.set(recordKey, current);
918
+ throw error;
919
+ }
920
+ return;
921
+ }
732
922
  if (
733
923
  frame.type === "agent_end" &&
734
924
  current === pending.finalizedRecord &&
925
+ incomingOutcome?.kind !== "failed" &&
926
+ !providerFailure &&
735
927
  (kind !== "prompt" ||
736
- frame.outcome?.kind === "stopped" ||
928
+ incomingOutcome?.kind === "stopped" ||
737
929
  frame.hasActivity === true ||
738
930
  frame.content?.text.trim())
739
931
  ) {
740
932
  // A real lifecycle end that arrived during deadline finalization is
741
933
  // stronger evidence than the synthetic deadline outcome. Upgrade the
742
- // durable terminal instead of treating the event as a duplicate.
934
+ // durable terminal instead of treating it as a duplicate.
935
+ const content = reduceTurnResultContent(current.content, frame.content);
743
936
  const upgraded = {
744
937
  ...current,
745
938
  revision: ++mutationRevision,
746
939
  status: "terminal_ok" as const,
940
+ content,
941
+ ...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
942
+ receiptState: reduceReceiptState(
943
+ current.receiptState,
944
+ reportableTurnResultContent(content) ? "present" : undefined,
945
+ ),
747
946
  };
748
947
  // Drop the superseded synthetic deadline error so the surfaced payload is
749
948
  // not the contradictory {status: terminal_ok, error: prompt_deadline_exceeded}.
750
949
  delete (upgraded as Partial<InvocationRecord>).error;
950
+ if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
751
951
  records.set(recordKey, upgraded);
752
952
  try {
753
953
  await persist();
@@ -771,18 +971,115 @@ export function createInvocationReconciliation(
771
971
  }
772
972
  }
773
973
  if (record.terminalAt !== undefined) {
974
+ if (frame.type === "agent_end") {
975
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
976
+ if (incomingOutcome?.kind === "failed" && incomingOutcome.provenance !== "deadline") {
977
+ const content = reduceTurnResultContent(record.content, frame.content);
978
+ const failed = {
979
+ ...record,
980
+ revision: ++mutationRevision,
981
+ status: "failed" as const,
982
+ outcome: incomingOutcome,
983
+ error:
984
+ record.error?.code !== "prompt_deadline_exceeded" && record.error !== undefined
985
+ ? record.error
986
+ : { code: incomingOutcome.code, message: incomingOutcome.message },
987
+ content,
988
+ receiptState: reduceReceiptState(
989
+ record.receiptState,
990
+ reportableTurnResultContent(content) ? "present" : undefined,
991
+ ),
992
+ };
993
+ delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
994
+ delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
995
+ records.set(recordKey, failed);
996
+ try {
997
+ await persist();
998
+ } catch (error) {
999
+ if (records.get(recordKey) === failed) records.set(recordKey, record);
1000
+ throw error;
1001
+ }
1002
+ return;
1003
+ }
1004
+ }
1005
+ if (frame.type === "agent_failed" && record.error?.code === "prompt_deadline_exceeded") {
1006
+ const failure = sanitizePromptFailure(frame.error);
1007
+ const next = {
1008
+ ...record,
1009
+ revision: ++mutationRevision,
1010
+ status: "failed" as const,
1011
+ error: failure,
1012
+ outcome: canonicalFailedOutcome(failure),
1013
+ };
1014
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1015
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1016
+ records.set(recordKey, next);
1017
+ try {
1018
+ await persist();
1019
+ } catch (error) {
1020
+ if (records.get(recordKey) === next) records.set(recordKey, record);
1021
+ throw error;
1022
+ }
1023
+ return;
1024
+ }
1025
+ if (frame.type === "agent_end" && record.error?.code !== "prompt_deadline_exceeded") {
1026
+ const content = reduceTurnResultContent(record.content, frame.content);
1027
+ const receiptState = reduceReceiptState(
1028
+ record.receiptState,
1029
+ reportableTurnResultContent(content) ? "present" : undefined,
1030
+ );
1031
+ let outcome = record.outcome;
1032
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
1033
+ if (outcome === undefined) {
1034
+ if (record.status === "failed" && record.error !== undefined)
1035
+ outcome = canonicalFailedOutcome(record.error);
1036
+ else if (record.status === "terminal_ok" && incomingOutcome?.kind === "stopped")
1037
+ outcome = incomingOutcome;
1038
+ }
1039
+ if (content !== record.content || receiptState !== record.receiptState || outcome !== record.outcome) {
1040
+ const enriched = {
1041
+ ...record,
1042
+ revision: ++mutationRevision,
1043
+ content,
1044
+ receiptState,
1045
+ ...(outcome === undefined ? {} : { outcome }),
1046
+ };
1047
+ records.set(recordKey, enriched);
1048
+ try {
1049
+ await persist();
1050
+ } catch (error) {
1051
+ if (records.get(recordKey) === enriched) records.set(recordKey, record);
1052
+ throw error;
1053
+ }
1054
+ }
1055
+ return;
1056
+ }
774
1057
  if (
775
1058
  frame.type === "agent_end" &&
776
1059
  record.error?.code === "prompt_deadline_exceeded" &&
1060
+ canonicalTerminalOutcome(frame.outcome)?.kind !== "failed" &&
777
1061
  (kind !== "prompt" ||
778
- frame.outcome?.kind === "stopped" ||
1062
+ canonicalTerminalOutcome(frame.outcome)?.kind === "stopped" ||
779
1063
  frame.hasActivity === true ||
780
1064
  frame.content?.text.trim())
781
1065
  ) {
782
- const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
1066
+ const content = reduceTurnResultContent(record.content, frame.content);
1067
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
1068
+ const upgraded = {
1069
+ ...record,
1070
+ revision: ++mutationRevision,
1071
+ status: "terminal_ok" as const,
1072
+ content,
1073
+ ...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
1074
+ receiptState: reduceReceiptState(
1075
+ record.receiptState,
1076
+ reportableTurnResultContent(content) ? "present" : undefined,
1077
+ ),
1078
+ };
783
1079
  // The synthetic deadline error is superseded evidence; terminal_ok must not
784
1080
  // surface a deadline failure for a prompt that actually completed.
785
1081
  delete (upgraded as Partial<InvocationRecord>).error;
1082
+ if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
786
1083
  records.set(recordKey, upgraded);
787
1084
  try {
788
1085
  await persist();
@@ -839,19 +1136,43 @@ export function createInvocationReconciliation(
839
1136
  if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
840
1137
  next.error = failure;
841
1138
  } else {
1139
+ const pendingOutcome = (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1140
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1141
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1142
+ next.content = reduceTurnResultContent(next.content, frame.content);
1143
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome) ?? canonicalTerminalOutcome(pendingOutcome);
842
1144
  if (
1145
+ incomingOutcome?.kind === "stopped" &&
1146
+ (next.error === undefined || next.error.code === "prompt_deadline_exceeded")
1147
+ ) {
1148
+ next.outcome = incomingOutcome;
1149
+ if (next.error?.code === "prompt_deadline_exceeded") delete next.error;
1150
+ next.status = "terminal_ok";
1151
+ } else if (incomingOutcome?.kind === "failed") {
1152
+ next.outcome = incomingOutcome;
1153
+ if (next.error === undefined)
1154
+ next.error = { code: incomingOutcome.code, message: incomingOutcome.message };
1155
+ next.status = "failed";
1156
+ } else if (next.error !== undefined) {
1157
+ next.outcome = canonicalFailedOutcome(next.error);
1158
+ next.status = "failed";
1159
+ } else if (
843
1160
  kind === "prompt" &&
844
- next.error === undefined &&
845
1161
  frame.type === "agent_end" &&
846
- frame.outcome?.kind !== "stopped" &&
847
1162
  !frame.content?.text.trim() &&
848
1163
  !frame.hasActivity
849
1164
  ) {
850
1165
  next.status = "failed";
851
1166
  next.error = EMPTY_PROMPT_FAILURE;
852
- } else next.status = next.error === undefined ? "terminal_ok" : "failed";
1167
+ next.outcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
1168
+ } else next.status = "terminal_ok";
1169
+ next.receiptState = reduceReceiptState(
1170
+ next.receiptState,
1171
+ reportableTurnResultContent(next.content) ? "present" : "missing",
1172
+ );
853
1173
  next.terminalAt = Date.now();
854
1174
  }
1175
+
855
1176
  records.set(recordKey, next);
856
1177
  try {
857
1178
  await persist();
@@ -862,20 +1183,24 @@ export function createInvocationReconciliation(
862
1183
  },
863
1184
  lookup(kind, selector) {
864
1185
  const record = find(kind, selector);
865
- if (!record) return { status: "unknown" };
1186
+ if (!record) return { status: "unknown", receiptState: "unknown" };
866
1187
  const identity = {
867
1188
  commandId: record.commandId,
868
1189
  turnId: record.turnId,
869
1190
  ...(record.clientRef === undefined ? {} : { clientRef: record.clientRef }),
870
1191
  acceptedAt: record.acceptedAt,
871
1192
  };
872
- if (record.status === "accepted") return { status: "accepted", ...identity };
873
- if (record.status === "in_flight") return { status: "in_flight", ...identity, startedAt: record.startedAt };
1193
+ if (record.status === "accepted") return { status: "accepted", receiptState: "absent", ...identity };
1194
+ if (record.status === "in_flight")
1195
+ return { status: "in_flight", receiptState: "absent", ...identity, startedAt: record.startedAt };
874
1196
  return {
875
1197
  status: record.status,
876
1198
  ...identity,
877
1199
  ...(record.startedAt === undefined ? {} : { startedAt: record.startedAt }),
878
1200
  terminalAt: record.terminalAt,
1201
+ receiptState: record.receiptState ?? "unknown",
1202
+ ...(record.outcome === undefined ? {} : { outcome: record.outcome }),
1203
+ ...(reportableTurnResultContent(record.content) ? { content: record.content } : {}),
879
1204
  ...(record.error === undefined ? {} : { error: record.error }),
880
1205
  };
881
1206
  },
@@ -902,11 +1227,13 @@ export function createInvocationReconciliation(
902
1227
  hydrate,
903
1228
  async claimPendingOutcome(kind, correlation, outcome) {
904
1229
  const record = records.get(key(kind, correlation));
905
- if (!record || record.terminalAt !== undefined || record.kind !== kind) return outcome;
1230
+ const normalized = canonicalTerminalOutcome(outcome);
1231
+ if (normalized === undefined) throw new Error("Invalid terminal outcome.");
1232
+ if (!record || record.terminalAt !== undefined || record.kind !== kind) return normalized;
906
1233
  const pending = (record as unknown as { pendingOutcome?: unknown }).pendingOutcome;
907
- if (pending !== undefined) return pending;
1234
+ if (pending !== undefined) return canonicalTerminalOutcome(pending) ?? normalized;
908
1235
  const next = { ...record, revision: ++mutationRevision } as InvocationRecord & { pendingOutcome?: unknown };
909
- next.pendingOutcome = outcome;
1236
+ next.pendingOutcome = normalized;
910
1237
  records.set(key(kind, correlation), next);
911
1238
  try {
912
1239
  await persist();
@@ -914,7 +1241,7 @@ export function createInvocationReconciliation(
914
1241
  if (records.get(key(kind, correlation)) === next) records.set(key(kind, correlation), record);
915
1242
  throw error;
916
1243
  }
917
- return outcome;
1244
+ return normalized;
918
1245
  },
919
1246
  async finalizeOutcome(
920
1247
  kind,
@@ -946,36 +1273,64 @@ export function createInvocationReconciliation(
946
1273
  const recordKey = key(kind, correlation);
947
1274
  const record = records.get(recordKey);
948
1275
  if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
949
- const finalOutcome = (outcome ??
950
- (record as unknown as { pendingOutcome?: { kind: string; code: string; message: string } })
951
- .pendingOutcome) as { kind: string; code: string; message: string } | undefined;
1276
+ const requestedOutcome = canonicalTerminalOutcome(
1277
+ outcome ?? (record as unknown as { pendingOutcome?: unknown }).pendingOutcome,
1278
+ );
1279
+ const priorFailure = record.error;
1280
+ // A provider diagnostic recorded before a deadline finalization is stronger
1281
+ // than the synthetic deadline claim. Preserve it as the terminal failure
1282
+ // intent instead of allowing a later agent_end to look successful.
1283
+ const finalOutcome =
1284
+ priorFailure !== undefined && priorFailure.code !== "prompt_deadline_exceeded"
1285
+ ? canonicalFailedOutcome(priorFailure)
1286
+ : requestedOutcome?.kind === "stopped"
1287
+ ? requestedOutcome
1288
+ : priorFailure !== undefined &&
1289
+ (requestedOutcome === undefined ||
1290
+ (requestedOutcome.kind === "failed" &&
1291
+ (requestedOutcome.code === "prompt_deadline_exceeded" ||
1292
+ priorFailure.code !== "prompt_deadline_exceeded")))
1293
+ ? canonicalFailedOutcome(priorFailure)
1294
+ : requestedOutcome;
952
1295
  const previousRecord = { ...record };
953
1296
  const finalizedRecord: InvocationRecord = { ...record, revision: ++mutationRevision, terminalAt: Date.now() };
954
- if (finalOutcome?.kind === "failed") {
1297
+ finalizedRecord.content = reduceTurnResultContent(finalizedRecord.content, evidence?.content);
1298
+ let resolvedOutcome = finalOutcome;
1299
+ if (resolvedOutcome?.kind === "stopped") {
1300
+ // Stopped is a successful terminal boundary: it must never coexist with
1301
+ // an earlier diagnostic failure.
1302
+ finalizedRecord.status = "terminal_ok";
1303
+ delete finalizedRecord.error;
1304
+ } else if (resolvedOutcome?.kind === "failed") {
955
1305
  finalizedRecord.status = "failed";
956
- // Legacy positional recordError overrides the recorded cause; the
957
- // outcome's own code/message is the default (exact-head review P1).
958
1306
  finalizedRecord.error =
959
1307
  recordError !== undefined
960
1308
  ? { code: recordError.code, message: recordError.message }
961
- : { code: finalOutcome.code, message: finalOutcome.message };
962
- } else if (kind === "prompt" && finalOutcome === undefined) {
1309
+ : priorFailure !== undefined &&
1310
+ (resolvedOutcome.code === "prompt_deadline_exceeded" ||
1311
+ priorFailure.code !== "prompt_deadline_exceeded")
1312
+ ? priorFailure
1313
+ : { code: resolvedOutcome.code, message: resolvedOutcome.message };
1314
+ } else if (kind === "prompt") {
963
1315
  // Evidence-based empty predicate (exact-head review P1/P2): the same
964
1316
  // semantics as the noteTransition agent_end classifier. A
965
- // content-bearing, activity-bearing, or explicitly stopped
966
- // finalization without an explicit outcome is a successful terminal;
1317
+ // content-bearing or activity-bearing finalization is successful;
967
1318
  // only a genuinely empty, no-activity completion fails closed.
968
1319
  const terminalText = evidence?.content?.text.trim() ?? "";
969
1320
  if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
1321
+ resolvedOutcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
970
1322
  finalizedRecord.status = "failed";
971
1323
  finalizedRecord.error = EMPTY_PROMPT_FAILURE;
972
- } else {
973
- finalizedRecord.status = "terminal_ok";
974
- }
975
- } else {
976
- finalizedRecord.status = "terminal_ok";
977
- }
978
- (finalizedRecord as unknown as Record<string, unknown>).pendingOutcome = undefined;
1324
+ } else finalizedRecord.status = "terminal_ok";
1325
+ } else finalizedRecord.status = "terminal_ok";
1326
+ finalizedRecord.receiptState = reduceReceiptState(
1327
+ finalizedRecord.receiptState,
1328
+ reportableTurnResultContent(finalizedRecord.content) ? "present" : "unknown",
1329
+ );
1330
+ if (resolvedOutcome === undefined) delete finalizedRecord.outcome;
1331
+ else finalizedRecord.outcome = resolvedOutcome;
1332
+ delete (finalizedRecord as unknown as Record<string, unknown>).pendingOutcome;
1333
+ delete (finalizedRecord as unknown as Record<string, unknown>).pendingReceiptState;
979
1334
  if (isCurrent !== undefined && !isCurrent()) return;
980
1335
  const commit = Promise.withResolvers<void>();
981
1336
  // This promise is an internal coordination signal for lifecycle transitions
@@ -1025,8 +1380,13 @@ export function createInvocationReconciliation(
1025
1380
  };
1026
1381
  if (isCurrent !== undefined && !isCurrent()) return;
1027
1382
  delete next.terminalAt;
1028
- delete next.error;
1029
- delete (next as unknown as { outcome?: unknown }).outcome;
1383
+ if (next.error?.code === "prompt_deadline_exceeded" || !isPreservedProviderFailure(next.error?.code))
1384
+ delete next.error;
1385
+ delete next.receiptState;
1386
+ delete (next as unknown as { outcome?: unknown; pendingOutcome?: unknown; pendingReceiptState?: unknown })
1387
+ .outcome;
1388
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1389
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1030
1390
  (next as unknown as { deadlineRecoveryPending?: boolean }).deadlineRecoveryPending = true;
1031
1391
  if (deadlineMaxAt !== undefined) (next as unknown as { deadlineMaxAt?: number }).deadlineMaxAt = deadlineMaxAt;
1032
1392
  records.set(recordKey, next);
@@ -1065,6 +1425,7 @@ export interface SdkSurfaceFactoryOptions {
1065
1425
  }) => unknown;
1066
1426
  steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
1067
1427
  hostTools?: boolean | (() => boolean);
1428
+ getRuntimeHost?: () => SessionSdkHost | undefined;
1068
1429
  }
1069
1430
 
1070
1431
  /** Shared policy, capability, and query-surface factory for every SDK transport. */
@@ -1094,6 +1455,7 @@ function createQuerySurface(
1094
1455
  }) => unknown;
1095
1456
  steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
1096
1457
  hostTools?: boolean | (() => boolean);
1458
+ getRuntimeHost?: () => SessionSdkHost | undefined;
1097
1459
  } = {},
1098
1460
  ): SessionSurface {
1099
1461
  const policy =
@@ -1117,18 +1479,15 @@ function createQuerySurface(
1117
1479
  kind: ctx.sessionMetadata?.kind ?? "main",
1118
1480
  });
1119
1481
  const lastAssistant = () => {
1120
- for (const entry of ctx.sessionManager.getBranch().toReversed()) {
1121
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
1122
- const content = entry.message.content;
1123
- if (typeof content === "string") return content;
1124
- if (Array.isArray(content))
1125
- return content
1126
- .filter(
1127
- (block): block is { type: "text"; text: string } =>
1128
- block.type === "text" && typeof block.text === "string",
1129
- )
1130
- .map(block => block.text)
1131
- .join("");
1482
+ const transcript =
1483
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
1484
+ for (const entry of transcript.toReversed()) {
1485
+ if (entry.role !== "assistant") continue;
1486
+ const text =
1487
+ typeof entry.body === "string"
1488
+ ? entry.body
1489
+ : entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
1490
+ if (text !== undefined && text.trim().length > 0) return text;
1132
1491
  }
1133
1492
  return undefined;
1134
1493
  };
@@ -1500,6 +1859,28 @@ function createQuerySurface(
1500
1859
  turnId?: string;
1501
1860
  clientRef?: string;
1502
1861
  }) => (options.turnResultLookup ?? (value => reconciliation.lookupResult(value.kind, value)))(selector),
1862
+ getCheckpointSnapshot: () => {
1863
+ const host = options.getRuntimeHost?.();
1864
+ if (!host)
1865
+ throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
1866
+ code: "unavailable",
1867
+ });
1868
+ const entries =
1869
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : undefined;
1870
+ if (!Array.isArray(entries))
1871
+ throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
1872
+ code: "unavailable",
1873
+ });
1874
+ return {
1875
+ entries,
1876
+ watermark: {
1877
+ revision: entries.length,
1878
+ generation: host.events.generation,
1879
+ seq: host.events.sequence,
1880
+ idle: ctx.isIdle(),
1881
+ },
1882
+ };
1883
+ },
1503
1884
  getSteerStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1504
1885
  (options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
1505
1886
  getModelProfiles: async () => {
@@ -1549,6 +1930,7 @@ export function createSdkSurfaceFactory(
1549
1930
  turnResultLookup: options.turnResultLookup,
1550
1931
  steerStatusLookup: options.steerStatusLookup,
1551
1932
  hostTools: options.hostTools,
1933
+ getRuntimeHost: options.getRuntimeHost,
1552
1934
  });
1553
1935
  return {
1554
1936
  policy,
@@ -1856,6 +2238,7 @@ function createControlSurface(
1856
2238
  ) => void,
1857
2239
  canResolveGate: () => boolean = () => true,
1858
2240
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2241
+ onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
1859
2242
  ): ControlSurface {
1860
2243
  const normalizePromptImages = (value: unknown): ImageContent[] => {
1861
2244
  if (!Array.isArray(value)) return [];
@@ -2103,18 +2486,9 @@ function createControlSurface(
2103
2486
  try {
2104
2487
  await reconciliation.noteTransition(kind, correlation, {
2105
2488
  type: "agent_end",
2106
- ...(typeof result === "string"
2107
- ? {
2108
- content: {
2109
- version: 1,
2110
- type: "text",
2111
- text: result,
2112
- byteLength: 0,
2113
- truncated: false,
2114
- },
2115
- }
2116
- : {}),
2489
+ ...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
2117
2490
  });
2491
+ onInvocationCompletionReconciledForTests?.(kind, correlation);
2118
2492
  retirePendingOwner?.(kind, correlation, "always");
2119
2493
  } catch (transitionError) {
2120
2494
  if (kind === "prompt" || kind === "skill") {
@@ -2269,7 +2643,7 @@ function createControlSurface(
2269
2643
  }
2270
2644
  };
2271
2645
  const terminalAbort = async (
2272
- input: { mode: "terminal"; scope?: "turn" | "owned" },
2646
+ input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
2273
2647
  idempotencyKey?: string,
2274
2648
  ): Promise<unknown> => {
2275
2649
  const scope = input.scope === "owned" ? "owned" : "turn";
@@ -2300,10 +2674,12 @@ function createControlSurface(
2300
2674
  typeof idempotencyKey === "string"
2301
2675
  ? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
2302
2676
  : undefined;
2303
- const inputHash = crypto
2304
- .createHash("sha256")
2305
- .update(JSON.stringify({ mode: "terminal", scope }))
2306
- .digest("hex");
2677
+ const abortIdentity = terminalAbortIdentity(
2678
+ { mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
2679
+ input.operator === true,
2680
+ );
2681
+ if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
2682
+ const inputHash = abortIdentity.inputHash;
2307
2683
  const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
2308
2684
  responseState: record.responseState ?? "pending",
2309
2685
  responsePayloadHash: record.responsePayloadHash ?? inputHash,
@@ -2676,6 +3052,16 @@ function createControlSurface(
2676
3052
  pendingPreflights.delete(requesterBucketKey);
2677
3053
  }
2678
3054
  };
3055
+ const returnNoActiveTurn = async () => {
3056
+ const result = {
3057
+ ok: true,
3058
+ selection: scope,
3059
+ turn: "no_active_turn",
3060
+ terminal: "terminal_no_effect",
3061
+ };
3062
+ await finalizeNoEffectReservation(result);
3063
+ return result;
3064
+ };
2679
3065
  if (!handle || epoch === undefined) {
2680
3066
  if ((await writeNoEffect()) === "conflict") {
2681
3067
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
@@ -2691,6 +3077,10 @@ function createControlSurface(
2691
3077
  if (replayed !== undefined) return replayed;
2692
3078
  }
2693
3079
  cancelRequesterPreflights();
3080
+ // Operator authority overrides connection ownership only for a turn
3081
+ // observed at admission. It must never adopt a successor that starts
3082
+ // while the durable no-effect reservation is being written.
3083
+ if (input.operator === true) return await returnNoActiveTurn();
2694
3084
  // A prompt for this requester may have become ACTIVE while the
2695
3085
  // reservation awaited the filesystem transaction: its submit()
2696
3086
  // cleanup already removed the preflight callback, so cancelling here
@@ -2706,14 +3096,7 @@ function createControlSurface(
2706
3096
  // No prompt won the race: finalize the reserved row so a later
2707
3097
  // same-key retry replays this deterministic no_active_turn result
2708
3098
  // (review thread P2).
2709
- const noActiveTurnResult = {
2710
- ok: true,
2711
- selection: scope,
2712
- turn: "no_active_turn",
2713
- terminal: "terminal_no_effect",
2714
- };
2715
- await finalizeNoEffectReservation(noActiveTurnResult);
2716
- return noActiveTurnResult;
3099
+ return await returnNoActiveTurn();
2717
3100
  }
2718
3101
  handle = recheckedHandle;
2719
3102
  epoch = recheckedEpoch;
@@ -2743,7 +3126,11 @@ function createControlSurface(
2743
3126
  // cleared after a terminal lifecycle boundary) authorizes no client, and
2744
3127
  // a stale prior owner authorizes only that old client — never a later
2745
3128
  // turn it did not submit (review thread P1).
2746
- if (handle && (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))) {
3129
+ if (
3130
+ input.operator !== true &&
3131
+ handle &&
3132
+ (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
3133
+ ) {
2747
3134
  if ((await writeNoEffect()) === "conflict") {
2748
3135
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
2749
3136
  code: "idempotency_conflict",
@@ -3412,6 +3799,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
3412
3799
  });
3413
3800
  }
3414
3801
 
3802
+ export function masterAttestationForEffectiveHost(input: {
3803
+ masterCapability: string | undefined;
3804
+ attestationEpoch: string | undefined;
3805
+ ownerSessionId?: string;
3806
+ sessionId: string;
3807
+ pid: number;
3808
+ processIncarnation: string | undefined;
3809
+ direct: MasterRoleAttestationV2 | undefined;
3810
+ }): MasterRoleAttestationV2 | undefined {
3811
+ const direct = input.direct;
3812
+ if (
3813
+ input.masterCapability === undefined ||
3814
+ input.attestationEpoch === undefined ||
3815
+ direct === undefined ||
3816
+ direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
3817
+ direct.launchPid !== input.pid ||
3818
+ direct.launchProcessIncarnation !== input.processIncarnation ||
3819
+ direct.attestationEpoch !== input.attestationEpoch
3820
+ )
3821
+ return undefined;
3822
+ return direct;
3823
+ }
3824
+
3825
+ function masterDirectAttestation(input: {
3826
+ masterCapability: string | undefined;
3827
+ attestationEpoch: string | undefined;
3828
+ ownerSessionId?: string;
3829
+ sessionId: string;
3830
+ pid: number;
3831
+ processIncarnation: string | undefined;
3832
+ }): MasterRoleAttestationV2 | undefined {
3833
+ if (
3834
+ input.masterCapability === undefined ||
3835
+ input.attestationEpoch === undefined ||
3836
+ input.processIncarnation === undefined
3837
+ )
3838
+ return undefined;
3839
+ return {
3840
+ version: 2,
3841
+ ownerSessionId: input.ownerSessionId ?? input.sessionId,
3842
+ launchPid: input.pid,
3843
+ launchProcessIncarnation: input.processIncarnation,
3844
+ role: "master",
3845
+ attestationEpoch: input.attestationEpoch,
3846
+ };
3847
+ }
3848
+
3849
+ function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
3850
+ return (
3851
+ left.version === right.version &&
3852
+ left.ownerSessionId === right.ownerSessionId &&
3853
+ left.launchPid === right.launchPid &&
3854
+ left.launchProcessIncarnation === right.launchProcessIncarnation &&
3855
+ left.role === right.role &&
3856
+ left.attestationEpoch === right.attestationEpoch
3857
+ );
3858
+ }
3859
+
3860
+ /**
3861
+ * Establish direct master authority for the session currently hosted by this
3862
+ * process. Session identity transitions retain the master capability and epoch
3863
+ * but require a new direct attestation before their effective endpoint can
3864
+ * adopt that authority.
3865
+ */
3866
+ export async function reattestMasterSessionIdentity(input: {
3867
+ index: SessionIndex;
3868
+ locator: SessionLocatorV2;
3869
+ masterCapability: string | undefined;
3870
+ attestationEpoch: string | undefined;
3871
+ ownerSessionId?: string;
3872
+ sessionId: string;
3873
+ pid: number;
3874
+ processIncarnation: string | undefined;
3875
+ }): Promise<MasterRoleAttestationV2 | undefined> {
3876
+ const masterRole = masterDirectAttestation(input);
3877
+ if (!masterRole) return undefined;
3878
+ const directExists = input.index.listSessionIdentities().some(row => {
3879
+ const processIdentity = row.hostIncarnation ?? row.processIncarnation;
3880
+ return (
3881
+ row.sessionId === input.sessionId &&
3882
+ row.endpointGeneration === 0 &&
3883
+ row.pid === input.pid &&
3884
+ processIdentity === input.processIncarnation &&
3885
+ row.masterRole !== undefined &&
3886
+ sameMasterAttestation(row.masterRole, masterRole)
3887
+ );
3888
+ });
3889
+ if (!directExists) {
3890
+ await input.index.append({
3891
+ type: "host_registered",
3892
+ sessionId: input.sessionId,
3893
+ locator: input.locator,
3894
+ endpointGeneration: 0,
3895
+ pid: input.pid,
3896
+ processIncarnation: input.processIncarnation,
3897
+ masterRole,
3898
+ });
3899
+ }
3900
+ return masterRole;
3901
+ }
3902
+
3415
3903
  function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
3416
3904
  const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
3417
3905
  const id = typeof frame.id === "string" ? frame.id : "";
@@ -3653,7 +4141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3653
4141
  lifecycleOwner?: LifecycleOwner,
3654
4142
  terminalContent?: TurnResultContent,
3655
4143
  terminalHasActivity = false,
3656
- terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
4144
+ terminalOutcome?: InvocationOutcome,
3657
4145
  ): Promise<void> => {
3658
4146
  const current = lifecycleOwner?.state ?? active;
3659
4147
  if (!current) return;
@@ -3918,7 +4406,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3918
4406
  ? ({
3919
4407
  content: terminalContent,
3920
4408
  hasActivity: terminalHasActivity,
3921
- outcome: terminalOutcome,
4409
+ ...(terminalOutcome?.kind === "stopped" ? { outcome: terminalOutcome } : {}),
3922
4410
  } satisfies PromptTerminalTransitionEvidence)
3923
4411
  : undefined,
3924
4412
  );
@@ -4122,7 +4610,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4122
4610
  const terminalOutcome =
4123
4611
  event.stopReason === "cancelled"
4124
4612
  ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4125
- : undefined;
4613
+ : failure === undefined
4614
+ ? undefined
4615
+ : canonicalFailedOutcome(failure);
4126
4616
  return trackLifecycle(async () => {
4127
4617
  if (failure && !failureAlreadyPublished) {
4128
4618
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -4172,6 +4662,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4172
4662
  await current.registerBroker();
4173
4663
  current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
4174
4664
  });
4665
+ const consumedMasterNonces = new Map<string, number>();
4175
4666
  api.on("turn_end", (_event, ctx) =>
4176
4667
  lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
4177
4668
  type: "turn_end",
@@ -4310,6 +4801,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4310
4801
  });
4311
4802
  await Promise.race([settled, timeout]);
4312
4803
  };
4804
+ let runtime: SessionSdkSessionRuntime;
4313
4805
  const surfaceFactory = createSdkSurfaceFactory({
4314
4806
  ctx,
4315
4807
  id: sessionId,
@@ -4319,6 +4811,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4319
4811
  steerStatusLookup: selector => steerReconciliation.lookupSteer(selector),
4320
4812
  configOverrides: options.configOverrides,
4321
4813
  settings: options.settings,
4814
+ getRuntimeHost: () => runtime?.host,
4322
4815
  });
4323
4816
  const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
4324
4817
  const inputGate = { quiescing: false };
@@ -4375,7 +4868,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4375
4868
  skillRecoveryControllers.delete(key);
4376
4869
  });
4377
4870
  };
4378
- let runtime: SessionSdkSessionRuntime;
4379
4871
  // Durable-first bounded terminalization for accepted submissions that leave
4380
4872
  // their queue or race a run WITHOUT consumption (exact-head review: clearing
4381
4873
  // the lease before the durable writes strands the row accepted with no
@@ -4664,6 +5156,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4664
5156
  },
4665
5157
  () => acceptingGateResolutions,
4666
5158
  trackGateResolution,
5159
+ options.onInvocationCompletionReconciledForTests,
4667
5160
  );
4668
5161
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
4669
5162
  if (capability === "permission") {
@@ -4731,6 +5224,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4731
5224
  };
4732
5225
  runtime = new SessionSdkSessionRuntime({
4733
5226
  transport,
5227
+ masterCapabilityVerify: frame =>
5228
+ verifyMasterCapabilityFrame({
5229
+ frame,
5230
+ expectedCapability: options.masterCapability,
5231
+ expectedEpoch: options.masterAttestationEpoch,
5232
+ replay: consumedMasterNonces,
5233
+ }),
4734
5234
  onFrameAdmission: (_connectionId, frame) =>
4735
5235
  inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
4736
5236
  control: async (connectionId, frame) => {
@@ -4776,21 +5276,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4776
5276
  (request.input as { mode?: unknown }).mode !== "terminal"
4777
5277
  )
4778
5278
  return;
4779
- const input = request.input as Record<string, unknown>;
4780
- const mode = input.mode;
4781
- const rawScope = input.scope;
4782
- if (mode !== "terminal") return;
4783
- if (rawScope !== undefined && rawScope !== "turn" && rawScope !== "owned") return;
4784
- // A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
4785
- // rejected by dispatch must never hash as the valid turn input and
4786
- // advance the legitimate stored row (review thread P2).
4787
- for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
4788
- const scopeInput = rawScope === "owned" ? "owned" : "turn";
5279
+ const rawInput = request.input as Record<string, unknown>;
5280
+ const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
5281
+ const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
5282
+ const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
5283
+ if (!abortIdentity) return;
4789
5284
  const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
4790
- const inputHash = crypto
4791
- .createHash("sha256")
4792
- .update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
4793
- .digest("hex");
5285
+ const inputHash = abortIdentity.inputHash;
4794
5286
  // Hash the ACTUAL written response payload: the durable state may only
4795
5287
  // advance when the written response corresponds to the row's payload.
4796
5288
  // When more than 256 concurrent requests evict an in-flight abort from
@@ -4843,24 +5335,103 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4843
5335
  },
4844
5336
  });
4845
5337
  if (isAutoroutingInactive(api)) markAutoroutingInactive(runtime.host);
4846
- const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
5338
+ const disposeWorkflowGate = ctx.workflowGate?.onGateEmitted?.(gate =>
4847
5339
  runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
4848
5340
  );
5341
+ // Every fold (steer, chord, auto-background timer, `bash.background`)
5342
+ // publishes one replayable `bash_folded` frame; `runtime.jobs.list`
5343
+ // `foldReason` remains the source of truth clients reconcile against after
5344
+ // a replay, since a replayed frame may describe a job that has since ended.
5345
+ const disposeFold = ctx.onJobFold?.(event => {
5346
+ try {
5347
+ runtime.emitEvent({
5348
+ type: "bash_folded",
5349
+ sessionId,
5350
+ jobId: event.jobId,
5351
+ generation: event.generation,
5352
+ reason: event.reason,
5353
+ });
5354
+ } catch (error) {
5355
+ // The fold already happened; publication failure must not affect it,
5356
+ // but it must leave evidence because a client relying on the event
5357
+ // instead of runtime.jobs.list would otherwise never learn of the fold.
5358
+ logger.warn("sdk: bash_folded publication failed", {
5359
+ jobId: event.jobId,
5360
+ reason: event.reason,
5361
+ error: error instanceof Error ? error.message : String(error),
5362
+ });
5363
+ }
5364
+ });
5365
+ const disposeGate = (): void => {
5366
+ disposeWorkflowGate?.();
5367
+ disposeFold?.();
5368
+ };
5369
+ let publishedEndpointUrl: string | undefined;
4849
5370
  let brokerRegistered = false;
4850
5371
  const registerBroker = async (): Promise<void> => {
4851
5372
  if (brokerRegistered) return;
4852
5373
  try {
5374
+ if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
5375
+ throw new Error("Lifecycle broker registration requires a request identity.");
4853
5376
  await ensureBroker({ agentDir: options.agentDir });
4854
5377
  const index = await new SessionIndex(options.agentDir).open();
4855
- const locator = { repo: path.resolve(ctx.cwd), stateRoot };
5378
+ const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
5379
+ const effectiveIncarnation = processIncarnation(process.pid);
5380
+ const direct = await reattestMasterSessionIdentity({
5381
+ index,
5382
+ locator,
5383
+ masterCapability: options.masterCapability,
5384
+ attestationEpoch: options.masterAttestationEpoch,
5385
+ ownerSessionId: options.masterOwnerSessionId,
5386
+ sessionId,
5387
+ pid: process.pid,
5388
+ processIncarnation: effectiveIncarnation,
5389
+ });
4856
5390
  await runtime.registerWithBroker({
4857
5391
  register: async input => {
4858
- const endpointMtimeMs = (await fs.stat(path.join(input.stateRoot, "sdk", `${input.sessionId}.json`)))
4859
- .mtimeMs;
4860
- await index.append({ type: "host_registered", ...input, locator, pid: process.pid, endpointMtimeMs });
5392
+ if (publishedEndpointUrl === undefined)
5393
+ throw new Error("SDK transport endpoint was not published before broker registration.");
5394
+ const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
5395
+ const file = await readEndpointFile(endpointPath);
5396
+ if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
5397
+ const endpoint = JSON.parse(file.source) as Record<string, unknown>;
5398
+ if (
5399
+ endpoint.sessionId !== input.sessionId ||
5400
+ endpoint.pid !== process.pid ||
5401
+ endpoint.url !== publishedEndpointUrl ||
5402
+ endpoint.token !== transport.token
5403
+ )
5404
+ throw new Error("SDK endpoint did not match the published transport authority.");
5405
+ const endpointMtimeMs = file.mtimeMs;
5406
+ const endpointFileId = `${file.dev}:${file.ino}`;
5407
+ const masterRole = masterAttestationForEffectiveHost({
5408
+ masterCapability: options.masterCapability,
5409
+ attestationEpoch: options.masterAttestationEpoch,
5410
+ ownerSessionId: options.masterOwnerSessionId,
5411
+ sessionId: input.sessionId,
5412
+ pid: process.pid,
5413
+ processIncarnation: effectiveIncarnation,
5414
+ direct,
5415
+ });
5416
+ await index.append({
5417
+ type: "host_registered",
5418
+ ...input,
5419
+ locator,
5420
+ pid: process.pid,
5421
+ endpointMtimeMs,
5422
+ endpointFileId,
5423
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5424
+ ...(masterRole ? { masterRole } : {}),
5425
+ });
4861
5426
  },
4862
5427
  unregister: async input => {
4863
- await index.append({ type: "host_unregistered", ...input, locator, pid: process.pid });
5428
+ await index.append({
5429
+ type: "host_unregistered",
5430
+ ...input,
5431
+ locator,
5432
+ pid: process.pid,
5433
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5434
+ });
4864
5435
  },
4865
5436
  });
4866
5437
  brokerRegistered = true;
@@ -4899,7 +5470,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4899
5470
  lifecycleOwnerHolder.state = runtimeOwner;
4900
5471
  active = runtimeOwner;
4901
5472
  try {
4902
- await runtime.start();
5473
+ publishedEndpointUrl = (await runtime.start()).url;
4903
5474
  await registerBroker();
4904
5475
  } catch (error) {
4905
5476
  active = undefined;