@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,7 +5,7 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { AsyncJobManager } from "../../async";
7
7
  import type { Settings } from "../../config/settings";
8
- import type { ExtensionAPI, ExtensionContext } from "../../extensibility/extensions";
8
+ import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
9
9
  import {
10
10
  registerOwnedRegistration,
11
11
  resetTerminalAbortRegistriesForTests,
@@ -13,8 +13,11 @@ import {
13
13
  unregisterOwnedRegistration,
14
14
  } from "../../session/terminal-abort";
15
15
  import { Broker } from "../broker/broker";
16
+ import { createKindAwareReconciliation } from "../bus/kind-aware-reconciliation";
17
+ import { createPromptReconciliation } from "../bus/prompt-reconciliation";
16
18
  import { createReconciliationStore } from "../bus/reconciliation-store";
17
- import { CursorRegistry, QueryHandlers, RevisionStore } from "./query";
19
+ import { BROKER_RUNTIME_ABORT_CAPABILITY_FIELD, setBrokerRuntimeAbortCapabilityForTest } from "./control/runtime-gate";
20
+ import { CursorRegistry, QueryHandlers, type QueryResponse, RevisionStore } from "./query";
18
21
  import {
19
22
  createInvocationReconciliation,
20
23
  createSdkSessionRuntimeExtension,
@@ -88,6 +91,7 @@ function extensionContext(
88
91
  goalState?: unknown;
89
92
  branch?: unknown[];
90
93
  liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
94
+ transcript?: ExtensionTranscriptEntry[];
91
95
  } = {},
92
96
  ): ExtensionContext {
93
97
  return {
@@ -100,6 +104,7 @@ function extensionContext(
100
104
  getSessionName: () => undefined,
101
105
  getBranch: () => options.branch ?? [],
102
106
  },
107
+ getTranscript: () => options.transcript ?? [],
103
108
  getGoalState: () => options.goalState,
104
109
  } as unknown as ExtensionContext;
105
110
  }
@@ -146,6 +151,167 @@ async function queryGoalState(
146
151
  return response.page?.items[0];
147
152
  }
148
153
 
154
+ async function queryLastAssistant(ctx: ExtensionContext, sessionId: string): Promise<QueryResponse> {
155
+ const surface = createSdkSurfaceFactory({
156
+ ctx,
157
+ id: sessionId,
158
+ api: {} as ExtensionAPI,
159
+ }).query;
160
+ const store = new RevisionStore(sessionId);
161
+ const response = await new QueryHandlers(
162
+ surface,
163
+ sessionId,
164
+ store,
165
+ new CursorRegistry("last-assistant-test-token", store),
166
+ ).dispatch({
167
+ query: "session.last_assistant",
168
+ connectionId: "last-assistant-test-connection",
169
+ });
170
+ return response;
171
+ }
172
+
173
+ test("session.last_assistant returns the latest projected readable text past non-text assistant tails", async () => {
174
+ const sessionId = "last-assistant-readable-tail";
175
+ const transcript: ExtensionTranscriptEntry[] = [
176
+ {
177
+ id: "visible-readable",
178
+ role: "assistant",
179
+ textSummary: "first line second line",
180
+ ts: new Date(1).toISOString(),
181
+ content: [
182
+ { type: "text", text: "first line" },
183
+ { type: "thinking", thinking: "private reasoning" },
184
+ { type: "text", text: "second line" },
185
+ ],
186
+ },
187
+ ...Array.from(
188
+ { length: 2_000 },
189
+ (_, index): ExtensionTranscriptEntry => ({
190
+ id: `tool-tail-${index}`,
191
+ role: "assistant",
192
+ textSummary: "",
193
+ ts: new Date(index + 2).toISOString(),
194
+ body: index % 3 === 0 ? "" : index % 3 === 1 ? " \n\t " : undefined,
195
+ content:
196
+ index % 3 === 2
197
+ ? [
198
+ { type: "thinking", thinking: "not readable" },
199
+ { type: "toolCall", id: `call-${index}`, name: "read", arguments: {} },
200
+ ]
201
+ : undefined,
202
+ }),
203
+ ),
204
+ ];
205
+ const ctx = extensionContext(sessionId, "/tmp", {
206
+ transcript,
207
+ branch: [
208
+ {
209
+ type: "message",
210
+ message: { role: "assistant", content: [{ type: "text", text: "raw private tail" }] },
211
+ },
212
+ ],
213
+ });
214
+
215
+ expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({
216
+ ok: true,
217
+ page: { items: ["first line\nsecond line"] },
218
+ });
219
+ });
220
+
221
+ test("session.last_assistant returns resource_gone when the projected transcript has no readable assistant text", async () => {
222
+ const sessionId = "last-assistant-empty";
223
+ const ctx = extensionContext(sessionId, "/tmp", {
224
+ transcript: [
225
+ {
226
+ id: "user",
227
+ role: "user",
228
+ textSummary: "hello",
229
+ ts: new Date(1).toISOString(),
230
+ body: "hello",
231
+ },
232
+ {
233
+ id: "thinking-only",
234
+ role: "assistant",
235
+ textSummary: "",
236
+ ts: new Date(2).toISOString(),
237
+ content: [{ type: "thinking", thinking: "private reasoning" }],
238
+ },
239
+ ],
240
+ });
241
+
242
+ expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: false, error: { code: "resource_gone" } });
243
+ });
244
+
245
+ test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
246
+ const reconciliation = createPromptReconciliation();
247
+ const correlation = { commandId: "native-empty-command", turnId: "native-empty-turn" };
248
+ reconciliation.admit("native-empty-ref");
249
+ reconciliation.noteAccepted(correlation, "native-empty-ref");
250
+ reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "" });
251
+ expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
252
+ status: "failed",
253
+ error: { code: "prompt_failed" },
254
+ receiptState: "missing",
255
+ });
256
+ reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
257
+ expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({ status: "failed" });
258
+ });
259
+
260
+ test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
261
+ let records: unknown[] = [];
262
+ const store = {
263
+ load: async () => records,
264
+ transact: async (mutator: (current: never[]) => never[]) => {
265
+ records = mutator(records as never);
266
+ },
267
+ } as never;
268
+ const reconciliation = createKindAwareReconciliation({ store });
269
+ const correlation = { commandId: "broker-empty-command", turnId: "broker-empty-turn" };
270
+ await reconciliation.noteAccepted("prompt", correlation, "broker-empty-ref");
271
+ await reconciliation.noteTransition("prompt", correlation, {
272
+ type: "agent_end",
273
+ content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
274
+ });
275
+ const settled = reconciliation.lookupResult("prompt", { clientRef: "broker-empty-ref" });
276
+ expect(settled).toMatchObject({ status: "failed", error: { code: "prompt_failed" } });
277
+ await reconciliation.noteTransition("prompt", correlation, {
278
+ type: "agent_end",
279
+ content: { version: 1, type: "text", text: "late text", byteLength: 9, truncated: false },
280
+ });
281
+ const reloaded = createKindAwareReconciliation({ store });
282
+ await reloaded.hydrateFromStore();
283
+ expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
284
+ status: "failed",
285
+ error: { code: "prompt_failed" },
286
+ });
287
+ const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
288
+ await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
289
+ await reconciliation.finalizeOutcome(
290
+ "prompt",
291
+ noOpCorrelation,
292
+ { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
293
+ undefined,
294
+ "",
295
+ );
296
+ expect(reconciliation.lookupResult("prompt", { clientRef: "broker-noop-ref" })).toMatchObject({
297
+ status: "terminal_ok",
298
+ });
299
+ });
300
+
301
+ test("session-host prompt reconciliation fails closed when the accepted result is empty", async () => {
302
+ const reconciliation = createInvocationReconciliation();
303
+ const correlation = { commandId: "host-empty-command", turnId: "host-empty-turn" };
304
+ await reconciliation.noteAccepted("prompt", correlation, "host-empty-ref");
305
+ await reconciliation.noteTransition("prompt", correlation, {
306
+ type: "agent_end",
307
+ content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
308
+ });
309
+ expect(reconciliation.lookup("prompt", { clientRef: "host-empty-ref" })).toMatchObject({
310
+ status: "failed",
311
+ error: { code: "prompt_failed" },
312
+ });
313
+ });
314
+
149
315
  describe("SDK goal snapshot lifecycle", () => {
150
316
  test("recovers an active nonzero-activity goal from the durable session projection", async () => {
151
317
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-recovery-"));
@@ -405,7 +571,10 @@ test("durable reload keeps agent_end terminal across a paused successor transiti
405
571
  const correlation = { commandId: "durable-race-command", turnId: "durable-race-turn" };
406
572
  await reconciliation.noteAccepted("prompt", correlation, "durable-race-ref");
407
573
  pauseWrites = 2;
408
- const terminal = reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
574
+ const terminal = reconciliation.noteTransition("prompt", correlation, {
575
+ type: "agent_end",
576
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
577
+ });
409
578
  await writeStarted[0]!.promise;
410
579
  // A successor lifecycle event arrives while the terminal write is held. It
411
580
  // must observe the staged terminal record and never resurrect in_flight state.
@@ -455,7 +624,10 @@ test("agent_end is not swallowed when deadline persistence fails", async () => {
455
624
  message: "deadline",
456
625
  });
457
626
  await persistStarted.promise;
458
- const terminal = reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
627
+ const terminal = reconciliation.noteTransition("prompt", correlation, {
628
+ type: "agent_end",
629
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
630
+ });
459
631
  releasePersist.resolve();
460
632
  await expect(deadline).rejects.toThrow("held store failed");
461
633
  await terminal;
@@ -523,13 +695,86 @@ test("agent_end upgrades a durable deadline terminal when it races a successful
523
695
  code: "prompt_deadline_exceeded",
524
696
  message: "deadline",
525
697
  });
526
- await reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
698
+ await reconciliation.noteTransition("prompt", correlation, {
699
+ type: "agent_end",
700
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
701
+ });
527
702
  const reloaded = createInvocationReconciliation({ store });
528
703
  await reloaded.hydrate();
529
704
  expect(reloaded.lookup("prompt", { clientRef: "upgrade-race-ref" })).toMatchObject({ status: "terminal_ok" });
530
705
  });
531
706
 
532
- test("failed agent_end upgrade persistence retains the deadline record for retry", async () => {
707
+ test("an empty late agent_end cannot upgrade a durable prompt deadline failure", async () => {
708
+ let records: unknown[] = [];
709
+ const store = {
710
+ path: null,
711
+ load: async () => records,
712
+ transact: async (mutator: (current: never[]) => never[]) => {
713
+ records = mutator(records as never);
714
+ },
715
+ } as never;
716
+ const reconciliation = createInvocationReconciliation({ store });
717
+ const correlation = { commandId: "empty-deadline-command", turnId: "empty-deadline-turn" };
718
+ await reconciliation.noteAccepted("prompt", correlation, "empty-deadline-ref");
719
+ await reconciliation.finalizeOutcome("prompt", correlation, {
720
+ kind: "failed",
721
+ code: "prompt_deadline_exceeded",
722
+ message: "deadline",
723
+ });
724
+ await reconciliation.noteTransition("prompt", correlation, {
725
+ type: "agent_end",
726
+ content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
727
+ });
728
+ expect(reconciliation.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
729
+ status: "failed",
730
+ error: { code: "prompt_deadline_exceeded" },
731
+ });
732
+ const reloaded = createInvocationReconciliation({ store });
733
+ await reloaded.hydrate();
734
+ expect(reloaded.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
735
+ status: "failed",
736
+ error: { code: "prompt_deadline_exceeded" },
737
+ });
738
+ });
739
+
740
+ test("whitespace and provider failures stay actionable across a late empty agent_end", async () => {
741
+ let records: unknown[] = [];
742
+ const store = {
743
+ path: null,
744
+ load: async () => records,
745
+ transact: async (mutator: (current: never[]) => never[]) => {
746
+ records = mutator(records as never);
747
+ },
748
+ } as never;
749
+ const reconciliation = createInvocationReconciliation({ store });
750
+ const whitespace = { commandId: "whitespace-command", turnId: "whitespace-turn" };
751
+ await reconciliation.noteAccepted("prompt", whitespace, "whitespace-ref");
752
+ await reconciliation.noteTransition("prompt", whitespace, {
753
+ type: "agent_end",
754
+ content: { version: 1, type: "text", text: " \t\n ", byteLength: 4, truncated: false },
755
+ });
756
+ expect(reconciliation.lookup("prompt", { clientRef: "whitespace-ref" })).toMatchObject({
757
+ status: "failed",
758
+ error: { code: "prompt_failed", message: "Prompt submission failed." },
759
+ });
760
+
761
+ const providerFailure = { commandId: "provider-command", turnId: "provider-turn" };
762
+ await reconciliation.noteAccepted("prompt", providerFailure, "provider-ref");
763
+ await reconciliation.noteTransition("prompt", providerFailure, {
764
+ type: "agent_failed",
765
+ error: Object.assign(new Error("provider leaked detail"), { code: "provider_unavailable" }),
766
+ });
767
+ await reconciliation.noteTransition("prompt", providerFailure, {
768
+ type: "agent_end",
769
+ content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
770
+ });
771
+ expect(reconciliation.lookup("prompt", { clientRef: "provider-ref" })).toMatchObject({
772
+ status: "failed",
773
+ error: { code: "provider_unavailable", message: "Prompt submission failed." },
774
+ });
775
+ });
776
+
777
+ test("an empty prompt terminal remains failed after a late real failure", async () => {
533
778
  let records: unknown[] = [];
534
779
  let writes = 0;
535
780
  const store = {
@@ -550,9 +795,12 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
550
795
  code: "prompt_deadline_exceeded",
551
796
  message: "deadline",
552
797
  });
553
- await expect(reconciliation.noteTransition("prompt", correlation, { type: "agent_end" })).rejects.toThrow(
554
- "upgrade persist failed",
555
- );
798
+ await expect(
799
+ reconciliation.noteTransition("prompt", correlation, {
800
+ type: "agent_end",
801
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
802
+ }),
803
+ ).rejects.toThrow("upgrade persist failed");
556
804
  expect(reconciliation.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({
557
805
  status: "failed",
558
806
  error: { code: "prompt_deadline_exceeded" },
@@ -563,17 +811,132 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
563
811
  status: "failed",
564
812
  error: { code: "prompt_deadline_exceeded" },
565
813
  });
566
- await reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
814
+ await reconciliation.noteTransition("prompt", correlation, {
815
+ type: "agent_end",
816
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
817
+ });
567
818
  const reloaded = createInvocationReconciliation({ store });
568
819
  await reloaded.hydrate();
569
820
  expect(reloaded.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({ status: "terminal_ok" });
570
821
  });
571
822
 
823
+ test("late cancellation and non-text activity upgrade deadline terminals", async () => {
824
+ const makeStore = () => {
825
+ let records: unknown[] = [];
826
+ return {
827
+ path: null,
828
+ load: async () => records,
829
+ transact: async (mutator: (current: never[]) => never[]) => {
830
+ records = mutator(records as never);
831
+ },
832
+ } as never;
833
+ };
834
+ const cancelled = createInvocationReconciliation({ store: makeStore() });
835
+ const cancelledCorrelation = { commandId: "late-cancel-command", turnId: "late-cancel-turn" };
836
+ await cancelled.noteAccepted("prompt", cancelledCorrelation, "late-cancel-ref");
837
+ await cancelled.finalizeOutcome("prompt", cancelledCorrelation, {
838
+ kind: "failed",
839
+ code: "prompt_deadline_exceeded",
840
+ message: "deadline",
841
+ });
842
+ await cancelled.noteTransition("prompt", cancelledCorrelation, {
843
+ type: "agent_end",
844
+ outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
845
+ });
846
+ expect(cancelled.lookup("prompt", { clientRef: "late-cancel-ref" })).toMatchObject({ status: "terminal_ok" });
847
+
848
+ const activity = createInvocationReconciliation({ store: makeStore() });
849
+ const activityCorrelation = { commandId: "late-activity-command", turnId: "late-activity-turn" };
850
+ await activity.noteAccepted("prompt", activityCorrelation, "late-activity-ref");
851
+ await activity.finalizeOutcome("prompt", activityCorrelation, {
852
+ kind: "failed",
853
+ code: "prompt_deadline_exceeded",
854
+ message: "deadline",
855
+ });
856
+ await activity.noteTransition("prompt", activityCorrelation, { type: "agent_end", hasActivity: true });
857
+ expect(activity.lookup("prompt", { clientRef: "late-activity-ref" })).toMatchObject({ status: "terminal_ok" });
858
+ });
859
+ test("finalization without an explicit outcome follows the noteTransition evidence predicate", async () => {
860
+ const makeLocalStore = () => {
861
+ let records: unknown[] = [];
862
+ return {
863
+ path: null,
864
+ load: async () => records,
865
+ transact: async (mutator: (current: never[]) => never[]) => {
866
+ records = mutator(records as never);
867
+ },
868
+ } as never;
869
+ };
870
+ const contentBearing = createInvocationReconciliation({ store: makeLocalStore() });
871
+ const contentCorrelation = { commandId: "final-content-command", turnId: "final-content-turn" };
872
+ await contentBearing.noteAccepted("prompt", contentCorrelation, "final-content-ref");
873
+ await contentBearing.finalizeOutcome("prompt", contentCorrelation, undefined, undefined, undefined, {
874
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
875
+ });
876
+ expect(contentBearing.lookup("prompt", { clientRef: "final-content-ref" })).toMatchObject({
877
+ status: "terminal_ok",
878
+ });
879
+
880
+ const activityBearing = createInvocationReconciliation({ store: makeLocalStore() });
881
+ const activityCorrelation = { commandId: "final-activity-command", turnId: "final-activity-turn" };
882
+ await activityBearing.noteAccepted("prompt", activityCorrelation, "final-activity-ref");
883
+ await activityBearing.finalizeOutcome("prompt", activityCorrelation, undefined, undefined, undefined, {
884
+ hasActivity: true,
885
+ });
886
+ expect(activityBearing.lookup("prompt", { clientRef: "final-activity-ref" })).toMatchObject({
887
+ status: "terminal_ok",
888
+ });
889
+
890
+ const stoppedOutcome = createInvocationReconciliation({ store: makeLocalStore() });
891
+ const stoppedCorrelation = { commandId: "final-stopped-command", turnId: "final-stopped-turn" };
892
+ await stoppedOutcome.noteAccepted("prompt", stoppedCorrelation, "final-stopped-ref");
893
+ await stoppedOutcome.finalizeOutcome("prompt", stoppedCorrelation, undefined, undefined, undefined, {
894
+ outcomeKind: "stopped",
895
+ });
896
+ expect(stoppedOutcome.lookup("prompt", { clientRef: "final-stopped-ref" })).toMatchObject({
897
+ status: "terminal_ok",
898
+ });
899
+
900
+ const evidenceFree = createInvocationReconciliation({ store: makeLocalStore() });
901
+ const emptyCorrelation = { commandId: "final-empty-command", turnId: "final-empty-turn" };
902
+ await evidenceFree.noteAccepted("prompt", emptyCorrelation, "final-empty-ref");
903
+ await evidenceFree.finalizeOutcome("prompt", emptyCorrelation);
904
+ expect(evidenceFree.lookup("prompt", { clientRef: "final-empty-ref" })).toMatchObject({
905
+ status: "failed",
906
+ error: { code: "prompt_failed" },
907
+ });
908
+ const legacyArg5 = createInvocationReconciliation({ store: makeLocalStore() });
909
+ const legacyCorrelation = { commandId: "final-legacy-command", turnId: "final-legacy-turn" };
910
+ await legacyArg5.noteAccepted("prompt", legacyCorrelation, "final-legacy-ref");
911
+ await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
912
+ content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
913
+ });
914
+ expect(legacyArg5.lookup("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
915
+ status: "terminal_ok",
916
+ });
917
+
918
+ const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
919
+ const failureCorrelation = { commandId: "final-failure-command", turnId: "final-failure-turn" };
920
+ await explicitFailure.noteAccepted("prompt", failureCorrelation, "final-failure-ref");
921
+ await explicitFailure.finalizeOutcome("prompt", failureCorrelation, {
922
+ kind: "failed",
923
+ code: "provider_rejected",
924
+ message: "provider rejected",
925
+ });
926
+ expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
927
+ status: "failed",
928
+ error: { code: "provider_rejected" },
929
+ });
930
+ });
931
+
572
932
  test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
573
933
  const reconciliation = createInvocationReconciliation();
574
934
  const correlation = { commandId: "late-reason-command", turnId: "late-reason-turn" };
575
935
  await reconciliation.noteAccepted("prompt", correlation, "late-reason-ref");
576
- await reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
936
+ await reconciliation.noteTransition("prompt", correlation, {
937
+ type: "agent_end",
938
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
939
+ });
577
940
  const claimed = reconciliation.lookup("prompt", { clientRef: "late-reason-ref" }) as Record<string, unknown>;
578
941
  expect(claimed).toEqual({
579
942
  status: "terminal_ok",
@@ -649,7 +1012,10 @@ test("retains terminal host reconciliation records past the 15-minute window", a
649
1012
  reconciliation.admit("skill", "aged-skill-ref");
650
1013
  await reconciliation.noteAccepted("prompt", promptCorrelation, "aged-ref");
651
1014
  await reconciliation.noteAccepted("skill", skillCorrelation, "aged-skill-ref");
652
- await reconciliation.noteTransition("prompt", promptCorrelation, { type: "agent_end" });
1015
+ await reconciliation.noteTransition("prompt", promptCorrelation, {
1016
+ type: "agent_end",
1017
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
1018
+ });
653
1019
  await reconciliation.noteTransition("skill", skillCorrelation, { type: "agent_end" });
654
1020
 
655
1021
  clock += 15 * 60_000 + 1;
@@ -679,7 +1045,10 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
679
1045
  const correlation = { commandId: `capacity-command-${index}`, turnId: `capacity-turn-${index}` };
680
1046
  reconciliation.admit("prompt", `capacity-ref-${index}`);
681
1047
  await reconciliation.noteAccepted("prompt", correlation, `capacity-ref-${index}`);
682
- await reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
1048
+ await reconciliation.noteTransition("prompt", correlation, {
1049
+ type: "agent_end",
1050
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
1051
+ });
683
1052
  }
684
1053
  expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
685
1054
  status: "unknown",
@@ -735,6 +1104,8 @@ describe("SessionSdkSessionRuntime", () => {
735
1104
  });
736
1105
  test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
737
1106
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
1107
+ const operatorCapability = "runtime-terminal-abort-capability";
1108
+ setBrokerRuntimeAbortCapabilityForTest(operatorCapability);
738
1109
  const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
739
1110
  const api = {
740
1111
  on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
@@ -770,7 +1141,13 @@ describe("SessionSdkSessionRuntime", () => {
770
1141
  },
771
1142
  abortPromptAndWaitWithTerminal: async (handle, options) => {
772
1143
  seamCalls.push({ handle, scope: options.terminal?.scope ?? "none" });
773
- return { status: "settled", terminalScope: {} };
1144
+ return {
1145
+ status: "settled",
1146
+ terminalScope: {
1147
+ abortedAttemptEpoch: activeEpoch,
1148
+ lineageIdHash: "runtime-test-lineage",
1149
+ },
1150
+ };
774
1151
  },
775
1152
  },
776
1153
  });
@@ -865,7 +1242,43 @@ describe("SessionSdkSessionRuntime", () => {
865
1242
  }),
866
1243
  ]),
867
1244
  );
1245
+
1246
+ // A separately connected, explicitly confirmed local operator may stop
1247
+ // the active turn and its exact owned work without weakening ordinary
1248
+ // connection ownership checks.
1249
+ transport.feed("local-operator", {
1250
+ type: "control_request",
1251
+ id: "terminal-operator-abort",
1252
+ operation: "turn.abort",
1253
+ input: {
1254
+ mode: "terminal",
1255
+ scope: "owned",
1256
+ operator: true,
1257
+ [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: operatorCapability,
1258
+ },
1259
+ confirm: true,
1260
+ idempotencyKey: "terminal-operator-key",
1261
+ } as SdkFrame);
1262
+ await waitForFrame("terminal-operator-abort");
1263
+ expect(seamCalls).toEqual([
1264
+ { handle: "exact-run-handle", scope: "turn" },
1265
+ { handle: "later-run-handle", scope: "owned" },
1266
+ ]);
1267
+ expect(transport.sent.find(frame => frame.id === "terminal-operator-abort")).toMatchObject({
1268
+ ok: true,
1269
+ result: expect.objectContaining({ turn: "stopped", ownedWork: "stopped" }),
1270
+ });
1271
+ const operatorKeyHash = createHash("sha256").update("terminal-operator-key").digest("hex");
1272
+ const deliveryDeadline = Date.now() + 15_000;
1273
+ while (
1274
+ reconciliationStore.snapshotTerminalScopes().find(record => record.idempotencyKeyHash === operatorKeyHash)
1275
+ ?.responseState !== "sent"
1276
+ ) {
1277
+ if (Date.now() > deliveryDeadline) throw new Error("Timed out waiting for operator delivery receipt");
1278
+ await Bun.sleep(20);
1279
+ }
868
1280
  } finally {
1281
+ setBrokerRuntimeAbortCapabilityForTest(undefined);
869
1282
  await handlers.get("session_shutdown")?.({}, ctx);
870
1283
  await rm(cwd, { recursive: true, force: true });
871
1284
  }
@@ -1076,7 +1489,71 @@ describe("SessionSdkSessionRuntime", () => {
1076
1489
  }
1077
1490
  });
1078
1491
 
1079
- test("SDK-only host re-reads the active prompt AND its owner after an owner-mismatch reservation wins the race", async () => {
1492
+ test("SDK-only operator abort never adopts a turn that starts after idle admission", async () => {
1493
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-operator-idle-race-"));
1494
+ const capability = "operator-idle-race-capability";
1495
+ setBrokerRuntimeAbortCapabilityForTest(capability);
1496
+ const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
1497
+ const api = {
1498
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
1499
+ handlers.set(event, handler);
1500
+ },
1501
+ } as unknown as ExtensionAPI;
1502
+ const transport = memoryTransport();
1503
+ const reconciliationStore = createReconciliationStore({
1504
+ sessionFile: path.join(cwd, "session.json"),
1505
+ sessionId: transport.sessionId,
1506
+ });
1507
+ const seamCalls: string[] = [];
1508
+ let promptReads = 0;
1509
+ createSdkSessionRuntimeExtension(api, {
1510
+ agentDir: cwd,
1511
+ createTransport: async () => transport,
1512
+ terminalAbortSeams: {
1513
+ getReconciliationStore: () => reconciliationStore,
1514
+ getTerminalTurnEpoch: () => (promptReads++ === 0 ? undefined : 9),
1515
+ getActivePromptHandle: () => (promptReads === 0 ? undefined : "successor-handle"),
1516
+ getActivePromptOwnerConnectionId: () => "successor-owner",
1517
+ cancelPendingPreflightForTerminalAbort: () => {},
1518
+ abortPromptAndWaitWithTerminal: async handle => {
1519
+ seamCalls.push(handle);
1520
+ return { status: "settled", terminalScope: {} };
1521
+ },
1522
+ },
1523
+ });
1524
+ const ctx = extensionContext(transport.sessionId, cwd);
1525
+ try {
1526
+ await handlers.get("session_start")?.({}, ctx);
1527
+ transport.feed("local-operator", {
1528
+ type: "control_request",
1529
+ id: "operator-idle-race",
1530
+ operation: "turn.abort",
1531
+ input: {
1532
+ mode: "terminal",
1533
+ operator: true,
1534
+ [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: capability,
1535
+ },
1536
+ confirm: true,
1537
+ idempotencyKey: "operator-idle-race-key",
1538
+ } as SdkFrame);
1539
+ const deadline = Date.now() + 15_000;
1540
+ while (!transport.sent.some(frame => frame.id === "operator-idle-race")) {
1541
+ if (Date.now() > deadline) throw new Error("Timed out waiting for operator idle-race response");
1542
+ await Bun.sleep(20);
1543
+ }
1544
+ expect(transport.sent.find(frame => frame.id === "operator-idle-race")).toMatchObject({
1545
+ ok: true,
1546
+ result: expect.objectContaining({ turn: "no_active_turn", terminal: "terminal_no_effect" }),
1547
+ });
1548
+ expect(seamCalls).toHaveLength(0);
1549
+ } finally {
1550
+ setBrokerRuntimeAbortCapabilityForTest(undefined);
1551
+ await handlers.get("session_shutdown")?.({}, ctx);
1552
+ await rm(cwd, { recursive: true, force: true });
1553
+ }
1554
+ });
1555
+
1556
+ test("SDK-only host re-reads the active prompt AND its owner after an owner-mismatch reservation race", async () => {
1080
1557
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-owner-race-"));
1081
1558
  const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
1082
1559
  const api = {
@@ -1870,10 +2347,10 @@ describe("SessionSdkSessionRuntime", () => {
1870
2347
  prompt("conn-b", "stale-b");
1871
2348
  await waitResponse("stale-b");
1872
2349
  // A later AGENT-INITIATED turn starts: the pending queue is empty, so
1873
- // the owner stays conn-a (B's stale connection is not associated).
2350
+ // the predecessor's conn-a owner is cleared and no SDK client owns it.
1874
2351
  await handlers.get("agent_start")?.({ type: "agent_start" }, ctx);
1875
- // B's abort must NOT stop the agent-initiated turn.
1876
- transport.feed("conn-b", {
2352
+ // A's stale owner must NOT stop the agent-initiated turn.
2353
+ transport.feed("conn-a", {
1877
2354
  type: "control_request",
1878
2355
  id: "stale-owner-abort",
1879
2356
  operation: "turn.abort",
@@ -1987,6 +2464,40 @@ describe("SessionSdkSessionRuntime", () => {
1987
2464
  }
1988
2465
  });
1989
2466
 
2467
+ test("agent-initiated successor activity cannot renew a stale predecessor deadline", async () => {
2468
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-stale-deadline-owner-"));
2469
+ try {
2470
+ const harness = await invocationHarness("stale-deadline-owner", cwd, {
2471
+ settings: {
2472
+ get: (key: string) =>
2473
+ key === "sdk.promptDeadlineMs" ? 25 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
2474
+ } as unknown as Settings,
2475
+ sendUserMessage: async (_content, options) => {
2476
+ await options?.onPreflightAcceptCommit?.();
2477
+ await neverSettlingPromise();
2478
+ },
2479
+ });
2480
+ const prompt = await harness.control("turn.prompt", { text: "predecessor" });
2481
+ expect(prompt.ok).toBe(true);
2482
+ const ids = { commandId: prompt.result?.commandId, turnId: prompt.result?.turnId };
2483
+ await harness.emit("agent_start");
2484
+ // Empty drain represents an agent-initiated successor with no SDK owner.
2485
+ await harness.emit("agent_start");
2486
+ for (let i = 0; i < 3; i += 1) {
2487
+ await harness.emit("tool_execution_start");
2488
+ await Bun.sleep(15);
2489
+ }
2490
+ expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
2491
+ status: "failed",
2492
+ error: { code: "prompt_deadline_exceeded" },
2493
+ });
2494
+ await harness.stop();
2495
+ } finally {
2496
+ await Bun.sleep(50);
2497
+ await rm(cwd, { recursive: true, force: true });
2498
+ }
2499
+ });
2500
+
1990
2501
  test("SDK-only host retires an accepted-then-failed submission so a later agent-initiated turn is not mis-owned", async () => {
1991
2502
  // An idle submission is accepted (pending entry pushed) and then REJECTS
1992
2503
  // before agent_start (e.g. the busy retry expires). The failed entry must
@@ -2863,6 +3374,7 @@ describe("SessionSdkSessionRuntime", () => {
2863
3374
  },
2864
3375
  } as any;
2865
3376
  const sessionId = "broker-recovery";
3377
+ const endpointUrl = "ws://127.0.0.1:1";
2866
3378
  createSdkSessionRuntimeExtension(api, {
2867
3379
  agentDir,
2868
3380
  createTransport: async ({ stateRoot, token }) => ({
@@ -2874,8 +3386,8 @@ describe("SessionSdkSessionRuntime", () => {
2874
3386
  start: async () => {
2875
3387
  const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
2876
3388
  await mkdir(path.dirname(endpoint), { recursive: true });
2877
- await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
2878
- return { url: "ws://127.0.0.1:1" };
3389
+ await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid, url: endpointUrl }));
3390
+ return { url: endpointUrl };
2879
3391
  },
2880
3392
  stop: async () => {},
2881
3393
  }),
@@ -2932,6 +3444,7 @@ describe("SessionSdkSessionRuntime", () => {
2932
3444
  createSdkSessionRuntimeExtension(api, {
2933
3445
  agentDir,
2934
3446
  brokerRegistrationRequired: true,
3447
+ lifecycleRequestId: "broker-required-marker",
2935
3448
  createTransport: async ({ sessionId, stateRoot, token }) => ({
2936
3449
  sessionId,
2937
3450
  stateRoot,
@@ -2985,7 +3498,7 @@ async function invocationHarness(
2985
3498
  sessionId: string,
2986
3499
  cwd: string,
2987
3500
  hooks: {
2988
- sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<void>;
3501
+ sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<unknown>;
2989
3502
  invokeSkill?: (name: string, args?: string, options?: PreflightHooks) => Promise<unknown>;
2990
3503
  abort?: () => void;
2991
3504
  isIdle?: () => boolean;
@@ -3010,7 +3523,10 @@ async function invocationHarness(
3010
3523
  on(event: string, handler: (event: unknown, ctx: unknown) => Promise<void> | void) {
3011
3524
  handlers.set(event, handler);
3012
3525
  },
3013
- sendUserMessage: hooks.sendUserMessage ?? (async () => {}),
3526
+ sendUserMessage: async (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => {
3527
+ const result = await hooks.sendUserMessage?.(content, options);
3528
+ return result === undefined ? "completed" : result;
3529
+ },
3014
3530
  } as unknown as ExtensionAPI;
3015
3531
  const interceptorStore = hooks.persistInterceptor
3016
3532
  ? createInterceptorReconciliationStore(
@@ -3202,13 +3718,19 @@ async function settledStatus(
3202
3718
  name: string,
3203
3719
  input: Record<string, unknown>,
3204
3720
  ): Promise<NonNullable<ResponseFrame["result"]>> {
3205
- for (let attempt = 0; attempt < 200; attempt += 1) {
3721
+ // Wall-clock budget instead of a fixed poll count: CI runners can starve the
3722
+ // event loop long enough to exhaust 200 ~1ms polls before a 25ms deadline
3723
+ // timer is dispatched, failing a correct implementation on timing alone.
3724
+ // The contract is unchanged — the status must still reach a terminal
3725
+ // reconciliation state with the asserted shape within a bounded horizon.
3726
+ const budgetEndsAt = Date.now() + 10_000;
3727
+ for (;;) {
3206
3728
  const frame = await harness.query(name, input);
3207
3729
  const result = frame.result;
3208
3730
  if (result && (result.status === "failed" || result.status === "terminal_ok")) return result;
3731
+ if (Date.now() > budgetEndsAt) throw new Error(`${name} never reported a terminal reconciliation status`);
3209
3732
  await Bun.sleep(1);
3210
3733
  }
3211
- throw new Error(`${name} never reported a terminal reconciliation status`);
3212
3734
  }
3213
3735
 
3214
3736
  function neverSettlingPromise(): Promise<void> {
@@ -3429,7 +3951,10 @@ describe("post-acceptance invocation terminalization", () => {
3429
3951
  },
3430
3952
  ],
3431
3953
  });
3432
- expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({ status: "terminal_ok" });
3954
+ expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
3955
+ status: "failed",
3956
+ error: { code: "prompt_failed" },
3957
+ });
3433
3958
  await harness.stop();
3434
3959
  } finally {
3435
3960
  await Bun.sleep(50);
@@ -3460,7 +3985,10 @@ describe("post-acceptance invocation terminalization", () => {
3460
3985
  },
3461
3986
  ],
3462
3987
  });
3463
- expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({ status: "terminal_ok" });
3988
+ expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
3989
+ status: "failed",
3990
+ error: { code: "prompt_failed" },
3991
+ });
3464
3992
  await harness.stop();
3465
3993
  } finally {
3466
3994
  await Bun.sleep(50);
@@ -3489,7 +4017,8 @@ describe("post-acceptance invocation terminalization", () => {
3489
4017
  });
3490
4018
  await harness.emit("agent_end", event);
3491
4019
  expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
3492
- status: "terminal_ok",
4020
+ status: "failed",
4021
+ error: { code: "prompt_failed" },
3493
4022
  });
3494
4023
  await harness.stop();
3495
4024
  } finally {
@@ -3530,7 +4059,9 @@ describe("post-acceptance invocation terminalization", () => {
3530
4059
  error: { code: "provider_http_402" },
3531
4060
  });
3532
4061
  expect((await harness.query("turn.prompt_status", replacementIds)).result?.status).toBe("in_flight");
3533
- await harness.emit("agent_end", { messages: [{ role: "assistant", stopReason: "stop" }] });
4062
+ await harness.emit("agent_end", {
4063
+ messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
4064
+ });
3534
4065
  expect(await settledStatus(harness, "turn.prompt_status", replacementIds)).toMatchObject({
3535
4066
  status: "terminal_ok",
3536
4067
  });
@@ -3656,7 +4187,9 @@ describe("post-acceptance invocation terminalization", () => {
3656
4187
  expect(successor.ok).toBe(true);
3657
4188
  const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
3658
4189
  await harness.emit("agent_start");
3659
- await harness.emit("agent_end", { messages: [{ role: "assistant", stopReason: "stop" }] });
4190
+ await harness.emit("agent_end", {
4191
+ messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
4192
+ });
3660
4193
  secondInflight.resolve();
3661
4194
  expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
3662
4195
  status: "terminal_ok",
@@ -3789,7 +4322,9 @@ describe("post-acceptance invocation terminalization", () => {
3789
4322
  expect(successor.ok).toBe(true);
3790
4323
  const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
3791
4324
  await harness.emit("agent_start");
3792
- await harness.emit("agent_end", { messages: [{ role: "assistant", stopReason: "stop" }] });
4325
+ await harness.emit("agent_end", {
4326
+ messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
4327
+ });
3793
4328
  secondInflight.resolve();
3794
4329
  expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
3795
4330
  status: "terminal_ok",
@@ -3938,7 +4473,7 @@ describe("post-acceptance invocation terminalization", () => {
3938
4473
  status: "failed",
3939
4474
  error: { code: "aborted" },
3940
4475
  });
3941
- await harness.emit("agent_end");
4476
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
3942
4477
  expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
3943
4478
  status: "terminal_ok",
3944
4479
  });
@@ -3989,8 +4524,8 @@ describe("post-acceptance invocation terminalization", () => {
3989
4524
  expect(await harness.query("turn.prompt_status", successorIds)).toMatchObject({
3990
4525
  result: { status: expect.stringMatching(/accepted|in_flight/) },
3991
4526
  });
3992
- await harness.emit("agent_end");
3993
- await harness.emit("agent_end");
4527
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4528
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
3994
4529
  expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
3995
4530
  status: "terminal_ok",
3996
4531
  });
@@ -4078,7 +4613,7 @@ describe("post-acceptance invocation terminalization", () => {
4078
4613
  expect(accepted.ok).toBe(true);
4079
4614
  const successorIds = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
4080
4615
  await harness.emit("agent_start");
4081
- await harness.emit("agent_end");
4616
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4082
4617
  expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
4083
4618
  status: "terminal_ok",
4084
4619
  });
@@ -4277,7 +4812,7 @@ describe("post-acceptance invocation terminalization", () => {
4277
4812
  const accepted = await harness.control("turn.prompt", { text: "hello" });
4278
4813
  const { commandId, turnId } = accepted.result ?? {};
4279
4814
  await harness.emit("agent_start");
4280
- await harness.emit("agent_end");
4815
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4281
4816
  const claimed = await settledStatus(harness, "turn.prompt_status", { commandId, turnId });
4282
4817
  expect(claimed).toMatchObject({ status: "terminal_ok" });
4283
4818
  inflight.reject(Object.assign(new Error("late provider failure"), { code: "upstream_error" }));
@@ -4445,7 +4980,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
4445
4980
  const first = await harness.control("turn.prompt", { text: "first" });
4446
4981
  expect(first.ok).toBe(true);
4447
4982
  await harness.emit("agent_start");
4448
- await harness.emit("agent_end");
4983
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4449
4984
  const followUpB = await harness.control("turn.follow_up", { text: "b" });
4450
4985
  const followUpC = await harness.control("turn.follow_up", { text: "c" });
4451
4986
  promoted[0]?.({ startsOwnRun: true });
@@ -4593,7 +5128,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
4593
5128
  }
4594
5129
  const midRun = await harness.query("turn.prompt_status", ids);
4595
5130
  expect(midRun.result?.status).not.toBe("failed");
4596
- await harness.emit("agent_end");
5131
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4597
5132
  const final = await harness.query("turn.prompt_status", ids);
4598
5133
  expect(final.result?.status).toBe("terminal_ok");
4599
5134
  await harness.stop();
@@ -4688,7 +5223,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
4688
5223
  // The consuming run ends. The consumed submission must terminalize
4689
5224
  // WITH it (terminal_ok, never a fabricated prompt_deadline_exceeded):
4690
5225
  // with the bug it would still be parked in pending as merely accepted.
4691
- await handlers.get("agent_end")?.({}, ctx);
5226
+ await handlers.get("agent_end")?.({ messages: [{ role: "assistant", content: "completed" }] }, ctx);
4692
5227
  const statusOf = async (ids: { commandId?: string; turnId?: string }, frameId: string) => {
4693
5228
  transport.feed("conn-a", {
4694
5229
  type: "query_request",
@@ -4766,7 +5301,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
4766
5301
  expect(midRun.result?.status).not.toBe("terminal_ok");
4767
5302
  expect(midRun.result?.status).not.toBe("failed");
4768
5303
  // The in-flight run ends: the diverted correlation terminalizes with it.
4769
- await harness.emit("agent_end");
5304
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4770
5305
  expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
4771
5306
  status: "terminal_ok",
4772
5307
  });
@@ -4825,7 +5360,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
4825
5360
  expect(queued.result?.status).not.toBe("failed");
4826
5361
  // Real consumption re-leases and attaches to the in-flight run.
4827
5362
  consumed?.({ startsOwnRun: false });
4828
- await harness.emit("agent_end");
5363
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4829
5364
  expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
4830
5365
  status: "terminal_ok",
4831
5366
  });
@@ -5265,6 +5800,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5265
5800
  const acceptedA = (await waitFrame("settle-early-a")) as { result?: { commandId?: string; turnId?: string } };
5266
5801
  const idsA = { commandId: acceptedA.result?.commandId, turnId: acceptedA.result?.turnId };
5267
5802
  const statusDeadline = Date.now() + 15_000;
5803
+ let settledStatus: string | undefined;
5268
5804
  for (;;) {
5269
5805
  transport.feed("conn-a", {
5270
5806
  type: "query_request",
@@ -5276,10 +5812,12 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5276
5812
  result?: { status?: string };
5277
5813
  };
5278
5814
  transport.sent.splice(transport.sent.indexOf(statusFrame), 1);
5279
- if (statusFrame.result?.status === "terminal_ok") break;
5815
+ settledStatus = statusFrame.result?.status;
5816
+ if (statusFrame.result?.status === "terminal_ok" || statusFrame.result?.status === "failed") break;
5280
5817
  if (Date.now() > statusDeadline) throw new Error("settled prompt never reported terminal_ok");
5281
5818
  await Bun.sleep(20);
5282
5819
  }
5820
+ expect(settledStatus).toBe("failed");
5283
5821
  // conn-b's prompt is accepted and hangs; its run then starts.
5284
5822
  transport.feed("conn-b", {
5285
5823
  type: "control_request",
@@ -5954,7 +6492,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
5954
6492
  Promise.resolve(options?.onPreflightAcceptCommit?.()).then(() => {
5955
6493
  if (options?.onQueuedPromoted) promoted.push(options.onQueuedPromoted);
5956
6494
  options?.onPreflightAccepted?.();
5957
- return {};
6495
+ return "completed";
5958
6496
  }),
5959
6497
  } as unknown as ExtensionAPI;
5960
6498
  const transport = memoryTransport();
@@ -6042,7 +6580,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
6042
6580
  // invocation left the remaining follow-ups durably accepted — their
6043
6581
  // result lookups would never complete and a restart would report them
6044
6582
  // failed even though they ran in the shared turn.
6045
- await handlers.get("agent_end")?.({ type: "agent_end" }, ctx);
6583
+ await handlers.get("agent_end")?.({ type: "agent_end", stopReason: "cancelled", messages: [] }, ctx);
6046
6584
  {
6047
6585
  const terminalDeadline = Date.now() + 15_000;
6048
6586
  const promptStatuses = () =>