@gajae-code/coding-agent 0.14.2 → 0.15.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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -74,9 +74,47 @@ export interface SdkRequestOptions {
74
74
  timeoutMs?: number;
75
75
  idempotencyKey?: string;
76
76
  confirm?: boolean;
77
+ /**
78
+ * Synchronous pre-send observer for one request. Called after the
79
+ * connection is live and validated, immediately before the frame is written
80
+ * to the socket. Throwing aborts the dispatch: nothing is written, no sent
81
+ * record is retained, and the request rejects with the thrown error, so the
82
+ * caller may safely retry (pre-send semantics).
83
+ */
84
+ beforeDispatch?: SdkBeforeDispatchHandler;
85
+ /**
86
+ * Synchronous dispatch-boundary observer for one request. Called immediately
87
+ * after the frame was handed to the socket — never before — and before any
88
+ * other client work. From this point a transport close before the response
89
+ * settles the request as `uncertain_after_send`; observer exceptions cannot
90
+ * alter that settlement.
91
+ */
92
+ onDispatch?: SdkDispatchHandler;
77
93
  }
78
-
79
94
  export type SdkFrame = Record<string, unknown>;
95
+ /**
96
+ * Synchronous by contract. Returning a thenable (e.g. an `async` function) is a
97
+ * contract violation: pre-send the dispatch aborts retryably and the eventual
98
+ * rejection is sunk, never escaping to the process unhandled-rejection channel.
99
+ */
100
+ export type SdkBeforeDispatchHandler = (request: SdkDispatchContext) => void;
101
+ /**
102
+ * Synchronous by contract. A returned thenable's rejection is sunk; it can
103
+ * neither displace request settlement nor escape to the process
104
+ * unhandled-rejection channel.
105
+ */
106
+ export type SdkDispatchHandler = (request: SdkDispatchContext) => void;
107
+
108
+ /**
109
+ * Facts about one request at its dispatch boundary. `frame.id` is the exact
110
+ * correlated identity a response frame must carry to settle this request.
111
+ */
112
+ export interface SdkDispatchContext {
113
+ readonly frame: SdkFrame;
114
+ /** Transport generation this request was written to. */
115
+ readonly connectionId: string | undefined;
116
+ readonly generation: number;
117
+ }
80
118
 
81
119
  /** Request identity retained after an uncertain send for lifecycle reconciliation. */
82
120
 
@@ -158,6 +196,52 @@ function parseFrame(value: unknown): Frame {
158
196
  }
159
197
  throw new SdkClientError("protocol_error", "SDK server sent a malformed frame.");
160
198
  }
199
+ /**
200
+ * Recursively freezes the parsed request frame handed to dispatch-boundary
201
+ * observers. The frame a consumer sees in `beforeDispatch`/`onDispatch` must be
202
+ * the exact immutable bytes-derived identity that went on the wire, so a
203
+ * mutating callback can never desynchronize the advertised identity or the
204
+ * reconciliation fingerprint from the request that was actually sent.
205
+ */
206
+ function deepFreeze<T>(value: T): T {
207
+ if (value && typeof value === "object") {
208
+ for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested);
209
+ Object.freeze(value);
210
+ }
211
+ return value;
212
+ }
213
+
214
+ type Thenable = { then?: unknown };
215
+
216
+ /**
217
+ * Observers are synchronous by contract, but TypeScript happily accepts an
218
+ * `async` function where `() => void` is expected. A rejected promise returned
219
+ * by an observer would otherwise reach the process-level unhandled-rejection
220
+ * channel — terminating `--unhandled-rejections=strict` consumers — and a
221
+ * rejected `beforeDispatch` would silently not abort the dispatch. Observers
222
+ * that return a thenable are treated as a contract violation: detected
223
+ * explicitly, sunk here, and (pre-send) failed before the wire.
224
+ */
225
+ function isThenable(value: unknown): value is Thenable {
226
+ return (
227
+ (typeof value === "object" || typeof value === "function") &&
228
+ value !== null &&
229
+ typeof (value as Thenable).then === "function"
230
+ );
231
+ }
232
+
233
+ function sinkThenable(value: unknown): void {
234
+ const thenable = value as { then: (onFulfilled: unknown, onRejected: (error: unknown) => void) => unknown };
235
+ try {
236
+ thenable.then(undefined, () => {
237
+ // Observer rejections never displace settlement; swallow them here
238
+ // so they cannot escape to the process unhandled-rejection channel.
239
+ });
240
+ } catch {
241
+ // A throwing then-accessor already surfaced synchronously to the caller
242
+ // of the observer; nothing further to sink.
243
+ }
244
+ }
161
245
 
162
246
  function lifecycleFingerprint(operation: string, input: unknown): string {
163
247
  return createHash("sha256").update(JSON.stringify({ operation, input })).digest("hex");
@@ -255,9 +339,9 @@ export class SdkClient {
255
339
  }
256
340
  }
257
341
 
258
- request(frame: SdkFrame, timeout?: number | { timeoutMs?: number; idempotencyKey?: string }): Promise<SdkFrame> {
259
- const options = typeof timeout === "number" ? { timeoutMs: timeout } : (timeout ?? {});
260
- return this.#request(frame, options) as Promise<SdkFrame>;
342
+ request(frame: SdkFrame, options?: number | SdkRequestOptions): Promise<SdkFrame> {
343
+ const resolved = typeof options === "number" ? { timeoutMs: options } : (options ?? {});
344
+ return this.#request(frame, resolved) as Promise<SdkFrame>;
261
345
  }
262
346
 
263
347
  close(): Promise<void> {
@@ -370,7 +454,18 @@ export class SdkClient {
370
454
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
371
455
  };
372
456
  const serializedRequest = JSON.stringify(requestFrame);
373
- const serializedFrame = JSON.parse(serializedRequest) as Frame;
457
+ const serializedFrame: Frame = deepFreeze(JSON.parse(serializedRequest) as Frame);
458
+ // Snapshot reconciliation identity from the exact serialized bytes BEFORE any
459
+ // observer runs. Callers own `options`, so rereading `options.idempotencyKey`
460
+ // or recomputing the fingerprint after a callback would let a mutating or
461
+ // getter-swapping observer diverge the retained record from the wire request.
462
+ const sentOperation = typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined;
463
+ const sentIdempotencyKey =
464
+ typeof serializedFrame.idempotencyKey === "string" ? serializedFrame.idempotencyKey : undefined;
465
+ const sentFingerprint =
466
+ typeof serializedFrame.operation === "string"
467
+ ? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
468
+ : inputFingerprint(serializedFrame.input ?? {});
374
469
  const deferred = Promise.withResolvers<unknown>();
375
470
  const pending: Pending = {
376
471
  incarnation,
@@ -397,26 +492,113 @@ export class SdkClient {
397
492
  this.#settlePending(id, pending, new SdkClientError("unavailable", "SDK WebSocket is not connected"));
398
493
  return await deferred.promise;
399
494
  }
495
+ if (options.beforeDispatch) {
496
+ try {
497
+ const observerResult = options.beforeDispatch({
498
+ frame: serializedFrame,
499
+ connectionId: this.connectionId,
500
+ generation: incarnation.generation,
501
+ });
502
+ if (isThenable(observerResult)) {
503
+ // An async observer cannot honor the synchronous pre-send
504
+ // contract: its abort decision is unavailable before the
505
+ // write. Sink the eventual rejection so it never escapes,
506
+ // and abort the dispatch pre-send (retryable, nothing on
507
+ // the wire).
508
+ sinkThenable(observerResult);
509
+ this.#settlePending(
510
+ id,
511
+ pending,
512
+ new SdkClientError(
513
+ "invalid_input",
514
+ "beforeDispatch must be synchronous; an async observer cannot gate the dispatch boundary.",
515
+ ),
516
+ false,
517
+ );
518
+ return await deferred.promise;
519
+ }
520
+ } catch (error) {
521
+ // Pre-send abort: the wire never saw this request, so retirement
522
+ // must not retain a sent record or classify it as uncertain. The
523
+ // caller's own rejection comes back unchanged so it stays
524
+ // retryable and distinguishable from transport failure.
525
+ this.#settlePending(
526
+ id,
527
+ pending,
528
+ error instanceof Error ? error : new SdkClientError("invalid_input", String(error), error),
529
+ false,
530
+ );
531
+ return await deferred.promise;
532
+ }
533
+ }
534
+ // `beforeDispatch` is arbitrary synchronous caller code: it may have closed
535
+ // the client, retired this incarnation, settled this pending entry, or run
536
+ // the deadline past its end. Nothing about the pre-call validation still
537
+ // holds, so revalidate all of it before the wire write and bookkeeping.
538
+ if (this.#closed || this.#pending.get(id) !== pending || !this.#isActive(incarnation)) {
539
+ if (this.#pending.get(id) === pending)
540
+ this.#settlePending(
541
+ id,
542
+ pending,
543
+ new SdkClientError("connection_closed", "SDK client closed during dispatch"),
544
+ );
545
+ return await deferred.promise;
546
+ }
547
+ if (this.#deadline !== undefined && Date.now() >= this.#deadline) {
548
+ this.#settlePending(id, pending, this.#deadlineError());
549
+ return await deferred.promise;
550
+ }
551
+ if (incarnation.socket.readyState !== WebSocket.OPEN) {
552
+ this.#settlePending(id, pending, new SdkClientError("unavailable", "SDK WebSocket is not connected"));
553
+ return await deferred.promise;
554
+ }
555
+ // Handoff bookkeeping is reentrancy-safe: `sent` flips BEFORE the wire
556
+ // write so a send that synchronously triggers a close event or response
557
+ // retires this request as already-sent (uncertain_after_send on close,
558
+ // never pre-send), and the sent record is retained up-front for the same
559
+ // reason. Only a synchronous send THROW rolls the pre-write state back:
560
+ // nothing reached the transport, so the request stays retryable and
561
+ // non-uncertain with no record retained.
562
+ pending.sent = true;
563
+ this.#rememberSentRecord({
564
+ id,
565
+ operation: sentOperation,
566
+ idempotencyKey: sentIdempotencyKey,
567
+ fingerprint: sentFingerprint,
568
+ });
400
569
  try {
401
570
  incarnation.socket.send(serializedRequest);
402
- pending.sent = true;
403
- this.#rememberSentRecord({
404
- id,
405
- operation: typeof serializedFrame.operation === "string" ? serializedFrame.operation : undefined,
406
- idempotencyKey: options.idempotencyKey,
407
- fingerprint:
408
- typeof serializedFrame.operation === "string"
409
- ? lifecycleFingerprint(serializedFrame.operation, serializedFrame.input ?? {})
410
- : inputFingerprint(serializedFrame.input ?? {}),
411
- });
412
571
  } catch (error) {
413
- this.#settlePending(
414
- id,
415
- pending,
416
- error instanceof SdkClientError
417
- ? error
418
- : new SdkClientError("unavailable", "SDK WebSocket send failed", error),
419
- );
572
+ if (this.#pending.get(id) === pending) {
573
+ // Nothing was accepted by the transport (the write threw before
574
+ // handoff), so this stays a retryable pre-send failure — unless a
575
+ // reentrant event already settled the request during the throw,
576
+ // in which case that settlement stands and must not be displaced.
577
+ pending.sent = false;
578
+ this.#sentRecords.delete(id);
579
+ this.#settlePending(
580
+ id,
581
+ pending,
582
+ error instanceof SdkClientError
583
+ ? error
584
+ : new SdkClientError("unavailable", "SDK WebSocket send failed", error),
585
+ );
586
+ }
587
+ return await deferred.promise;
588
+ }
589
+ // Reconciliation identity was retained above the send so reentrant
590
+ // close/response handling finds it; nothing further to record here.
591
+ try {
592
+ const observerResult = options.onDispatch?.({
593
+ frame: serializedFrame,
594
+ connectionId: this.connectionId,
595
+ generation: incarnation.generation,
596
+ });
597
+ if (isThenable(observerResult)) sinkThenable(observerResult);
598
+ } catch {
599
+ // The frame was already handed to the socket. An observer failure can
600
+ // neither un-send it nor displace settlement, so the request stays
601
+ // pending for its response, deadline, or transport-close retirement.
420
602
  }
421
603
  return await deferred.promise;
422
604
  }
@@ -1,6 +1,8 @@
1
1
  import { logger } from "@gajae-code/utils";
2
+ import { AUTOROUTING_INACTIVE_WARNING } from "../../config/autorouting-contract";
2
3
  import { redactBrokerRuntimeCloseCapability } from "./control/runtime-gate";
3
4
  import { type EventFrame, SessionEventStream } from "./events";
5
+ import { isAutoroutingInactive } from "./internal-autorouting-state";
4
6
  import { type ProviderLease, ReverseLeaseError, ReverseLeaseRuntime } from "./reverse-leases";
5
7
  import type { BrokerIndexWriter, HostEndpointAdapters, SdkFrame } from "./types";
6
8
 
@@ -77,6 +79,7 @@ export interface SessionSdkHostOptions extends HostEndpointAdapters {
77
79
  connectionCapabilities?: (connectionId: string) => ReadonlySet<string> | undefined;
78
80
  /** Readiness publication mode; defaults to the stock immediate contract. */
79
81
  readiness?: SessionReadinessMode;
82
+
80
83
  /**
81
84
  * Authorization for a deferred activation. It is consulted on every attempt
82
85
  * that would publish readiness and never on an idempotent replay, and a gate
@@ -247,9 +250,22 @@ export class SessionSdkHost {
247
250
  return this.events.emit(frame);
248
251
  }
249
252
 
253
+ emitAutoroutingInactiveNotice(): EventFrame {
254
+ return this.emitEvent({
255
+ kind: "notice",
256
+ payload: {
257
+ type: "notice",
258
+ level: "warning",
259
+ message: AUTOROUTING_INACTIVE_WARNING,
260
+ source: "autorouting",
261
+ },
262
+ });
263
+ }
264
+
250
265
  async start(): Promise<"started" | "already"> {
251
266
  if (this.#started) return "already";
252
267
  this.events.restart();
268
+ if (isAutoroutingInactive(this)) this.emitAutoroutingInactiveNotice();
253
269
  if (this.#options.readiness !== "deferred") this.#publishReadiness();
254
270
  else
255
271
  this.emitEvent({
@@ -0,0 +1,13 @@
1
+ const inactiveAutoroutingState = new WeakMap<object, boolean>();
2
+
3
+ export function markAutoroutingInactive(target: object): void {
4
+ inactiveAutoroutingState.set(target, true);
5
+ }
6
+
7
+ export function clearAutoroutingInactive(target: object): void {
8
+ inactiveAutoroutingState.delete(target);
9
+ }
10
+
11
+ export function isAutoroutingInactive(target: unknown): boolean {
12
+ return typeof target === "object" && target !== null && inactiveAutoroutingState.get(target) === true;
13
+ }
@@ -0,0 +1,101 @@
1
+ import * as path from "node:path";
2
+ import type { AuthStorage } from "@gajae-code/ai/core";
3
+ import { ModelRegistry } from "../../config/model-registry";
4
+ import {
5
+ formatModelSelectorValue,
6
+ formatModelString,
7
+ type ResolveCliModelResult,
8
+ resolveCliModel,
9
+ } from "../../config/model-resolver";
10
+ import { Settings } from "../../config/settings";
11
+ import { discoverAuthStorage } from "../session";
12
+
13
+ const MAX_ECHOED_MODEL_LENGTH = 256;
14
+
15
+ export interface SdkHostModelResolveContext {
16
+ cwd?: string;
17
+ }
18
+
19
+ /** Registry loader owned by the SDK host, not by machine-facing adapters. */
20
+ export type SdkHostModelRegistryLoader = (
21
+ context?: SdkHostModelResolveContext,
22
+ ) => ModelRegistry | Promise<ModelRegistry>;
23
+
24
+ /**
25
+ * Builds an offline model resolver for a single SDK host process.
26
+ *
27
+ * The registry is reused, but refreshed for every validation so additions and
28
+ * removals made while the broker is alive are observed without network I/O.
29
+ */
30
+ export function createSdkHostModelRegistryLoader(
31
+ discoverStorage: () => Promise<AuthStorage>,
32
+ modelsPath?: string,
33
+ loadSettings?: (context?: SdkHostModelResolveContext) => Promise<Pick<Settings, "get" | "getGlobal">>,
34
+ ): SdkHostModelRegistryLoader {
35
+ const cachedRegistries = new Map<string, Promise<ModelRegistry>>();
36
+ return async (context?: SdkHostModelResolveContext) => {
37
+ const scopeKey = context?.cwd ?? "";
38
+ let cachedRegistry = cachedRegistries.get(scopeKey);
39
+ if (cachedRegistry === undefined) {
40
+ const initializing = Promise.all([discoverStorage(), loadSettings?.(context)]).then(
41
+ ([storage, registrySettings]) => new ModelRegistry(storage, modelsPath, registrySettings),
42
+ );
43
+ cachedRegistries.set(scopeKey, initializing);
44
+ cachedRegistry = initializing;
45
+ try {
46
+ await initializing;
47
+ } catch (error) {
48
+ if (cachedRegistries.get(scopeKey) === initializing) cachedRegistries.delete(scopeKey);
49
+ throw error;
50
+ }
51
+ }
52
+ const registry = await cachedRegistry;
53
+ if (loadSettings) registry.setScopedSettings(await loadSettings(context));
54
+ await registry.refresh("offline");
55
+ return registry;
56
+ };
57
+ }
58
+
59
+ export type SdkHostModelResolution =
60
+ | { ok: true; model: string | null }
61
+ | { ok: false; reason: "unknown_model"; model: string; error: string };
62
+
63
+ export type SdkHostModelResolver = (
64
+ raw: unknown,
65
+ context?: SdkHostModelResolveContext,
66
+ ) => Promise<SdkHostModelResolution>;
67
+
68
+ /** Resolve the explicit model pin at the SDK host boundary. */
69
+ export async function resolveSdkHostModel(
70
+ raw: unknown,
71
+ loadRegistry: SdkHostModelRegistryLoader,
72
+ context?: SdkHostModelResolveContext,
73
+ ): Promise<SdkHostModelResolution> {
74
+ if (raw === undefined || raw === null) return { ok: true, model: null };
75
+ const requested = typeof raw === "string" ? raw : "";
76
+ const echoed = requested.trim().slice(0, MAX_ECHOED_MODEL_LENGTH);
77
+ const registry = await loadRegistry(context);
78
+ const resolved: ResolveCliModelResult = resolveCliModel({ cliModel: requested, modelRegistry: registry });
79
+ if (!resolved.model)
80
+ return {
81
+ ok: false,
82
+ reason: "unknown_model",
83
+ model: echoed,
84
+ error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
85
+ };
86
+ return {
87
+ ok: true,
88
+ model: formatModelSelectorValue(formatModelString(resolved.model), resolved.thinkingLevel),
89
+ };
90
+ }
91
+
92
+ /** Default resolver used by the SDK broker host. */
93
+ export function createDefaultSdkHostModelResolver(agentDir: string): SdkHostModelResolver {
94
+ const loadRegistry = createSdkHostModelRegistryLoader(
95
+ () => discoverAuthStorage(agentDir),
96
+ path.join(agentDir, "models.yml"),
97
+ context => Settings.loadReadonly({ agentDir, cwd: context?.cwd }),
98
+ );
99
+ return (raw: unknown, context?: SdkHostModelResolveContext): Promise<SdkHostModelResolution> =>
100
+ resolveSdkHostModel(raw, loadRegistry, context);
101
+ }
@@ -281,8 +281,9 @@ export class ReverseLeaseRuntime {
281
281
  throw new ReverseLeaseError("not_lease_owner");
282
282
  this.#takeOutstanding(id);
283
283
  if (error) {
284
- const rejection = new Error(error.message);
284
+ const rejection = new Error(error.message) as Error & { code: string };
285
285
  rejection.name = error.code;
286
+ rejection.code = error.code;
286
287
  request.reject(rejection);
287
288
  } else request.resolve(result);
288
289
  }
@@ -13,9 +13,11 @@ import {
13
13
  } from "../../session/terminal-abort";
14
14
  import { Broker } from "../broker/broker";
15
15
  import { createReconciliationStore } from "../bus/reconciliation-store";
16
+ import { CursorRegistry, QueryHandlers, RevisionStore } from "./query";
16
17
  import {
17
18
  createInvocationReconciliation,
18
19
  createSdkSessionRuntimeExtension,
20
+ createSdkSurfaceFactory,
19
21
  SessionSdkSessionRuntime,
20
22
  type SessionSdkTransport,
21
23
  } from "./session-runtime";
@@ -76,7 +78,15 @@ function admissionBarrier(target: number) {
76
78
  };
77
79
  }
78
80
 
79
- function extensionContext(sessionId: string, cwd: string): ExtensionContext {
81
+ function extensionContext(
82
+ sessionId: string,
83
+ cwd: string,
84
+ options: {
85
+ goalState?: unknown;
86
+ branch?: unknown[];
87
+ liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
88
+ } = {},
89
+ ): ExtensionContext {
80
90
  return {
81
91
  cwd,
82
92
  workflowGate: undefined,
@@ -85,10 +95,133 @@ function extensionContext(sessionId: string, cwd: string): ExtensionContext {
85
95
  getSessionId: () => sessionId,
86
96
  getSessionFile: () => path.join(cwd, `${sessionId}.json`),
87
97
  getSessionName: () => undefined,
98
+ getBranch: () => options.branch ?? [],
88
99
  },
100
+ getGoalState: () => options.goalState,
89
101
  } as unknown as ExtensionContext;
90
102
  }
91
103
 
104
+ function goalModeEntry(sessionId: string, tokensUsed: number): Record<string, unknown> {
105
+ return {
106
+ type: "mode_change",
107
+ id: `goal-${sessionId}`,
108
+ parentId: null,
109
+ timestamp: new Date(1_000).toISOString(),
110
+ mode: "goal",
111
+ data: {
112
+ goal: {
113
+ id: `goal-${sessionId}`,
114
+ objective: "Preserve the active SDK goal",
115
+ status: "active",
116
+ tokensUsed,
117
+ timeUsedSeconds: 2,
118
+ createdAt: 1_000,
119
+ updatedAt: 2_000,
120
+ },
121
+ },
122
+ };
123
+ }
124
+
125
+ async function queryGoalState(
126
+ ctx: ExtensionContext,
127
+ sessionId: string,
128
+ liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number },
129
+ ): Promise<unknown> {
130
+ const surface = createSdkSurfaceFactory({
131
+ ctx,
132
+ id: sessionId,
133
+ api: {} as ExtensionAPI,
134
+ getLiveState: () => liveState ?? { isStreaming: false, steeringQueueDepth: 0, followupQueueDepth: 0 },
135
+ }).query;
136
+ const store = new RevisionStore(sessionId);
137
+ const cursors = new CursorRegistry("goal-test-token", store);
138
+ const response = await new QueryHandlers(surface, sessionId, store, cursors).dispatch({
139
+ query: "goal.list/get",
140
+ connectionId: "goal-test-connection",
141
+ });
142
+ if (!response.ok) throw new Error(`goal query failed: ${JSON.stringify(response)}`);
143
+ return response.page?.items[0];
144
+ }
145
+
146
+ describe("SDK goal snapshot lifecycle", () => {
147
+ test("recovers an active nonzero-activity goal from the durable session projection", async () => {
148
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-recovery-"));
149
+ try {
150
+ const sessionId = "runtime-recreated";
151
+ const branch = [goalModeEntry(sessionId, 17)];
152
+ const liveGoal = {
153
+ enabled: true,
154
+ mode: "active",
155
+ goal: { ...(branch[0] as { data: { goal: Record<string, unknown> } }).data.goal },
156
+ };
157
+ const live = await queryGoalState(
158
+ extensionContext(sessionId, cwd, { goalState: liveGoal, branch }),
159
+ sessionId,
160
+ { isStreaming: true, steeringQueueDepth: 1, followupQueueDepth: 0 },
161
+ );
162
+ const recreated = await queryGoalState(extensionContext(sessionId, cwd, { branch }), sessionId);
163
+
164
+ expect(live).toMatchObject({ enabled: true, goal: { status: "active", tokensUsed: 17 } });
165
+ expect(recreated).toEqual(live);
166
+ } finally {
167
+ await rm(cwd, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ test("keeps fresh session/worktree projections isolated while an active turn has activity", async () => {
172
+ const root = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-isolation-"));
173
+ try {
174
+ const first = await queryGoalState(
175
+ extensionContext("fresh-session-a", path.join(root, "a"), {
176
+ branch: [goalModeEntry("fresh-session-a", 11)],
177
+ }),
178
+ "fresh-session-a",
179
+ );
180
+ const second = await queryGoalState(
181
+ extensionContext("fresh-session-b", path.join(root, "b"), {
182
+ branch: [goalModeEntry("fresh-session-b", 23)],
183
+ }),
184
+ "fresh-session-b",
185
+ );
186
+
187
+ expect(first).toMatchObject({ goal: { id: "goal-fresh-session-a", tokensUsed: 11 } });
188
+ expect(second).toMatchObject({ goal: { id: "goal-fresh-session-b", tokensUsed: 23 } });
189
+ expect(first).not.toEqual(second);
190
+ } finally {
191
+ await rm(root, { recursive: true, force: true });
192
+ }
193
+ });
194
+
195
+ test("returns an explicit no-active-goal diagnostic for the zero-activity control", async () => {
196
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-empty-"));
197
+ try {
198
+ const result = await queryGoalState(extensionContext("zero-activity", cwd), "zero-activity");
199
+ expect(result).toMatchObject({ enabled: false, goal: null, reason: "no_active_goal" });
200
+ } finally {
201
+ await rm(cwd, { recursive: true, force: true });
202
+ }
203
+ });
204
+
205
+ test("returns a recoverable diagnostic when durable active-goal state is malformed", async () => {
206
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-corrupt-"));
207
+ try {
208
+ const result = await queryGoalState(
209
+ extensionContext("corrupt-goal", cwd, {
210
+ branch: [{ ...goalModeEntry("corrupt-goal", 19), data: { goal: { status: "active" } } }],
211
+ }),
212
+ "corrupt-goal",
213
+ );
214
+ expect(result).toMatchObject({
215
+ reason: "goal_state_unavailable",
216
+ recoverable: true,
217
+ goal: null,
218
+ });
219
+ } finally {
220
+ await rm(cwd, { recursive: true, force: true });
221
+ }
222
+ });
223
+ });
224
+
92
225
  test("preserves an agent failure code in host prompt reconciliation", async () => {
93
226
  const reconciliation = createInvocationReconciliation();
94
227
  const correlation = { commandId: "failed-command", turnId: "failed-turn" };
@@ -2534,6 +2667,96 @@ describe("post-acceptance invocation terminalization", () => {
2534
2667
  await rm(cwd, { recursive: true, force: true });
2535
2668
  }
2536
2669
  });
2670
+ test("immediate prompt after abort ack is not terminalized by the aborted turn's delayed agent_end", async () => {
2671
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-abort-immediate-prompt-"));
2672
+ try {
2673
+ const firstInflight = Promise.withResolvers<void>();
2674
+ const successorStarted = Promise.withResolvers<void>();
2675
+ let prompts = 0;
2676
+ const harness = await invocationHarness("abort-immediate-prompt", cwd, {
2677
+ sendUserMessage: async (_content, options) => {
2678
+ prompts += 1;
2679
+ await options?.onPreflightAcceptCommit?.();
2680
+ if (prompts === 1) {
2681
+ await firstInflight.promise;
2682
+ return;
2683
+ }
2684
+ successorStarted.resolve();
2685
+ await Promise.withResolvers<void>().promise;
2686
+ },
2687
+ });
2688
+ const first = await harness.control("turn.prompt", { text: "first" });
2689
+ expect(first.ok).toBe(true);
2690
+ const firstIds = { commandId: first.result?.commandId, turnId: first.result?.turnId };
2691
+ await harness.emit("agent_start");
2692
+ expect(await harness.control("turn.abort", {})).toMatchObject({ ok: true });
2693
+ firstInflight.reject(Object.assign(new Error("turn aborted"), { code: "aborted" }));
2694
+ const second = await harness.control("turn.prompt", { text: "successor" });
2695
+ expect(second.ok).toBe(true);
2696
+ const secondIds = { commandId: second.result?.commandId, turnId: second.result?.turnId };
2697
+ expect(secondIds.commandId).not.toBe(firstIds.commandId);
2698
+ expect(secondIds.turnId).not.toBe(firstIds.turnId);
2699
+ await harness.emit("agent_start");
2700
+ await successorStarted.promise;
2701
+ await harness.emit("agent_end");
2702
+ expect(await harness.query("turn.prompt_status", secondIds)).toMatchObject({
2703
+ result: { status: expect.stringMatching(/accepted|in_flight/) },
2704
+ });
2705
+ expect(await settledStatus(harness, "turn.prompt_status", firstIds)).toMatchObject({
2706
+ status: "failed",
2707
+ error: { code: "aborted" },
2708
+ });
2709
+ await harness.emit("agent_end");
2710
+ expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
2711
+ status: "terminal_ok",
2712
+ });
2713
+ expect(prompts).toBe(2);
2714
+ await harness.stop();
2715
+ } finally {
2716
+ await Bun.sleep(50);
2717
+ await rm(cwd, { recursive: true, force: true });
2718
+ }
2719
+ });
2720
+
2721
+ test("abort_and_prompt starts exactly one successor and is not duplicated by delayed abort teardown", async () => {
2722
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-abort-and-prompt-once-"));
2723
+ try {
2724
+ const firstInflight = Promise.withResolvers<void>();
2725
+ const abortReleased = Promise.withResolvers<void>();
2726
+ let prompts = 0;
2727
+ const harness = await invocationHarness("abort-and-prompt-once", cwd, {
2728
+ sendUserMessage: async (_content, options) => {
2729
+ prompts += 1;
2730
+ await options?.onPreflightAcceptCommit?.();
2731
+ if (prompts === 1) {
2732
+ await firstInflight.promise;
2733
+ return;
2734
+ }
2735
+ },
2736
+ abort: () => abortReleased.promise,
2737
+ });
2738
+ const first = await harness.control("turn.prompt", { text: "first" });
2739
+ expect(first.ok).toBe(true);
2740
+ await harness.emit("agent_start");
2741
+ const replacement = harness.control("turn.abort_and_prompt", { text: "replacement" });
2742
+ firstInflight.reject(Object.assign(new Error("turn aborted"), { code: "aborted" }));
2743
+ await harness.emit("agent_end");
2744
+ abortReleased.resolve();
2745
+ const accepted = await replacement;
2746
+ expect(accepted.ok).toBe(true);
2747
+ const successorIds = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
2748
+ await harness.emit("agent_start");
2749
+ await harness.emit("agent_end");
2750
+ expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
2751
+ status: "terminal_ok",
2752
+ });
2753
+ expect(prompts).toBe(2);
2754
+ await harness.stop();
2755
+ } finally {
2756
+ await Bun.sleep(50);
2757
+ await rm(cwd, { recursive: true, force: true });
2758
+ }
2759
+ });
2537
2760
 
2538
2761
  test("a failed skill invocation still reports a terminal failed status", async () => {
2539
2762
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-skill-"));