@gajae-code/coding-agent 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  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/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  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/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -5,16 +5,30 @@ import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { ThinkingLevel } from "@gajae-code/agent-core";
8
- import type { Api, Model } from "@gajae-code/ai/core";
8
+ import type { Api, ImageContent, Model } from "@gajae-code/ai/core";
9
9
  import { logger } from "@gajae-code/utils";
10
10
  import { AsyncJobManager } from "../../async";
11
+ import {
12
+ getProxyRoutableProviders,
13
+ inspectProxyProviderId,
14
+ requiresQualifiedModelProfileRoleResolution,
15
+ resolveProxyMode,
16
+ rewriteSelectorForProxy,
17
+ tryResolveProxyProviderId,
18
+ } from "../../config/model-profile-activation";
11
19
  import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
12
- import { type ModelProfileDefinition, resolveProfileBindings } from "../../config/model-profiles";
20
+ import {
21
+ deriveModelProfileMappedProviders,
22
+ type ModelProfileDefinition,
23
+ resolveProfileBindings,
24
+ } from "../../config/model-profiles";
25
+ import { isAuthenticated, kNoAuth } from "../../config/model-registry";
13
26
  import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../config/model-resolver";
14
27
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
15
28
  import { type Settings, validateSettingPatch } from "../../config/settings";
16
29
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
17
30
  import { normalizeGoal } from "../../goals/state";
31
+ import type { SdkRunCapability } from "../../session/sdk-run-capability";
18
32
  import {
19
33
  boundTerminalRetentionState,
20
34
  findOwnedRegistrationsForTurn,
@@ -23,10 +37,16 @@ import {
23
37
  settleOwnedWork,
24
38
  } from "../../session/terminal-abort";
25
39
  import { parseThinkingLevel } from "../../thinking";
40
+ import { readEndpointFile } from "../broker/endpoint-authority";
26
41
  import { ensureBroker } from "../broker/ensure";
27
- 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";
28
49
  import {
29
- collectAuthenticatedProfileProviders,
30
50
  parseSyntheticModelId,
31
51
  resolveSyntheticModelSelection,
32
52
  SYNTHETIC_PROVIDER_ID,
@@ -34,8 +54,9 @@ import {
34
54
  syntheticNamespaceCollision,
35
55
  } from "../model-profile-model";
36
56
  import { projectQ10Models } from "../models.js";
37
- import { PromptDeadlineManager } from "../prompt-deadline-manager";
57
+ import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
38
58
  import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
59
+ import { validateRequiredPromptText } from "../protocol/adapter-validation";
39
60
  import { OPERATIONS } from "../protocol/operation-registry";
40
61
  import {
41
62
  createKindAwareReconciliation,
@@ -43,11 +64,17 @@ import {
43
64
  type KindAwareReconciliation,
44
65
  resolveReconciliationSessionFile,
45
66
  } from "../reconciliation-extensions";
46
- import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
47
- import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
67
+ import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
68
+ import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
69
+ import {
70
+ BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
71
+ BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
72
+ hasBrokerRuntimeAbortCapability,
73
+ } from "./control/runtime-gate";
48
74
  import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
49
75
  import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
50
76
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
77
+ import { createSdkRunCapability } from "./sdk-run-capability";
51
78
  import {
52
79
  createSdkCapabilities,
53
80
  createSdkSurfacePolicyForContext,
@@ -87,6 +114,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
87
114
  * after this bound are abandoned — their durable broker state is the recovery
88
115
  * authority, and outcomes are inherently uncertain. */
89
116
  const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
117
+ /** Master verification fields are ephemeral protocol values, not unbounded input. */
118
+ const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
119
+ const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
120
+ /** Keep replay protection finite even when an authenticated client floods nonces. */
121
+ const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
122
+ /** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
123
+ const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
124
+
125
+ export type MasterCapabilityReplayState = Map<string, number>;
126
+
127
+ export function verifyMasterCapabilityFrame(input: {
128
+ frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
129
+ expectedCapability: string | undefined;
130
+ expectedEpoch: string | undefined;
131
+ replay: MasterCapabilityReplayState;
132
+ now?: number;
133
+ }): { ok: boolean; nonce: string; attestationEpoch: string } {
134
+ const now = input.now ?? Date.now();
135
+ for (const [consumedNonce, expiresAt] of input.replay) {
136
+ if (expiresAt <= now) input.replay.delete(consumedNonce);
137
+ }
138
+ const bounded = (value: unknown): value is string =>
139
+ typeof value === "string" &&
140
+ value.length > 0 &&
141
+ value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
142
+ MASTER_AUTH_VALUE_PATTERN.test(value);
143
+ const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
144
+ const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
145
+ const capability = bounded(input.frame.capability) ? input.frame.capability : "";
146
+ const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
147
+ const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
148
+ const authenticatedFields =
149
+ bounded(input.frame.nonce) &&
150
+ bounded(input.frame.attestationEpoch) &&
151
+ bounded(input.frame.capability) &&
152
+ bounded(input.expectedCapability) &&
153
+ bounded(input.expectedEpoch) &&
154
+ epoch === expectedEpoch;
155
+ const expectedBytes = Buffer.from(expectedCapability);
156
+ const capabilityBytes = Buffer.from(capability);
157
+ const capabilityMatches =
158
+ authenticatedFields &&
159
+ expectedBytes.length === capabilityBytes.length &&
160
+ crypto.timingSafeEqual(expectedBytes, capabilityBytes);
161
+ const reusable = capabilityMatches && !input.replay.has(nonce);
162
+ if (reusable) {
163
+ if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
164
+ const oldest = input.replay.keys().next().value;
165
+ if (typeof oldest === "string") input.replay.delete(oldest);
166
+ }
167
+ input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
168
+ }
169
+ return {
170
+ ok: reusable,
171
+ nonce,
172
+ attestationEpoch: authenticatedFields ? epoch : "",
173
+ };
174
+ }
175
+
90
176
  /** Maximum time a replaced runtime may retain a lifecycle persistence task. */
91
177
  const LIFECYCLE_QUIESCENCE_MS = 1_000;
92
178
 
@@ -126,6 +212,8 @@ export interface SessionSdkRuntimeOptions
126
212
  settings?: Settings;
127
213
  /** Mutable shadow of patched config values merged into query readback. */
128
214
  configOverrides?: Map<string, unknown>;
215
+ /** Stable master lineage identity retained across session switches/branches. */
216
+ masterOwnerSessionId?: string;
129
217
  }
130
218
 
131
219
  export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
@@ -384,6 +472,8 @@ export interface CreateSdkSessionRuntimeOptions {
384
472
  agentDir: string;
385
473
  /** Lifecycle-owned sessions require broker publication before they become usable. */
386
474
  brokerRegistrationRequired?: boolean;
475
+ /** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
476
+ lifecycleRequestId?: string;
387
477
  createTransport(input: {
388
478
  sessionId: string;
389
479
  stateRoot: string;
@@ -395,6 +485,12 @@ export interface CreateSdkSessionRuntimeOptions {
395
485
  onSdkRequest?: SessionSdkHostOptions["onRequest"];
396
486
  /** Mutable shadow of patched config values merged into query readback. */
397
487
  configOverrides?: Map<string, unknown>;
488
+ /** In-memory master capability for private broker verification only. */
489
+ masterCapability?: string;
490
+ /** Opaque direct-role epoch this effective host may adopt. */
491
+ masterAttestationEpoch?: string;
492
+ /** Stable master lineage identity retained across session switches/branches. */
493
+ masterOwnerSessionId?: string;
398
494
  /** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
399
495
  terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
400
496
  /** Callback when a frame is admitted to the runtime (test harness). */
@@ -420,6 +516,7 @@ export interface InvocationCorrelation {
420
516
 
421
517
  export type InvocationKind = "prompt" | "skill" | "steer";
422
518
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
519
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
423
520
  interface InvocationRecord extends InvocationCorrelation {
424
521
  kind: InvocationKind;
425
522
  revision: number;
@@ -440,7 +537,14 @@ export interface InvocationReconciliation {
440
537
  noteTransition(
441
538
  kind: InvocationKind,
442
539
  correlation: InvocationCorrelation | undefined,
443
- frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
540
+ frame:
541
+ | {
542
+ type: "agent_start" | "agent_end";
543
+ content?: TurnResultContent;
544
+ hasActivity?: boolean;
545
+ outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
546
+ }
547
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
444
548
  ): Promise<void>;
445
549
  lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
446
550
  lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
@@ -464,7 +568,13 @@ export interface InvocationReconciliation {
464
568
  correlation: InvocationCorrelation,
465
569
  outcome?: { kind: string; code: string; message: string; provenance?: string },
466
570
  arg4?: (() => boolean) | { code: string; message: string },
571
+ // Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
572
+ // when an explicit outcome is absent, a content-bearing, activity-bearing,
573
+ // or explicitly stopped finalization is still a successful terminal; only a
574
+ // genuinely empty, no-activity completion fails closed with the sanitized
575
+ // empty-prompt failure. Mirrors the noteTransition evidence predicate.
467
576
  arg5?: unknown,
577
+ arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
468
578
  ): Promise<void>;
469
579
  markUncertain(
470
580
  kind: InvocationKind,
@@ -699,7 +809,14 @@ export function createInvocationReconciliation(
699
809
  // so a real lifecycle event is never swallowed.
700
810
  }
701
811
  const current = records.get(recordKey);
702
- if (frame.type === "agent_end" && current === pending.finalizedRecord) {
812
+ if (
813
+ frame.type === "agent_end" &&
814
+ current === pending.finalizedRecord &&
815
+ (kind !== "prompt" ||
816
+ frame.outcome?.kind === "stopped" ||
817
+ frame.hasActivity === true ||
818
+ frame.content?.text.trim())
819
+ ) {
703
820
  // A real lifecycle end that arrived during deadline finalization is
704
821
  // stronger evidence than the synthetic deadline outcome. Upgrade the
705
822
  // durable terminal instead of treating the event as a duplicate.
@@ -734,7 +851,14 @@ export function createInvocationReconciliation(
734
851
  }
735
852
  }
736
853
  if (record.terminalAt !== undefined) {
737
- if (frame.type === "agent_end" && record.error?.code === "prompt_deadline_exceeded") {
854
+ if (
855
+ frame.type === "agent_end" &&
856
+ record.error?.code === "prompt_deadline_exceeded" &&
857
+ (kind !== "prompt" ||
858
+ frame.outcome?.kind === "stopped" ||
859
+ frame.hasActivity === true ||
860
+ frame.content?.text.trim())
861
+ ) {
738
862
  const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
739
863
  // The synthetic deadline error is superseded evidence; terminal_ok must not
740
864
  // surface a deadline failure for a prompt that actually completed.
@@ -795,7 +919,17 @@ export function createInvocationReconciliation(
795
919
  if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
796
920
  next.error = failure;
797
921
  } else {
798
- next.status = next.error === undefined ? "terminal_ok" : "failed";
922
+ if (
923
+ kind === "prompt" &&
924
+ next.error === undefined &&
925
+ frame.type === "agent_end" &&
926
+ frame.outcome?.kind !== "stopped" &&
927
+ !frame.content?.text.trim() &&
928
+ !frame.hasActivity
929
+ ) {
930
+ next.status = "failed";
931
+ next.error = EMPTY_PROMPT_FAILURE;
932
+ } else next.status = next.error === undefined ? "terminal_ok" : "failed";
799
933
  next.terminalAt = Date.now();
800
934
  }
801
935
  records.set(recordKey, next);
@@ -868,6 +1002,7 @@ export function createInvocationReconciliation(
868
1002
  outcome,
869
1003
  arg4?: (() => boolean) | { code: string; message: string },
870
1004
  arg5?: unknown,
1005
+ arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
871
1006
  ) {
872
1007
  // Normalize every supported positional form (exact-head review P1):
873
1008
  // (…, isCurrent) | (…, recordError) | (…, undefined, recordError) |
@@ -880,6 +1015,14 @@ export function createInvocationReconciliation(
880
1015
  ? (arg5 as { code: string; message: string })
881
1016
  : undefined;
882
1017
  const recordError = recordErrorCandidate;
1018
+ // Terminal evidence may also arrive positionally through arg5 (legacy
1019
+ // callers) when it does not carry a recordError shape; normalize it so
1020
+ // the empty predicate below sees every evidence channel consistently.
1021
+ const legacyEvidenceCandidate =
1022
+ typeof arg5 === "object" && arg5 !== null && !("code" in arg5)
1023
+ ? (arg5 as { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string })
1024
+ : undefined;
1025
+ const evidence = arg6 ?? legacyEvidenceCandidate;
883
1026
  const recordKey = key(kind, correlation);
884
1027
  const record = records.get(recordKey);
885
1028
  if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
@@ -896,6 +1039,19 @@ export function createInvocationReconciliation(
896
1039
  recordError !== undefined
897
1040
  ? { code: recordError.code, message: recordError.message }
898
1041
  : { code: finalOutcome.code, message: finalOutcome.message };
1042
+ } else if (kind === "prompt" && finalOutcome === undefined) {
1043
+ // Evidence-based empty predicate (exact-head review P1/P2): the same
1044
+ // semantics as the noteTransition agent_end classifier. A
1045
+ // content-bearing, activity-bearing, or explicitly stopped
1046
+ // finalization without an explicit outcome is a successful terminal;
1047
+ // only a genuinely empty, no-activity completion fails closed.
1048
+ const terminalText = evidence?.content?.text.trim() ?? "";
1049
+ if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
1050
+ finalizedRecord.status = "failed";
1051
+ finalizedRecord.error = EMPTY_PROMPT_FAILURE;
1052
+ } else {
1053
+ finalizedRecord.status = "terminal_ok";
1054
+ }
899
1055
  } else {
900
1056
  finalizedRecord.status = "terminal_ok";
901
1057
  }
@@ -1041,37 +1197,63 @@ function createQuerySurface(
1041
1197
  kind: ctx.sessionMetadata?.kind ?? "main",
1042
1198
  });
1043
1199
  const lastAssistant = () => {
1044
- for (const entry of ctx.sessionManager.getBranch().toReversed()) {
1045
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
1046
- const content = entry.message.content;
1047
- if (typeof content === "string") return content;
1048
- if (Array.isArray(content))
1049
- return content
1050
- .filter(
1051
- (block): block is { type: "text"; text: string } =>
1052
- block.type === "text" && typeof block.text === "string",
1053
- )
1054
- .map(block => block.text)
1055
- .join("");
1200
+ const transcript =
1201
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
1202
+ for (const entry of transcript.toReversed()) {
1203
+ if (entry.role !== "assistant") continue;
1204
+ const text =
1205
+ typeof entry.body === "string"
1206
+ ? entry.body
1207
+ : entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
1208
+ if (text !== undefined && text.trim().length > 0) return text;
1056
1209
  }
1057
1210
  return undefined;
1058
1211
  };
1059
1212
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
1213
+ const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
1214
+ const getProfileAvailableModels = (): Model<Api>[] => {
1215
+ const getAvailableForProfileActivation = ctx.modelRegistry.getAvailableForProfileActivation;
1216
+ return typeof getAvailableForProfileActivation === "function"
1217
+ ? getAvailableForProfileActivation.call(ctx.modelRegistry)
1218
+ : ctx.modelRegistry.getAvailable();
1219
+ };
1060
1220
  const resolveProfileAvailability = async (
1061
1221
  profile: ModelProfileDefinition,
1062
1222
  authenticatedProviders: ReadonlySet<string>,
1063
1223
  ): Promise<{ available: boolean; defaultModel?: Model<Api> }> => {
1224
+ if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid")
1225
+ return { available: false };
1226
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1227
+ const proxyAuthenticated = proxyProvider !== undefined && authenticatedProviders.has(proxyProvider);
1228
+ const profileAuthenticated = new Set(authenticatedProviders);
1229
+ if (proxyAuthenticated) {
1230
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1231
+ }
1064
1232
  const rewriteSelectorProvider = (selector: string): string => {
1065
1233
  const slash = selector.indexOf("/");
1066
1234
  if (slash < 0) return selector;
1067
1235
  const provider = selector.slice(0, slash);
1068
- if (authenticatedProviders.has(provider)) return selector;
1236
+ if (profileAuthenticated.has(provider)) return selector;
1069
1237
  const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
1070
1238
  if (!group) return selector;
1071
- const replacement = group.find(candidate => authenticatedProviders.has(candidate));
1239
+ const replacement = group.find(candidate => profileAuthenticated.has(candidate));
1072
1240
  return replacement ? replacement + selector.slice(slash) : selector;
1073
1241
  };
1074
1242
  try {
1243
+ const proxyMode = profile.source === "user" ? "fallback" : resolveProxyMode(profileSettings);
1244
+ if (profile.source !== "user") {
1245
+ const configuredProviders = ctx.modelRegistry.getConfiguredProviderIds?.() ?? [];
1246
+ if (proxyProvider !== undefined && !configuredProviders.includes(proxyProvider))
1247
+ return { available: false };
1248
+ }
1249
+ if (profile.source !== "user" && proxyMode === "always") {
1250
+ if (
1251
+ proxyProvider === undefined ||
1252
+ !proxyAuthenticated ||
1253
+ !(ctx.modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
1254
+ )
1255
+ return { available: false };
1256
+ }
1075
1257
  const bindings = resolveProfileBindings(profile);
1076
1258
  const assignments: Array<{ value: ModelSelectorValue; isDefault: boolean }> = [];
1077
1259
  if (bindings.defaultSelector !== undefined) {
@@ -1079,23 +1261,44 @@ function createQuerySurface(
1079
1261
  }
1080
1262
  for (const value of Object.values(bindings.modelRoles)) assignments.push({ value, isDefault: false });
1081
1263
  for (const value of Object.values(bindings.agentModelOverrides)) assignments.push({ value, isDefault: false });
1264
+ const availableModels = getProfileAvailableModels();
1265
+ const resolutionRegistry = {
1266
+ ...ctx.modelRegistry,
1267
+ getAvailable: () => availableModels,
1268
+ getApiKey: (model: Model<Api>, sessionId?: string) =>
1269
+ ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
1270
+ resolveCanonicalModel: ctx.modelRegistry.resolveCanonicalModel?.bind(ctx.modelRegistry),
1271
+ getCanonicalVariants: ctx.modelRegistry.getCanonicalVariants?.bind(ctx.modelRegistry),
1272
+ getCanonicalId: ctx.modelRegistry.getCanonicalId?.bind(ctx.modelRegistry),
1273
+ resolveModelByLookupAlias: ctx.modelRegistry.resolveModelByLookupAlias?.bind(ctx.modelRegistry),
1274
+ lookupAliasExists: ctx.modelRegistry.lookupAliasExists?.bind(ctx.modelRegistry),
1275
+ clearCanonicalVariant: ctx.modelRegistry.clearCanonicalVariant?.bind(ctx.modelRegistry),
1276
+ };
1082
1277
  let defaultModel: Model<Api> | undefined;
1083
1278
  for (const assignment of assignments) {
1084
- const selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
1279
+ let selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
1280
+ if (proxyProvider !== undefined && proxyAuthenticated && profile.source !== "user") {
1281
+ selectors = selectors.map(selector =>
1282
+ rewriteSelectorForProxy(
1283
+ selector,
1284
+ proxyProvider,
1285
+ proxyMode,
1286
+ availableModels,
1287
+ new Set(authenticatedProviders),
1288
+ getProxyRoutableProviders(profile),
1289
+ ),
1290
+ );
1291
+ }
1085
1292
  const hasBareSelector = selectors.some(selector => {
1086
1293
  const suffix = splitSelectorThinkingSuffix(selector);
1087
1294
  const identity = suffix.thinkingLevel ? suffix.selector : selector;
1088
1295
  return !identity.includes("/");
1089
1296
  });
1090
- if (!assignment.isDefault && !hasBareSelector) continue;
1297
+ if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
1298
+ continue;
1091
1299
  const resolution = await resolveModelChainWithAuth(
1092
1300
  selectors,
1093
- {
1094
- ...ctx.modelRegistry,
1095
- getAvailable: () => ctx.modelRegistry.getAvailable(),
1096
- getApiKey: (model: Model<Api>, sessionId?: string) =>
1097
- ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
1098
- },
1301
+ resolutionRegistry,
1099
1302
  options.settings,
1100
1303
  getProfileCredentialSessionId(),
1101
1304
  {
@@ -1113,6 +1316,45 @@ function createQuerySurface(
1113
1316
  return { available: false };
1114
1317
  }
1115
1318
  };
1319
+ const collectProfileAuthentication = async (
1320
+ profiles: ReadonlyMap<string, ModelProfileDefinition>,
1321
+ ): Promise<Set<string>> => {
1322
+ const providers = new Set<string>();
1323
+ for (const profile of profiles.values()) {
1324
+ for (const provider of profile.requiredProviders) providers.add(provider);
1325
+ for (const group of profile.alternativeProviderGroups ?? []) {
1326
+ for (const provider of group) providers.add(provider);
1327
+ }
1328
+ for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
1329
+ }
1330
+ const authenticated = new Set<string>();
1331
+ await Promise.all(
1332
+ [...providers].map(async provider => {
1333
+ try {
1334
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId());
1335
+ if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(provider);
1336
+ } catch {
1337
+ // A provider whose credential state cannot be read is not currently configurable.
1338
+ }
1339
+ }),
1340
+ );
1341
+ const proxyProviders = new Set<string>();
1342
+ for (const profile of profiles.values()) {
1343
+ if (profile.source === "user") continue;
1344
+ const proxyProvider = tryResolveProxyProviderId(profileSettings);
1345
+ if (proxyProvider !== undefined) proxyProviders.add(proxyProvider);
1346
+ }
1347
+ for (const proxyProvider of proxyProviders) {
1348
+ try {
1349
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(proxyProvider, getProfileCredentialSessionId());
1350
+ if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(proxyProvider);
1351
+ } catch {
1352
+ // Passive availability must degrade to unavailable when proxy credential
1353
+ // refresh/storage fails; explicit activation retains its diagnostics.
1354
+ }
1355
+ }
1356
+ return authenticated;
1357
+ };
1116
1358
  const getDiff = async () => {
1117
1359
  try {
1118
1360
  const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff"], {
@@ -1245,9 +1487,7 @@ function createQuerySurface(
1245
1487
  if (collision) return degraded();
1246
1488
  let authenticatedProviders: ReadonlySet<string>;
1247
1489
  try {
1248
- authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
1249
- ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
1250
- );
1490
+ authenticatedProviders = await collectProfileAuthentication(profiles);
1251
1491
  } catch {
1252
1492
  // Availability join failed: degrade only the synthetic facade,
1253
1493
  // retain concrete rows and the active marker readback.
@@ -1265,7 +1505,12 @@ function createQuerySurface(
1265
1505
  );
1266
1506
  const availableProfileIds = new Set<string>();
1267
1507
  for (const [name, profile] of profiles) {
1268
- if (!isModelProfileProviderAvailable(profile, authenticatedProviders)) continue;
1508
+ if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid") continue;
1509
+ const profileAuthenticated = new Set(authenticatedProviders);
1510
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1511
+ if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
1512
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1513
+ if (!isModelProfileProviderAvailable(profile, profileAuthenticated)) continue;
1269
1514
  if (!fullyResolvedProfiles.has(name)) continue;
1270
1515
  // A profile with a default mapping is selectable only when its
1271
1516
  // default chain actually resolves to an authenticated model:
@@ -1336,18 +1581,19 @@ function createQuerySurface(
1336
1581
  (options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
1337
1582
  getModelProfiles: async () => {
1338
1583
  const profiles = ctx.modelRegistry.getModelProfiles();
1339
- const authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
1340
- ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
1341
- );
1584
+ const authenticatedProviders = await collectProfileAuthentication(profiles);
1342
1585
  return (await Promise.all(
1343
- projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => ({
1344
- ...item,
1345
- available:
1346
- isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders) &&
1347
- (
1348
- await resolveProfileAvailability(profiles.get(item.id)!, authenticatedProviders)
1349
- ).available,
1350
- })),
1586
+ projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => {
1587
+ const profile = profiles.get(item.id)!;
1588
+ const profileAuthenticated = new Set(authenticatedProviders);
1589
+ const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
1590
+ if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
1591
+ for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
1592
+ const available =
1593
+ isModelProfileProviderAvailable(profile, profileAuthenticated) &&
1594
+ (await resolveProfileAvailability(profile, authenticatedProviders)).available;
1595
+ return { ...item, available };
1596
+ }),
1351
1597
  )) as unknown[];
1352
1598
  },
1353
1599
  installedQueries: policy.installedQueries,
@@ -1600,6 +1846,55 @@ function providerFailureFromAgentEnd(event: unknown): { code: string; message: s
1600
1846
  }
1601
1847
  }
1602
1848
 
1849
+ interface PromptTerminalEvidence {
1850
+ content?: TurnResultContent;
1851
+ hasActivity: boolean;
1852
+ }
1853
+
1854
+ function promptTerminalEvidenceFromAgentEnd(event: unknown): PromptTerminalEvidence {
1855
+ try {
1856
+ if (!event || typeof event !== "object") return { hasActivity: false };
1857
+ const messages = (event as { messages?: unknown }).messages;
1858
+ if (!Array.isArray(messages)) return { hasActivity: false };
1859
+ const assistant = [...messages]
1860
+ .reverse()
1861
+ .find(
1862
+ message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
1863
+ );
1864
+ if (!assistant || typeof assistant !== "object") return { hasActivity: false };
1865
+ const content = (assistant as { content?: unknown }).content;
1866
+ if (typeof content === "string") {
1867
+ const bounded = sanitizeTurnResultContent(content);
1868
+ return { content: bounded, hasActivity: content.trim().length > 0 };
1869
+ }
1870
+ if (Array.isArray(content)) {
1871
+ const hasActivity = content.some(block => {
1872
+ if (block === null || typeof block !== "object") return false;
1873
+ const type = (block as { type?: unknown }).type;
1874
+ if (type === "text") {
1875
+ const text = (block as { text?: unknown }).text;
1876
+ return typeof text === "string" && text.trim().length > 0;
1877
+ }
1878
+ return typeof type === "string" && type.length > 0;
1879
+ });
1880
+ const text = content
1881
+ .filter(
1882
+ (block): block is { type: "text"; text: string } =>
1883
+ block !== null &&
1884
+ typeof block === "object" &&
1885
+ (block as { type?: unknown }).type === "text" &&
1886
+ typeof (block as { text?: unknown }).text === "string",
1887
+ )
1888
+ .map(block => block.text)
1889
+ .join("");
1890
+ return { content: sanitizeTurnResultContent(text), hasActivity };
1891
+ }
1892
+ return { content: sanitizeTurnResultContent(""), hasActivity: false };
1893
+ } catch {
1894
+ return { hasActivity: false };
1895
+ }
1896
+ }
1897
+
1603
1898
  function createControlSurface(
1604
1899
  ctx: ExtensionContext,
1605
1900
  api: ExtensionAPI,
@@ -1611,6 +1906,7 @@ function createControlSurface(
1611
1906
  startsOwnTurn: boolean,
1612
1907
  sdkRunToken: string,
1613
1908
  ) => void,
1909
+ armPromptDeadline: (correlation: InvocationCorrelation) => void,
1614
1910
  steerReconciliation: KindAwareReconciliation,
1615
1911
  onPromotedTurn?: (
1616
1912
  kind: InvocationKind,
@@ -1638,6 +1934,36 @@ function createControlSurface(
1638
1934
  canResolveGate: () => boolean = () => true,
1639
1935
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
1640
1936
  ): ControlSurface {
1937
+ const normalizePromptImages = (value: unknown): ImageContent[] => {
1938
+ if (!Array.isArray(value)) return [];
1939
+ const normalized: ImageContent[] = [];
1940
+ for (const candidate of value) {
1941
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
1942
+ const image = candidate as Record<string, unknown>;
1943
+ if (typeof image.data !== "string" || image.data.trim().length === 0) continue;
1944
+ if (image.mimeType !== undefined && typeof image.mimeType !== "string") continue;
1945
+ normalized.push({
1946
+ type: "image",
1947
+ data: image.data,
1948
+ mimeType: typeof image.mimeType === "string" && image.mimeType.length > 0 ? image.mimeType : "image/png",
1949
+ });
1950
+ }
1951
+ return normalized;
1952
+ };
1953
+ type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
1954
+ sdkRunCapability?: SdkRunCapability;
1955
+ };
1956
+ type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
1957
+ sendUserMessage: (
1958
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
1959
+ options?: InternalSendOptions,
1960
+ ) => Promise<void>;
1961
+ };
1962
+ const internalApi = api as InternalSdkApi;
1963
+ const sendSdkUserMessage = (
1964
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
1965
+ options?: Record<string, unknown>,
1966
+ ): Promise<void> => internalApi.sendUserMessage(content, options);
1641
1967
  const surfacePolicy =
1642
1968
  policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
1643
1969
  const typed = (operation: string, input: Record<string, unknown> = {}) =>
@@ -1722,7 +2048,7 @@ function createControlSurface(
1722
2048
  kind: InvocationKind,
1723
2049
  clientRef: string | undefined,
1724
2050
  run: (options: {
1725
- sdkRunToken: string;
2051
+ sdkRunCapability: SdkRunCapability;
1726
2052
  onPreflightAccepted: () => void;
1727
2053
  onPreflightAcceptCommit: () => Promise<void>;
1728
2054
  /** Internal disposition before a queued submission is actually consumed. */
@@ -1743,6 +2069,7 @@ function createControlSurface(
1743
2069
  reconciliation.admit(kind, retainedClientRef);
1744
2070
  const correlation = newCorrelation();
1745
2071
  const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
2072
+ const sdkRunCapability = createSdkRunCapability(sdkRunToken);
1746
2073
  const preflight = Promise.withResolvers<void>();
1747
2074
  let accepted = false;
1748
2075
  let settled = false;
@@ -1766,6 +2093,7 @@ function createControlSurface(
1766
2093
  await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
1767
2094
  accepted = true;
1768
2095
  settled = true;
2096
+ if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
1769
2097
  // The accepted submission does NOT own the active turn until its run
1770
2098
  // actually STARTS: the connection is carried on the pending entry and
1771
2099
  // associated at agent_start instead (review thread P1).
@@ -1801,7 +2129,7 @@ function createControlSurface(
1801
2129
  const submission = Promise.resolve(
1802
2130
  run({
1803
2131
  onPreflightAccepted: () => void accept().catch(() => undefined),
1804
- sdkRunToken,
2132
+ sdkRunCapability,
1805
2133
  onPreflightAcceptCommit: accept,
1806
2134
  onDispatchDisposition: promotion => {
1807
2135
  promotionStartsOwnRun = promotion.startsOwnRun;
@@ -2018,7 +2346,7 @@ function createControlSurface(
2018
2346
  }
2019
2347
  };
2020
2348
  const terminalAbort = async (
2021
- input: { mode: "terminal"; scope?: "turn" | "owned" },
2349
+ input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
2022
2350
  idempotencyKey?: string,
2023
2351
  ): Promise<unknown> => {
2024
2352
  const scope = input.scope === "owned" ? "owned" : "turn";
@@ -2049,10 +2377,12 @@ function createControlSurface(
2049
2377
  typeof idempotencyKey === "string"
2050
2378
  ? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
2051
2379
  : undefined;
2052
- const inputHash = crypto
2053
- .createHash("sha256")
2054
- .update(JSON.stringify({ mode: "terminal", scope }))
2055
- .digest("hex");
2380
+ const abortIdentity = terminalAbortIdentity(
2381
+ { mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
2382
+ input.operator === true,
2383
+ );
2384
+ if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
2385
+ const inputHash = abortIdentity.inputHash;
2056
2386
  const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
2057
2387
  responseState: record.responseState ?? "pending",
2058
2388
  responsePayloadHash: record.responsePayloadHash ?? inputHash,
@@ -2425,6 +2755,16 @@ function createControlSurface(
2425
2755
  pendingPreflights.delete(requesterBucketKey);
2426
2756
  }
2427
2757
  };
2758
+ const returnNoActiveTurn = async () => {
2759
+ const result = {
2760
+ ok: true,
2761
+ selection: scope,
2762
+ turn: "no_active_turn",
2763
+ terminal: "terminal_no_effect",
2764
+ };
2765
+ await finalizeNoEffectReservation(result);
2766
+ return result;
2767
+ };
2428
2768
  if (!handle || epoch === undefined) {
2429
2769
  if ((await writeNoEffect()) === "conflict") {
2430
2770
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
@@ -2440,6 +2780,10 @@ function createControlSurface(
2440
2780
  if (replayed !== undefined) return replayed;
2441
2781
  }
2442
2782
  cancelRequesterPreflights();
2783
+ // Operator authority overrides connection ownership only for a turn
2784
+ // observed at admission. It must never adopt a successor that starts
2785
+ // while the durable no-effect reservation is being written.
2786
+ if (input.operator === true) return await returnNoActiveTurn();
2443
2787
  // A prompt for this requester may have become ACTIVE while the
2444
2788
  // reservation awaited the filesystem transaction: its submit()
2445
2789
  // cleanup already removed the preflight callback, so cancelling here
@@ -2455,14 +2799,7 @@ function createControlSurface(
2455
2799
  // No prompt won the race: finalize the reserved row so a later
2456
2800
  // same-key retry replays this deterministic no_active_turn result
2457
2801
  // (review thread P2).
2458
- const noActiveTurnResult = {
2459
- ok: true,
2460
- selection: scope,
2461
- turn: "no_active_turn",
2462
- terminal: "terminal_no_effect",
2463
- };
2464
- await finalizeNoEffectReservation(noActiveTurnResult);
2465
- return noActiveTurnResult;
2802
+ return await returnNoActiveTurn();
2466
2803
  }
2467
2804
  handle = recheckedHandle;
2468
2805
  epoch = recheckedEpoch;
@@ -2492,7 +2829,11 @@ function createControlSurface(
2492
2829
  // cleared after a terminal lifecycle boundary) authorizes no client, and
2493
2830
  // a stale prior owner authorizes only that old client — never a later
2494
2831
  // turn it did not submit (review thread P1).
2495
- if (handle && (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))) {
2832
+ if (
2833
+ input.operator !== true &&
2834
+ handle &&
2835
+ (abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
2836
+ ) {
2496
2837
  if ((await writeNoEffect()) === "conflict") {
2497
2838
  throw Object.assign(new Error("Idempotency key was reused with different input."), {
2498
2839
  code: "idempotency_conflict",
@@ -2881,52 +3222,77 @@ function createControlSurface(
2881
3222
  }
2882
3223
  };
2883
3224
  return {
2884
- prompt: async (text, images, clientRef) =>
2885
- submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunToken, ...options }) =>
2886
- api.sendUserMessage(
2887
- typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
3225
+ prompt: async (text, images, clientRef) => {
3226
+ const invalid = validateRequiredPromptText("turn.prompt", { text, images });
3227
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
3228
+ return await submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunCapability, ...options }) =>
3229
+ sendSdkUserMessage(
3230
+ typeof images === "undefined"
3231
+ ? text
3232
+ : ([{ type: "text", text }, ...normalizePromptImages(images)] as [
3233
+ { type: "text"; text: string },
3234
+ ...ImageContent[],
3235
+ ]),
2888
3236
  {
2889
3237
  ...options,
2890
- sdkRunToken,
3238
+ sdkRunCapability,
3239
+ // ACP terminal settlement is owned by the correlated agent_end
3240
+ // publication. Post-prompt recovery may include independent
3241
+ // subagent work and must not hold that client-facing boundary.
2891
3242
  ...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
2892
3243
  },
2893
3244
  ),
2894
- ),
3245
+ );
3246
+ },
2895
3247
  steer: async (text, clientRef) => {
3248
+ const invalid = validateRequiredPromptText("turn.steer", { text });
3249
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
2896
3250
  const retainedClientRef = normalizeClientRef(clientRef);
2897
3251
  if (retainedClientRef === undefined) {
2898
3252
  const correlation = newCorrelation();
2899
- await api.sendUserMessage(text, { deliverAs: "steer" });
3253
+ await sendSdkUserMessage(text, { deliverAs: "steer" });
2900
3254
  return { accepted: true, ...correlation };
2901
3255
  }
2902
3256
  const durable = steerReconciliation;
2903
3257
  const reservation = await durable.reserveSteer(retainedClientRef, text);
2904
3258
  if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
2905
3259
  try {
2906
- await api.sendUserMessage(text, { deliverAs: "steer" });
3260
+ await sendSdkUserMessage(text, { deliverAs: "steer" });
2907
3261
  return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
2908
3262
  } catch (error) {
2909
3263
  return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
2910
3264
  }
2911
3265
  },
2912
- followUp: async text =>
2913
- submit(
3266
+ followUp: async text => {
3267
+ const invalid = validateRequiredPromptText("turn.follow_up", { text });
3268
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
3269
+ return await submit(
2914
3270
  "prompt",
2915
3271
  undefined,
2916
- options => api.sendUserMessage(text, { ...options, deliverAs: "followUp" }),
3272
+ ({ sdkRunCapability, ...options }) =>
3273
+ sendSdkUserMessage(text, {
3274
+ ...options,
3275
+ sdkRunCapability,
3276
+ deliverAs: "followUp",
3277
+ }),
2917
3278
  undefined,
2918
3279
  false,
2919
3280
  // Follow-ups never start inline; ownership correlates at promotion.
2920
3281
  true,
2921
- ),
3282
+ );
3283
+ },
2922
3284
  abort: async () => {
2923
3285
  await Promise.resolve(ctx.abort()).catch(() => undefined);
2924
3286
  return { aborted: true };
2925
3287
  },
2926
3288
  abortTerminal: terminalAbort,
2927
3289
  abortAndPrompt: async text => {
3290
+ const invalid = validateRequiredPromptText("turn.abort_and_prompt", { text });
3291
+ if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
2928
3292
  await ctx.abort();
2929
- return await submit("prompt", undefined, options => api.sendUserMessage(text, options));
3293
+ return await submit("prompt", undefined, ({ sdkRunCapability, ...options }) =>
3294
+ sendSdkUserMessage(text, { ...options, sdkRunCapability }),
3295
+ );
2930
3296
  },
2931
3297
  answerAsk: unavailable("ask.answer"),
2932
3298
  answerGate: async (id, response, expectedSessionId, idempotencyKey) =>
@@ -2953,9 +3319,10 @@ function createControlSurface(
2953
3319
  return await submit(
2954
3320
  "skill",
2955
3321
  clientRef,
2956
- options =>
3322
+ ({ sdkRunCapability, ...options }) =>
2957
3323
  ctx.invokeSkill!(name, args, {
2958
3324
  ...options,
3325
+ sdkRunCapability,
2959
3326
  onSkillPrepared: meta => {
2960
3327
  prepared = meta;
2961
3328
  },
@@ -3135,6 +3502,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
3135
3502
  });
3136
3503
  }
3137
3504
 
3505
+ export function masterAttestationForEffectiveHost(input: {
3506
+ masterCapability: string | undefined;
3507
+ attestationEpoch: string | undefined;
3508
+ ownerSessionId?: string;
3509
+ sessionId: string;
3510
+ pid: number;
3511
+ processIncarnation: string | undefined;
3512
+ direct: MasterRoleAttestationV2 | undefined;
3513
+ }): MasterRoleAttestationV2 | undefined {
3514
+ const direct = input.direct;
3515
+ if (
3516
+ input.masterCapability === undefined ||
3517
+ input.attestationEpoch === undefined ||
3518
+ direct === undefined ||
3519
+ direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
3520
+ direct.launchPid !== input.pid ||
3521
+ direct.launchProcessIncarnation !== input.processIncarnation ||
3522
+ direct.attestationEpoch !== input.attestationEpoch
3523
+ )
3524
+ return undefined;
3525
+ return direct;
3526
+ }
3527
+
3528
+ function masterDirectAttestation(input: {
3529
+ masterCapability: string | undefined;
3530
+ attestationEpoch: string | undefined;
3531
+ ownerSessionId?: string;
3532
+ sessionId: string;
3533
+ pid: number;
3534
+ processIncarnation: string | undefined;
3535
+ }): MasterRoleAttestationV2 | undefined {
3536
+ if (
3537
+ input.masterCapability === undefined ||
3538
+ input.attestationEpoch === undefined ||
3539
+ input.processIncarnation === undefined
3540
+ )
3541
+ return undefined;
3542
+ return {
3543
+ version: 2,
3544
+ ownerSessionId: input.ownerSessionId ?? input.sessionId,
3545
+ launchPid: input.pid,
3546
+ launchProcessIncarnation: input.processIncarnation,
3547
+ role: "master",
3548
+ attestationEpoch: input.attestationEpoch,
3549
+ };
3550
+ }
3551
+
3552
+ function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
3553
+ return (
3554
+ left.version === right.version &&
3555
+ left.ownerSessionId === right.ownerSessionId &&
3556
+ left.launchPid === right.launchPid &&
3557
+ left.launchProcessIncarnation === right.launchProcessIncarnation &&
3558
+ left.role === right.role &&
3559
+ left.attestationEpoch === right.attestationEpoch
3560
+ );
3561
+ }
3562
+
3563
+ /**
3564
+ * Establish direct master authority for the session currently hosted by this
3565
+ * process. Session identity transitions retain the master capability and epoch
3566
+ * but require a new direct attestation before their effective endpoint can
3567
+ * adopt that authority.
3568
+ */
3569
+ export async function reattestMasterSessionIdentity(input: {
3570
+ index: SessionIndex;
3571
+ locator: SessionLocatorV2;
3572
+ masterCapability: string | undefined;
3573
+ attestationEpoch: string | undefined;
3574
+ ownerSessionId?: string;
3575
+ sessionId: string;
3576
+ pid: number;
3577
+ processIncarnation: string | undefined;
3578
+ }): Promise<MasterRoleAttestationV2 | undefined> {
3579
+ const masterRole = masterDirectAttestation(input);
3580
+ if (!masterRole) return undefined;
3581
+ const directExists = input.index.listSessionIdentities().some(row => {
3582
+ const processIdentity = row.hostIncarnation ?? row.processIncarnation;
3583
+ return (
3584
+ row.sessionId === input.sessionId &&
3585
+ row.endpointGeneration === 0 &&
3586
+ row.pid === input.pid &&
3587
+ processIdentity === input.processIncarnation &&
3588
+ row.masterRole !== undefined &&
3589
+ sameMasterAttestation(row.masterRole, masterRole)
3590
+ );
3591
+ });
3592
+ if (!directExists) {
3593
+ await input.index.append({
3594
+ type: "host_registered",
3595
+ sessionId: input.sessionId,
3596
+ locator: input.locator,
3597
+ endpointGeneration: 0,
3598
+ pid: input.pid,
3599
+ processIncarnation: input.processIncarnation,
3600
+ masterRole,
3601
+ });
3602
+ }
3603
+ return masterRole;
3604
+ }
3605
+
3138
3606
  function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
3139
3607
  const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
3140
3608
  const id = typeof frame.id === "string" ? frame.id : "";
@@ -3374,6 +3842,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3374
3842
  failureCause?: unknown,
3375
3843
  maintenanceOutcome?: string,
3376
3844
  lifecycleOwner?: LifecycleOwner,
3845
+ terminalContent?: TurnResultContent,
3846
+ terminalHasActivity = false,
3847
+ terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
3377
3848
  ): Promise<void> => {
3378
3849
  const current = lifecycleOwner?.state ?? active;
3379
3850
  if (!current) return;
@@ -3461,6 +3932,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3461
3932
  if (current.activeInvocation?.kind === "prompt") {
3462
3933
  current.deadlineManager.onAccepted(current.activeInvocation.correlation);
3463
3934
  }
3935
+ } else {
3936
+ // An empty drain is an agent-initiated successor run. Clear the
3937
+ // predecessor's SDK owner and active invocation so abort ownership and
3938
+ // tool-progress renewal cannot leak into the successor.
3939
+ adoptLifecycleBatch(undefined);
3464
3940
  }
3465
3941
  transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
3466
3942
  } else {
@@ -3626,7 +4102,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3626
4102
  const reasonKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
3627
4103
  const unrecorded = type === "agent_end" ? current.unrecordedFailureReasons?.get(reasonKey) : undefined;
3628
4104
  if (type === "agent_end" && invocation.kind === "prompt")
3629
- current.deadlineManager.noteTerminalTransition(invocation.correlation, unrecorded);
4105
+ current.deadlineManager.noteTerminalTransition(
4106
+ invocation.correlation,
4107
+ unrecorded,
4108
+ terminalContent !== undefined || terminalHasActivity || terminalOutcome !== undefined
4109
+ ? ({
4110
+ content: terminalContent,
4111
+ hasActivity: terminalHasActivity,
4112
+ outcome: terminalOutcome,
4113
+ } satisfies PromptTerminalTransitionEvidence)
4114
+ : undefined,
4115
+ );
3630
4116
  if (type === "agent_end") {
3631
4117
  // Compound recovery (exact-head review P1): if this run's
3632
4118
  // agent_failed write failed, re-record the sanitized reason
@@ -3650,7 +4136,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3650
4136
  error:
3651
4137
  failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
3652
4138
  }
3653
- : { type };
4139
+ : {
4140
+ type,
4141
+ ...(type === "agent_end" && terminalContent !== undefined
4142
+ ? { content: terminalContent }
4143
+ : {}),
4144
+ ...(type === "agent_end" && terminalHasActivity ? { hasActivity: true } : {}),
4145
+ ...(type === "agent_end" && terminalOutcome !== undefined
4146
+ ? { outcome: terminalOutcome }
4147
+ : {}),
4148
+ };
3654
4149
  await current.reconciliation.noteTransition(invocation.kind, invocation.correlation, frame as never);
3655
4150
  if ((type as string) === "agent_end") {
3656
4151
  if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
@@ -3814,6 +4309,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3814
4309
  owner?.failureDiagnosticKeys.has(lifecycleCorrelationKey(correlation)),
3815
4310
  );
3816
4311
  const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
4312
+ const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
4313
+ const terminalOutcome =
4314
+ event.stopReason === "cancelled"
4315
+ ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4316
+ : undefined;
3817
4317
  return trackLifecycle(async () => {
3818
4318
  if (failure && !failureAlreadyPublished) {
3819
4319
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -3825,6 +4325,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3825
4325
  undefined,
3826
4326
  event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
3827
4327
  lifecycleOwner,
4328
+ terminalEvidence.content,
4329
+ terminalEvidence.hasActivity,
4330
+ terminalOutcome,
3828
4331
  );
3829
4332
  }, owner).finally(() => {
3830
4333
  if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
@@ -3860,6 +4363,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3860
4363
  await current.registerBroker();
3861
4364
  current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
3862
4365
  });
4366
+ const consumedMasterNonces = new Map<string, number>();
3863
4367
  api.on("turn_end", (_event, ctx) =>
3864
4368
  lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
3865
4369
  type: "turn_end",
@@ -4159,8 +4663,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4159
4663
  // promotion/agent_start instead, so a prompt waiting behind a
4160
4664
  // legitimately long turn never false-fires. The agent_start
4161
4665
  // re-entry in emitLifecycle is a no-op for an existing lease.
4162
- if (startsOwnTurn && kind === "prompt") deadlineManager.onAccepted(correlation);
4163
4666
  },
4667
+ correlation => deadlineManager.onAccepted(correlation),
4164
4668
  steerReconciliation,
4165
4669
  (kind, correlation, connectionId, sdkRunToken, promotion) => {
4166
4670
  const bindPromotedToken = (batch?: LifecycleBatch): void => {
@@ -4419,6 +4923,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4419
4923
  };
4420
4924
  runtime = new SessionSdkSessionRuntime({
4421
4925
  transport,
4926
+ masterCapabilityVerify: frame =>
4927
+ verifyMasterCapabilityFrame({
4928
+ frame,
4929
+ expectedCapability: options.masterCapability,
4930
+ expectedEpoch: options.masterAttestationEpoch,
4931
+ replay: consumedMasterNonces,
4932
+ }),
4422
4933
  onFrameAdmission: (_connectionId, frame) =>
4423
4934
  inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
4424
4935
  control: async (connectionId, frame) => {
@@ -4464,21 +4975,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4464
4975
  (request.input as { mode?: unknown }).mode !== "terminal"
4465
4976
  )
4466
4977
  return;
4467
- const input = request.input as Record<string, unknown>;
4468
- const mode = input.mode;
4469
- const rawScope = input.scope;
4470
- if (mode !== "terminal") return;
4471
- if (rawScope !== undefined && rawScope !== "turn" && rawScope !== "owned") return;
4472
- // A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
4473
- // rejected by dispatch must never hash as the valid turn input and
4474
- // advance the legitimate stored row (review thread P2).
4475
- for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
4476
- const scopeInput = rawScope === "owned" ? "owned" : "turn";
4978
+ const rawInput = request.input as Record<string, unknown>;
4979
+ const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
4980
+ const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
4981
+ const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
4982
+ if (!abortIdentity) return;
4477
4983
  const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
4478
- const inputHash = crypto
4479
- .createHash("sha256")
4480
- .update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
4481
- .digest("hex");
4984
+ const inputHash = abortIdentity.inputHash;
4482
4985
  // Hash the ACTUAL written response payload: the durable state may only
4483
4986
  // advance when the written response corresponds to the row's payload.
4484
4987
  // When more than 256 concurrent requests evict an in-flight abort from
@@ -4534,21 +5037,72 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4534
5037
  const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
4535
5038
  runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
4536
5039
  );
5040
+ let publishedEndpointUrl: string | undefined;
4537
5041
  let brokerRegistered = false;
4538
5042
  const registerBroker = async (): Promise<void> => {
4539
5043
  if (brokerRegistered) return;
4540
5044
  try {
5045
+ if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
5046
+ throw new Error("Lifecycle broker registration requires a request identity.");
4541
5047
  await ensureBroker({ agentDir: options.agentDir });
4542
5048
  const index = await new SessionIndex(options.agentDir).open();
4543
- const locator = { repo: path.resolve(ctx.cwd), stateRoot };
5049
+ const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
5050
+ const effectiveIncarnation = processIncarnation(process.pid);
5051
+ const direct = await reattestMasterSessionIdentity({
5052
+ index,
5053
+ locator,
5054
+ masterCapability: options.masterCapability,
5055
+ attestationEpoch: options.masterAttestationEpoch,
5056
+ ownerSessionId: options.masterOwnerSessionId,
5057
+ sessionId,
5058
+ pid: process.pid,
5059
+ processIncarnation: effectiveIncarnation,
5060
+ });
4544
5061
  await runtime.registerWithBroker({
4545
5062
  register: async input => {
4546
- const endpointMtimeMs = (await fs.stat(path.join(input.stateRoot, "sdk", `${input.sessionId}.json`)))
4547
- .mtimeMs;
4548
- await index.append({ type: "host_registered", ...input, locator, pid: process.pid, endpointMtimeMs });
5063
+ if (publishedEndpointUrl === undefined)
5064
+ throw new Error("SDK transport endpoint was not published before broker registration.");
5065
+ const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
5066
+ const file = await readEndpointFile(endpointPath);
5067
+ if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
5068
+ const endpoint = JSON.parse(file.source) as Record<string, unknown>;
5069
+ if (
5070
+ endpoint.sessionId !== input.sessionId ||
5071
+ endpoint.pid !== process.pid ||
5072
+ endpoint.url !== publishedEndpointUrl ||
5073
+ endpoint.token !== transport.token
5074
+ )
5075
+ throw new Error("SDK endpoint did not match the published transport authority.");
5076
+ const endpointMtimeMs = file.mtimeMs;
5077
+ const endpointFileId = `${file.dev}:${file.ino}`;
5078
+ const masterRole = masterAttestationForEffectiveHost({
5079
+ masterCapability: options.masterCapability,
5080
+ attestationEpoch: options.masterAttestationEpoch,
5081
+ ownerSessionId: options.masterOwnerSessionId,
5082
+ sessionId: input.sessionId,
5083
+ pid: process.pid,
5084
+ processIncarnation: effectiveIncarnation,
5085
+ direct,
5086
+ });
5087
+ await index.append({
5088
+ type: "host_registered",
5089
+ ...input,
5090
+ locator,
5091
+ pid: process.pid,
5092
+ endpointMtimeMs,
5093
+ endpointFileId,
5094
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5095
+ ...(masterRole ? { masterRole } : {}),
5096
+ });
4549
5097
  },
4550
5098
  unregister: async input => {
4551
- await index.append({ type: "host_unregistered", ...input, locator, pid: process.pid });
5099
+ await index.append({
5100
+ type: "host_unregistered",
5101
+ ...input,
5102
+ locator,
5103
+ pid: process.pid,
5104
+ ...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
5105
+ });
4552
5106
  },
4553
5107
  });
4554
5108
  brokerRegistered = true;
@@ -4587,7 +5141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4587
5141
  lifecycleOwnerHolder.state = runtimeOwner;
4588
5142
  active = runtimeOwner;
4589
5143
  try {
4590
- await runtime.start();
5144
+ publishedEndpointUrl = (await runtime.start()).url;
4591
5145
  await registerBroker();
4592
5146
  } catch (error) {
4593
5147
  active = undefined;