@gajae-code/coding-agent 0.18.7 → 0.18.8

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 (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -27,7 +27,13 @@ import * as fs from "node:fs";
27
27
  import * as os from "node:os";
28
28
  import * as path from "node:path";
29
29
  import { promisify } from "node:util";
30
- import { isNonDispatchedToolEvent, type RunSettlementProof, ThinkingLevel } from "@gajae-code/agent-core";
30
+ import {
31
+ type AgentTerminalOwnerContext,
32
+ isNonDispatchedToolEvent,
33
+ type RunCancellationDomain,
34
+ type RunSettlementProof,
35
+ ThinkingLevel,
36
+ } from "@gajae-code/agent-core";
31
37
  import type { ImageContent, TextContent, Tool } from "@gajae-code/ai/core";
32
38
  import type { NotificationServer as NativeNotificationServer } from "@gajae-code/natives";
33
39
 
@@ -1229,6 +1235,9 @@ export class PresentationArbiter {
1229
1235
  interface SessionRuntime {
1230
1236
  server: NotificationServer;
1231
1237
  host: SessionSdkHost;
1238
+ getJoinedPromptCorrelations: (owner: AgentTerminalOwnerContext) => Array<{ commandId: string; turnId: string }>;
1239
+ isPromptRunOwner: (correlation: { commandId: string; turnId: string }, owner: AgentTerminalOwnerContext) => boolean;
1240
+ retireJoinedPromptOwners: () => void;
1232
1241
  /** Delivers one ring-positioned event envelope to every attached subscriber
1233
1242
  * connection, applying the same capability gate as event replay. */
1234
1243
  broadcastEventFrame: (event: SdkFrame) => string[];
@@ -2618,6 +2627,12 @@ function sdkControlSurface(
2618
2627
  error: unknown,
2619
2628
  ) => void | Promise<void> = () => {},
2620
2629
  onPromptAcceptFailed: (correlation: { commandId: string; turnId: string }) => void = () => {},
2630
+ onPromptDiverted: (correlation: { commandId: string; turnId: string }) => void,
2631
+ onPromptPromoted: (
2632
+ correlation: { commandId: string; turnId: string },
2633
+ promotion: { startsOwnRun?: boolean; removed?: boolean },
2634
+ handle: string | undefined,
2635
+ ) => void,
2621
2636
  acceptGateResolution: () => boolean,
2622
2637
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
2623
2638
  admitPrompt: (clientRef?: string) => void,
@@ -3019,6 +3034,7 @@ function sdkControlSurface(
3019
3034
  admission.accepted();
3020
3035
  accepting = false;
3021
3036
  accepted = true;
3037
+ if (effectiveDeliverAs !== undefined) onPromptDiverted(correlation);
3022
3038
  // #4743: an accepted run owns a durable terminal publication when it
3023
3039
  // settles; teardown joins it via this latch (never-accepted preflights
3024
3040
  // produce no durable write and stay unjoined by design).
@@ -3044,7 +3060,14 @@ function sdkControlSurface(
3044
3060
  ...(effectiveDeliverAs ? { deliverAs: effectiveDeliverAs } : {}),
3045
3061
  onPreflightAcceptCommit,
3046
3062
  onPreflightAccepted,
3047
- ...admission.hooks,
3063
+ onQueuedPromoted: promotion => {
3064
+ admission.hooks.onQueuedPromoted(promotion);
3065
+ onPromptPromoted(correlation, promotion, ctx.getActivePromptHandle());
3066
+ },
3067
+ onDispatchDisposition: disposition => {
3068
+ admission.hooks.onDispatchDisposition(disposition);
3069
+ if (!disposition.startsOwnRun) onPromptDiverted(correlation);
3070
+ },
3048
3071
  preflightSignal: preflightController.signal,
3049
3072
  ...(sdkRunToken ? { sdkRunToken } : {}),
3050
3073
  }),
@@ -4290,6 +4313,8 @@ export function createNotificationsExtension(
4290
4313
  * preflight outside the durable admission path (review thread P2).
4291
4314
  */
4292
4315
  terminalAbortSeams?: {
4316
+ getTerminalRunOwnerForEvent?: (event: object) => AgentTerminalOwnerContext | undefined;
4317
+ getRunOwnerDomain?: (handle: string) => RunCancellationDomain | undefined;
4293
4318
  getTerminalTurnEpoch: () => number | undefined;
4294
4319
  cancelPendingPreflightForTerminalAbort: () => void;
4295
4320
  captureTerminalAbortSteeringSnapshot?: () => void;
@@ -4577,6 +4602,7 @@ export function createNotificationsExtension(
4577
4602
  if (reason === "session" && requestedRuntime) {
4578
4603
  requestedRuntime.inboundFenced = true;
4579
4604
  requestedRuntime.stopping = true;
4605
+ requestedRuntime.retireJoinedPromptOwners();
4580
4606
  requestedRuntime.abortEphemeralTurns();
4581
4607
  }
4582
4608
  if (reason === "session" && requestedRuntime) requestedRuntime.stopSessionNameObserver();
@@ -4845,6 +4871,19 @@ export function createNotificationsExtension(
4845
4871
  return failLifecycleStartup("failed", "Lifecycle SDK startup requires an agent directory.");
4846
4872
 
4847
4873
  const pendingInteractive = new Map<string, PendingInteractiveAsk>();
4874
+ const queuedRemovalTerminals = new Map<string, Promise<boolean>>();
4875
+ const joinedPromptOwners = new Map<
4876
+ string,
4877
+ { correlation: { commandId: string; turnId: string }; owner: AgentTerminalOwnerContext }
4878
+ >();
4879
+ const getJoinedPromptCorrelations = (terminalOwner: AgentTerminalOwnerContext) =>
4880
+ [...joinedPromptOwners.values()]
4881
+ .filter(
4882
+ ({ owner }) =>
4883
+ owner.resourceRunId === terminalOwner.resourceRunId && owner.domain === terminalOwner.domain,
4884
+ )
4885
+ .map(({ correlation }) => correlation);
4886
+ const retireJoinedPromptOwners = (): void => joinedPromptOwners.clear();
4848
4887
  const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
4849
4888
  const pendingPromptCorrelationsBySdkRunToken = new Map<string, { commandId: string; turnId: string }>();
4850
4889
  const workLease = ctx.getSessionWorkLease?.() ?? createSessionWorkLease();
@@ -5106,6 +5145,8 @@ export function createNotificationsExtension(
5106
5145
  * up to a hard maximum runtime anchored to acceptance.
5107
5146
  */
5108
5147
  deadlineLease: PromptDeadlineLease;
5148
+ deadlineSuspended?: true;
5149
+ sdkRunToken?: string;
5109
5150
  deadlineTimer?: Parameters<typeof clearTimeout>[0];
5110
5151
  /**
5111
5152
  * Identity of the in-flight deadline expiry attempt (set synchronously
@@ -5314,10 +5355,12 @@ export function createNotificationsExtension(
5314
5355
  submission.workLease = undefined;
5315
5356
  }
5316
5357
  promptSubmissions.delete(key);
5358
+ const [commandId, turnId] = key.split(":", 2);
5317
5359
  // A fatal closure is transport-level, not a committed semantic terminal: the
5318
- // durable record stays authoritative, so it must never leave a tombstone.
5360
+ // durable record stays authoritative, so it must never leave a tombstone
5361
+ // or release images still retained by an unsettled run. Runtime teardown
5362
+ // releases those reservations even if this delivery record expires.
5319
5363
  if (submission.fatal) return;
5320
- const [commandId, turnId] = key.split(":", 2);
5321
5364
  if (!commandId || !turnId) return;
5322
5365
  removePendingPromptCorrelation({ commandId, turnId });
5323
5366
  addTerminalTombstone(key, submission.connectionId);
@@ -5664,7 +5707,7 @@ export function createNotificationsExtension(
5664
5707
  */
5665
5708
  const armPromptDeadline = (key: string, correlation: { commandId: string; turnId: string }) => {
5666
5709
  const submission = promptSubmissions.get(key);
5667
- if (!submission) return;
5710
+ if (!submission || submission.deadlineSuspended) return;
5668
5711
  const lease = submission.deadlineLease;
5669
5712
  if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
5670
5713
  submission.deadlineTimer = setTimeout(
@@ -5772,25 +5815,37 @@ export function createNotificationsExtension(
5772
5815
  );
5773
5816
  };
5774
5817
  const emitPromptEvent = (event: AgentSessionEvent) => {
5775
- if (!runtime?.activePromptCorrelation) return;
5818
+ if (!runtime) return;
5776
5819
  cleanupPromptRecords();
5777
- const correlation = runtime.activePromptCorrelation;
5778
- // Renewal is attributed exactly like the correlated delivery below.
5779
- renewPromptDeadline(correlation, event);
5780
- noteToolDispatchBoundary(correlation, event);
5781
- const submission = promptSubmissions.get(promptSubmissionKey(correlation));
5782
- if (!submission || submission.abandoned) return;
5783
- const frame = {
5784
- type: "event",
5785
- kind: event.type,
5786
- payload: toAgentWireEventPayload(event),
5787
- ...correlation,
5788
- };
5789
- if (!submission.acknowledged) {
5790
- submission.bufferedFrames.push(frame);
5791
- return;
5820
+ const handle = ctx.getActivePromptHandle();
5821
+ const domain = handle ? terminalAbortSeams?.getRunOwnerDomain?.(handle) : undefined;
5822
+ const correlations = handle && domain ? getJoinedPromptCorrelations({ resourceRunId: handle, domain }) : [];
5823
+ const root = runtime.activePromptCorrelation;
5824
+ if (
5825
+ root &&
5826
+ !correlations.some(
5827
+ correlation => correlation.commandId === root.commandId && correlation.turnId === root.turnId,
5828
+ )
5829
+ )
5830
+ correlations.unshift(root);
5831
+ for (const correlation of correlations) {
5832
+ // Renewal is attributed exactly like the correlated delivery below.
5833
+ renewPromptDeadline(correlation, event);
5834
+ noteToolDispatchBoundary(correlation, event);
5835
+ const submission = promptSubmissions.get(promptSubmissionKey(correlation));
5836
+ if (!submission || submission.abandoned) continue;
5837
+ const frame = {
5838
+ type: "event",
5839
+ kind: event.type,
5840
+ payload: toAgentWireEventPayload(event),
5841
+ ...correlation,
5842
+ };
5843
+ if (!submission.acknowledged) {
5844
+ submission.bufferedFrames.push(frame);
5845
+ continue;
5846
+ }
5847
+ deliverCorrelatedFrame(submission, frame);
5792
5848
  }
5793
- deliverCorrelatedFrame(submission, frame);
5794
5849
  };
5795
5850
  /**
5796
5851
  * Status of a registered deadline expiry attempt after an awaited
@@ -5809,7 +5864,10 @@ export function createNotificationsExtension(
5809
5864
  submission: PromptSubmission,
5810
5865
  attempt: { lease: PromptDeadlineLease; generation: number; selfAbortStarted?: true },
5811
5866
  ): DeadlineAttemptStatus => {
5812
- if (promptSubmissions.get(key) !== submission || submission.deadlineLease !== attempt.lease) return "stale";
5867
+ if (promptSubmissions.get(key) !== submission) return "stale";
5868
+ if (!attempt.selfAbortStarted && (submission.deadlineSuspended || submission.deadlineLease !== attempt.lease))
5869
+ return "superseded";
5870
+ if (submission.deadlineLease !== attempt.lease) return "stale";
5813
5871
  const lease = submission.deadlineLease;
5814
5872
  // A deadline expiry attempt must not be superseded by progress that its
5815
5873
  // OWN abort caused. Terminal fencing aborts the running tool, and the
@@ -6018,6 +6076,7 @@ export function createNotificationsExtension(
6018
6076
  maxMs: resolveSdkPromptMaxRuntimeMs(settings?.get("sdk.promptMaxRuntimeMs")),
6019
6077
  }),
6020
6078
  phase: "active",
6079
+ ...(sdkRunToken ? { sdkRunToken } : {}),
6021
6080
  ...(workLeaseHandle ? { workLease: workLeaseHandle } : {}),
6022
6081
  // Bound to the Agent run at `agent_start`; acceptance precedes execution.
6023
6082
  executionHandle: undefined,
@@ -6123,12 +6182,75 @@ export function createNotificationsExtension(
6123
6182
  correlation: { commandId: string; turnId: string },
6124
6183
  handle: string | undefined,
6125
6184
  ) => {
6126
- const submission = promptSubmissions.get(promptSubmissionKey(correlation));
6185
+ const key = promptSubmissionKey(correlation);
6186
+ const submission = promptSubmissions.get(key);
6127
6187
  if (submission) {
6188
+ if (submission.deadlineSuspended) {
6189
+ submission.deadlineSuspended = undefined;
6190
+ submission.deadlineLease = createPromptDeadlineLease({
6191
+ now: Date.now(),
6192
+ leaseMs: submission.deadlineLease.leaseMs,
6193
+ maxMs: submission.deadlineLease.maxMs,
6194
+ });
6195
+ armPromptDeadline(key, correlation);
6196
+ }
6128
6197
  submission.executionHandle = handle;
6129
6198
  submission.preflightAbort = undefined;
6130
6199
  }
6131
6200
  };
6201
+ const onPromptDiverted = (correlation: { commandId: string; turnId: string }) => {
6202
+ const submission = promptSubmissions.get(promptSubmissionKey(correlation));
6203
+ // A follow-up's private token can only be adopted by its exact future run.
6204
+ // Implicit steering has no such future-run authority and must leave pending.
6205
+ if (!submission?.sdkRunToken) removePendingPromptCorrelation(correlation);
6206
+ if (!submission || submission.terminal) return;
6207
+ submission.deadlineSuspended = true;
6208
+ if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
6209
+ submission.deadlineTimer = undefined;
6210
+ };
6211
+ const onPromptPromoted = (
6212
+ correlation: { commandId: string; turnId: string },
6213
+ promotion: { startsOwnRun?: boolean; removed?: boolean },
6214
+ handle: string | undefined,
6215
+ ) => {
6216
+ const key = promptSubmissionKey(correlation);
6217
+ removePendingPromptCorrelation(correlation);
6218
+ if (promotion.removed) {
6219
+ const completion: { terminalized?: boolean } = {};
6220
+ const removal = terminalizePrompt(
6221
+ correlation,
6222
+ { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
6223
+ {},
6224
+ undefined,
6225
+ completion,
6226
+ ).then(() => completion.terminalized === true);
6227
+ queuedRemovalTerminals.set(key, removal);
6228
+ trackReconciliationProducer(
6229
+ removal.finally(() => {
6230
+ if (queuedRemovalTerminals.get(key) === removal) queuedRemovalTerminals.delete(key);
6231
+ }),
6232
+ );
6233
+ return;
6234
+ }
6235
+ const submission = promptSubmissions.get(key);
6236
+ if (!submission || submission.terminal) return;
6237
+ submission.deadlineSuspended = undefined;
6238
+ submission.deadlineLease = createPromptDeadlineLease({
6239
+ now: Date.now(),
6240
+ leaseMs: submission.deadlineLease.leaseMs,
6241
+ maxMs: submission.deadlineLease.maxMs,
6242
+ });
6243
+ armPromptDeadline(key, correlation);
6244
+ if (promotion.startsOwnRun === true) {
6245
+ if (submission.sdkRunToken) pendingPromptCorrelationsBySdkRunToken.set(submission.sdkRunToken, correlation);
6246
+ else pendingPromptCorrelations.push(correlation);
6247
+ } else {
6248
+ bindPromptExecutionHandle(correlation, handle);
6249
+ const domain = handle ? terminalAbortSeams?.getRunOwnerDomain?.(handle) : undefined;
6250
+ if (handle && domain)
6251
+ joinedPromptOwners.set(key, { correlation, owner: { resourceRunId: handle, domain } });
6252
+ }
6253
+ };
6132
6254
  const terminalizePrompt = async (
6133
6255
  correlation: { commandId: string; turnId: string },
6134
6256
  requestedOutcome: SdkPromptTerminalOutcome,
@@ -6156,6 +6278,7 @@ export function createNotificationsExtension(
6156
6278
  // the seam must refuse to fence the successor's lineage (review
6157
6279
  // thread P1).
6158
6280
  const terminalExpectedEpoch = options.terminal ? terminalAbortSeams?.getTerminalTurnEpoch?.() : undefined;
6281
+ const deadlineAttempt = submission.deadlineAttempt;
6159
6282
  let winner: SdkPromptTerminalOutcome;
6160
6283
  try {
6161
6284
  winner = await kindReconciliation.claimPendingOutcome(
@@ -6179,7 +6302,6 @@ export function createNotificationsExtension(
6179
6302
  // attempt must then back off — reset to active and reschedule —
6180
6303
  // instead of fencing and publishing a deadline terminal for a live
6181
6304
  // prompt. A re-accepted or evicted submission is never touched.
6182
- const deadlineAttempt = submission.deadlineAttempt;
6183
6305
  if (deadlineAttempt) {
6184
6306
  const key = promptSubmissionKey(correlation);
6185
6307
  const status = deadlineAttemptStatus(key, submission, deadlineAttempt);
@@ -6366,6 +6488,7 @@ export function createNotificationsExtension(
6366
6488
  }
6367
6489
  if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
6368
6490
  if (!recordPromptTerminal(correlation)) return;
6491
+ joinedPromptOwners.delete(promptSubmissionKey(correlation));
6369
6492
  if (!runtime) {
6370
6493
  // The runtime (and with it the positioned ring) is gone, so the
6371
6494
  // terminal can never be published from this process; expire the
@@ -6547,6 +6670,8 @@ export function createNotificationsExtension(
6547
6670
  recordPromptAccepted,
6548
6671
  recordPromptFailure,
6549
6672
  discardPromptAcceptance,
6673
+ onPromptDiverted,
6674
+ onPromptPromoted,
6550
6675
  () => runtime?.stopping !== true,
6551
6676
  trackGateResolution,
6552
6677
  admitPromptSubmission,
@@ -6599,9 +6724,15 @@ export function createNotificationsExtension(
6599
6724
  return { aborted: true, disposition: "cancelled" as const };
6600
6725
  }
6601
6726
  }
6602
- await terminalizePrompt({ commandId, turnId }, cancellationOutcome, {
6603
- fence: hasExecutionHandle || !submission.preflightAbort,
6604
- });
6727
+ const removal = queuedRemovalTerminals.get(key);
6728
+ if (removal) {
6729
+ if (!(await removal))
6730
+ throw new TypedControlError("busy", "Queued prompt cancellation could not be durably confirmed.");
6731
+ } else {
6732
+ await terminalizePrompt({ commandId, turnId }, cancellationOutcome, {
6733
+ fence: hasExecutionHandle || !submission.preflightAbort,
6734
+ });
6735
+ }
6605
6736
  }
6606
6737
  return { aborted: true, disposition: "cancelled" as const };
6607
6738
  },
@@ -7107,6 +7238,20 @@ export function createNotificationsExtension(
7107
7238
  );
7108
7239
  return already_terminalResult;
7109
7240
  }
7241
+ if (currentSubmission.preflightAbort) {
7242
+ try {
7243
+ await currentSubmission.preflightAbort();
7244
+ } catch (error) {
7245
+ logger.warn(`sdk: accepted queued cancellation failed: ${String(error)}`);
7246
+ failPromptClosed(
7247
+ { commandId, turnId },
7248
+ currentSubmission,
7249
+ "terminal_uncertain",
7250
+ "Accepted queued prompt could not be settled before cancellation.",
7251
+ );
7252
+ return { ok: false as const, reason: "worker_unsettled" as const };
7253
+ }
7254
+ }
7110
7255
  if (preflightCancel?.hasPending()) {
7111
7256
  preflightCancel.cancel();
7112
7257
  // The seam cancels the SESSION-WIDE preflight controller:
@@ -7115,11 +7260,30 @@ export function createNotificationsExtension(
7115
7260
  if (noOtherConnectionPreflights?.() !== false)
7116
7261
  terminalAbortSeams?.cancelPendingPreflightForTerminalAbort?.();
7117
7262
  }
7118
- await terminalizePrompt(
7119
- { commandId, turnId },
7120
- { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
7121
- {},
7122
- );
7263
+ const removal = queuedRemovalTerminals.get(promptSubmissionKey({ commandId, turnId }));
7264
+ const completion: { terminalized?: boolean } = {};
7265
+ if (removal) completion.terminalized = await removal;
7266
+ else
7267
+ await terminalizePrompt(
7268
+ { commandId, turnId },
7269
+ { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
7270
+ { fence: Boolean(currentSubmission.executionHandle) },
7271
+ undefined,
7272
+ completion,
7273
+ );
7274
+ const durable = kindReconciliation.lookupResult(currentSubmission.reconciliationKind, {
7275
+ commandId,
7276
+ turnId,
7277
+ });
7278
+ if (
7279
+ completion.terminalized !== true &&
7280
+ (durable.terminalAt === undefined ||
7281
+ (durable.status !== "terminal_ok" && durable.status !== "failed"))
7282
+ ) {
7283
+ // The reserved marker remains uncertain; never commit deterministic no-effect
7284
+ // authority while the accepted submission still lacks a durable terminal.
7285
+ return { ok: false as const, reason: "worker_unsettled" as const };
7286
+ }
7123
7287
  // No prompt won the race: finalize the reserved row so a later
7124
7288
  // same-key retry replays this deterministic no_active_turn result
7125
7289
  // instead of reservation uncertainty (review thread P2).
@@ -7993,6 +8157,12 @@ export function createNotificationsExtension(
7993
8157
  runtime = {
7994
8158
  server,
7995
8159
  host,
8160
+ getJoinedPromptCorrelations,
8161
+ isPromptRunOwner: (correlation, owner) => {
8162
+ const handle = promptSubmissions.get(promptSubmissionKey(correlation))?.executionHandle;
8163
+ return handle === owner.resourceRunId && terminalAbortSeams?.getRunOwnerDomain?.(handle) === owner.domain;
8164
+ },
8165
+ retireJoinedPromptOwners,
7996
8166
  broadcastEventFrame,
7997
8167
  broadcastEventFrameWithReceipts,
7998
8168
  revisions,
@@ -9896,13 +10066,25 @@ export function createNotificationsExtension(
9896
10066
  const id = sessionId(ctx);
9897
10067
  const rt = runtimes.get(id);
9898
10068
  if (!rt) return;
9899
- void rt.host
9900
- .reportActivity("idle")
9901
- .catch(error => logger.warn(`notifications: idle activity checkpoint failed: ${String(error)}`));
9902
- // Clear the streaming flag for SDK consumers even when notifications are off.
9903
- rt.busy = false;
9904
- const correlation = rt.activePromptCorrelation;
9905
- if (correlation) {
10069
+ // The Agent's terminal owner is independent of delivery correlation. Never
10070
+ // borrow a successor's run handle to settle a predecessor's SDK prompts.
10071
+ const terminalOwner = terminalAbortSeams?.getTerminalRunOwnerForEvent?.(event);
10072
+ const rootCorrelation = rt.activePromptCorrelation;
10073
+ const correlations = terminalOwner ? rt.getJoinedPromptCorrelations(terminalOwner) : [];
10074
+ if (
10075
+ rootCorrelation &&
10076
+ (!terminalOwner || rt.isPromptRunOwner(rootCorrelation, terminalOwner)) &&
10077
+ !correlations.some(
10078
+ correlation =>
10079
+ correlation.commandId === rootCorrelation.commandId && correlation.turnId === rootCorrelation.turnId,
10080
+ )
10081
+ )
10082
+ correlations.unshift(rootCorrelation);
10083
+ for (const correlation of correlations) {
10084
+ // This attributed agent_end is an execution boundary even when the
10085
+ // subsequent durable terminal claim fails. A fatal transport closure
10086
+ // clears attribution, so an unrelated later agent_end cannot settle a
10087
+ // predecessor's prompt correlation.
9906
10088
  const assistants = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).filter(
9907
10089
  message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
9908
10090
  ) as Array<{ stopReason?: unknown; errorKind?: unknown; errorCode?: unknown }>;
@@ -9990,11 +10172,24 @@ export function createNotificationsExtension(
9990
10172
  }
9991
10173
  : {}),
9992
10174
  });
9993
- } else {
9994
- rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
9995
10175
  }
10176
+ if (correlations.length === 0) rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
10177
+ if (
10178
+ rootCorrelation &&
10179
+ terminalOwner &&
10180
+ !rt.isPromptRunOwner(rootCorrelation, terminalOwner) &&
10181
+ !correlations.some(
10182
+ correlation =>
10183
+ correlation.commandId === rootCorrelation.commandId && correlation.turnId === rootCorrelation.turnId,
10184
+ )
10185
+ )
10186
+ return;
10187
+ rt.busy = false;
10188
+ void rt.host
10189
+ .reportActivity("idle")
10190
+ .catch(error => logger.warn(`notifications: idle activity checkpoint failed: ${String(error)}`));
9996
10191
  finishAgentWork(rt, event.stopReason === "paused");
9997
- rt.activePromptCorrelation = undefined;
10192
+ if (rt.activePromptCorrelation === rootCorrelation) rt.activePromptCorrelation = undefined;
9998
10193
  terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
9999
10194
  try {
10000
10195
  pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
@@ -296,8 +296,13 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
296
296
  * so generation-191 owners cannot reap hosts with queued or promoted work.
297
297
  * Generation 193 gates streamed content on negotiated observer capabilities,
298
298
  * so existing owners are replaced before exposing the new observer path.
299
+ * Generation 201 binds queued cancellation and terminal deadlines to the exact
300
+ * consuming run, so pre-fix owners cannot serve the new generic lifecycle path.
301
+ * Generation 202 joins queued-removal terminal publication before ordinary
302
+ * abort acknowledgement, replacing owners that still acknowledge cancellation
303
+ * before its terminal record is durable.
299
304
  */
300
- export const DAEMON_GENERATION = 200;
305
+ export const DAEMON_GENERATION = 202;
301
306
 
302
307
  /**
303
308
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
@@ -167,6 +167,8 @@ export function sdkPublicFailure(code: string, details?: unknown, proof?: Public
167
167
  const kinds: Record<string, PublicFailureKind> = {
168
168
  usage: "usage",
169
169
  invalid_input: "usage",
170
+ unknown_operation: "usage",
171
+ not_a_repository: "usage",
170
172
  invalid_json: "invalid_json",
171
173
  broker_unavailable: "broker_unavailable",
172
174
  session_unavailable: "endpoint_stale",
@@ -177,6 +179,7 @@ export function sdkPublicFailure(code: string, details?: unknown, proof?: Public
177
179
  tail_timeout: "timeout",
178
180
  wait_timeout: "wait_timeout",
179
181
  uncertain_after_send: "uncertain_after_send",
182
+ busy: "busy",
180
183
  authorization_denied: "authorization_denied",
181
184
  unauthorized: "authorization_denied",
182
185
  forbidden: "authorization_denied",
@@ -203,8 +206,15 @@ export function sdkPublicFailure(code: string, details?: unknown, proof?: Public
203
206
  references,
204
207
  // A typed `resource_gone` keeps its public classification; the fixed
205
208
  // diagnostic only separates absent resource state from an empty result.
206
- // It asserts no cause and echoes no host-supplied text.
207
- ...(code === "resource_gone" ? { diagnostics: ["sdk_resource_gone" as const] } : {}),
209
+ // It asserts no cause and echoes no host-supplied text. The usage-mapped
210
+ // codes likewise attach fixed guidance instead of the original message.
211
+ ...(code === "resource_gone"
212
+ ? { diagnostics: ["sdk_resource_gone" as const] }
213
+ : code === "unknown_operation"
214
+ ? { diagnostics: ["sdk_unknown_operation" as const] }
215
+ : code === "not_a_repository"
216
+ ? { diagnostics: ["sdk_scope_requires_repository" as const] }
217
+ : {}),
208
218
  });
209
219
  }
210
220
 
@@ -1248,7 +1258,11 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
1248
1258
  ? ("wait_timeout" as const)
1249
1259
  : failure.input.kind,
1250
1260
  }
1251
- : {}),
1261
+ : failure.input.kind === "busy"
1262
+ ? // turn.prompt is refused while a turn runs and its admission is released
1263
+ // before any execution, so the prompt definitively did not apply.
1264
+ { proof: "pre-effect" as const }
1265
+ : {}),
1252
1266
  references: failure.input.references,
1253
1267
  });
1254
1268
  }
@@ -100,6 +100,8 @@ export interface SdkClientOptions {
100
100
 
101
101
  export interface SdkRequestOptions {
102
102
  timeoutMs?: number;
103
+ /** Absolute wall-clock deadline for this request, including connection setup. */
104
+ deadline?: number;
103
105
  idempotencyKey?: string;
104
106
  confirm?: boolean;
105
107
  /**
@@ -496,6 +498,14 @@ export class SdkClient {
496
498
  if (options.connectedOnly && error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
497
499
  throw error;
498
500
  }
501
+ const requestDeadline =
502
+ typeof options.deadline === "number" && Number.isFinite(options.deadline)
503
+ ? Math.min(options.deadline, this.#deadline ?? Number.POSITIVE_INFINITY)
504
+ : this.#deadline;
505
+ if (requestDeadline !== undefined && Date.now() >= requestDeadline) {
506
+ const error = this.#deadlineError();
507
+ throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
508
+ }
499
509
  let incarnation: Incarnation;
500
510
  if (options.connectedOnly) {
501
511
  try {
@@ -504,8 +514,8 @@ export class SdkClient {
504
514
  if (error instanceof SdkClientError) throw new SdkPreparedDispatchError(error);
505
515
  throw error;
506
516
  }
507
- } else incarnation = await this.#connect();
508
- const timeoutMs = this.#remainingTimeout(options.timeoutMs ?? this.#timeoutMs);
517
+ } else incarnation = await this.#connect(requestDeadline);
518
+ const timeoutMs = this.#remainingTimeout(options.timeoutMs ?? this.#timeoutMs, requestDeadline);
509
519
  if (timeoutMs <= 0) {
510
520
  const error = this.#deadlineError();
511
521
  throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
@@ -615,7 +625,7 @@ export class SdkClient {
615
625
  );
616
626
  return await deferred.promise;
617
627
  }
618
- if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
628
+ if (requestDeadline !== undefined && Date.now() >= requestDeadline) {
619
629
  const error = this.#deadlineError();
620
630
  this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
621
631
  return await deferred.promise;
@@ -686,17 +696,18 @@ export class SdkClient {
686
696
  return transportError("timeout", "SDK client deadline elapsed.", undefined, reconnect);
687
697
  }
688
698
 
689
- #remainingTimeout(limit = this.#timeoutMs): number {
690
- if (this.#deadline === undefined) return limit;
691
- return Math.min(limit, Math.max(0, this.#deadline - Date.now()));
699
+ #remainingTimeout(limit = this.#timeoutMs, deadline = this.#deadline): number {
700
+ if (deadline === undefined) return limit;
701
+ return Math.min(limit, Math.max(0, deadline - Date.now()));
692
702
  }
693
703
 
694
704
  #throwIfDeadlineElapsed(): void {
695
705
  if (this.#deadline !== undefined && Date.now() >= this.#deadline) throw this.#deadlineError();
696
706
  }
697
707
 
698
- async #connect(): Promise<Incarnation> {
708
+ async #connect(requestDeadline?: number): Promise<Incarnation> {
699
709
  this.#throwIfDeadlineElapsed();
710
+ if (requestDeadline !== undefined && Date.now() >= requestDeadline) throw this.#deadlineError();
700
711
  const current = this.#currentSocketRecord;
701
712
  if (current && this.#isActive(current) && current.socket.readyState === WebSocket.OPEN) return current;
702
713
  if (current)
@@ -708,7 +719,16 @@ export class SdkClient {
708
719
  this.#opening = cycle;
709
720
  cycle.promise = this.#openWithRetry(cycle);
710
721
  }
711
- return await cycle.promise!;
722
+ if (requestDeadline === undefined) return await cycle.promise!;
723
+ const remaining = requestDeadline - Date.now();
724
+ if (remaining <= 0) throw this.#deadlineError();
725
+ const expired = Promise.withResolvers<never>();
726
+ const timer = setTimeout(() => expired.reject(this.#deadlineError()), remaining);
727
+ try {
728
+ return await Promise.race([cycle.promise!, expired.promise]);
729
+ } finally {
730
+ clearTimeout(timer);
731
+ }
712
732
  }
713
733
 
714
734
  #requireConnectedIncarnation(): Incarnation {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import type { ProjectProgressSnapshot } from "../../../progress/progress-contract.js";
2
3
  import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../../prompt-status.js";
3
4
  import { TURN_RESULT_PROMPT_ALIAS, TURN_RESULT_SKILL_ALIAS } from "../../protocol/operation-registry.js";
4
-
5
5
  import type { ActiveProviderDescriptor } from "../../providers.js";
6
6
  import { ActiveProviderResolutionError } from "../../providers.js";
7
7
 
@@ -51,6 +51,8 @@ export interface SessionSurface {
51
51
  | Promise<{ bytes: Uint8Array; totalBytes: number } | undefined>;
52
52
 
53
53
  getJobs(): unknown | Promise<unknown>;
54
+ /** Q32 read-only project progress snapshot (`gjc.project_progress.v1`). */
55
+ getProjectProgress?(): ProjectProgressSnapshot | Promise<ProjectProgressSnapshot>;
54
56
  /** Q26 keyed lookup of a submitted prompt's authoritative reconciliation status. */
55
57
  getPromptStatus?(selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown | Promise<unknown>;
56
58
  getSkillInvokeStatus?(selector: {
@@ -152,6 +154,7 @@ const sources: Record<string, { resource: string; method: keyof SessionSurface;
152
154
  Q25: { resource: "jobs", method: "getJobs", mvcc: false },
153
155
  Q29: { resource: "activeProviders", method: "getActiveProviders", mvcc: false },
154
156
  Q27: { resource: "modelProfiles", method: "getModelProfiles", mvcc: true },
157
+ Q32: { resource: "progress", method: "getProjectProgress", mvcc: false },
155
158
  };
156
159
  const names = [
157
160
  "transcript.list",
@@ -186,6 +189,7 @@ const names = [
186
189
  "providers.list/active",
187
190
  "session.checkpoint",
188
191
  "turn.steer_status",
192
+ "session.progress",
189
193
  ];
190
194
 
191
195
  export class QueryHandlers {
@@ -275,6 +279,10 @@ export class QueryHandlers {
275
279
  );
276
280
  if (query === "Q29" && typeof this.surface.getActiveProviders !== "function")
277
281
  return this.#error(request, "unavailable", false, "providers.list/active is unavailable for this session.");
282
+ if (query === "Q32" && request.input && Object.keys(request.input).length > 0)
283
+ return this.#error(request, "invalid_request", false, "session.progress does not accept input fields.");
284
+ if (query === "Q32" && typeof this.surface.getProjectProgress !== "function")
285
+ return this.#error(request, "unavailable", false, "session.progress is unavailable for this session.");
278
286
  const source = sources[query];
279
287
  if (!source) return this.#error(request, "invalid_request");
280
288
  return await this.#pageSource(request, query, source);