@gajae-code/coding-agent 0.11.0 → 0.11.2

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 (418) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/cli/config-cli.d.ts +11 -1
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +56 -1
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  54. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  55. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  56. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  57. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  58. package/dist/types/goals/runtime.d.ts +1 -0
  59. package/dist/types/goals/state.d.ts +8 -0
  60. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  61. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  62. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  63. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  64. package/dist/types/hindsight/state.d.ts +11 -4
  65. package/dist/types/lsp/lspmux.d.ts +5 -3
  66. package/dist/types/lsp/path-trust.d.ts +1 -0
  67. package/dist/types/main.d.ts +3 -2
  68. package/dist/types/memories/storage.d.ts +1 -0
  69. package/dist/types/modes/action-registry.d.ts +29 -0
  70. package/dist/types/modes/components/chrome.d.ts +14 -0
  71. package/dist/types/modes/components/command-palette.d.ts +27 -0
  72. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +2 -1
  74. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  75. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  76. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  77. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  78. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  79. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  80. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  81. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  82. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  83. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  84. package/dist/types/modes/components/status-line.d.ts +1 -94
  85. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  86. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  87. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  88. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  89. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  90. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  91. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  92. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  93. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  94. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  95. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  96. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  97. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  98. package/dist/types/modes/interactive-mode.d.ts +21 -19
  99. package/dist/types/modes/jobs-observer.d.ts +2 -0
  100. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  101. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
  102. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  103. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  104. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  105. package/dist/types/modes/types.d.ts +22 -15
  106. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  107. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  108. package/dist/types/sdk/broker/broker.d.ts +78 -12
  109. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  110. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  111. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  112. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  113. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  114. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  115. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  116. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  117. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  120. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  121. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  122. package/dist/types/sdk/bus/index.d.ts +27 -0
  123. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  124. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  125. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  126. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  127. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  128. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  129. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  130. package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
  131. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  132. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  133. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  134. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  135. package/dist/types/sdk/host/host.d.ts +2 -0
  136. package/dist/types/sdk/index.d.ts +1 -0
  137. package/dist/types/sdk/session-directory.d.ts +56 -0
  138. package/dist/types/session/agent-session.d.ts +82 -47
  139. package/dist/types/session/agent-storage.d.ts +7 -0
  140. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  141. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  142. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  143. package/dist/types/session/messages.d.ts +16 -0
  144. package/dist/types/session/session-manager-internal.d.ts +14 -0
  145. package/dist/types/session/session-manager.d.ts +82 -12
  146. package/dist/types/session/session-storage.d.ts +43 -3
  147. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  148. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  149. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  150. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  151. package/dist/types/system-prompt.d.ts +2 -11
  152. package/dist/types/task/executor.d.ts +3 -1
  153. package/dist/types/thinking.d.ts +7 -0
  154. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  155. package/dist/types/tools/ask.d.ts +64 -2
  156. package/dist/types/tools/bash.d.ts +3 -0
  157. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  158. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  159. package/dist/types/tools/cron.d.ts +2 -0
  160. package/dist/types/tools/fetch.d.ts +1 -0
  161. package/dist/types/tools/index.d.ts +30 -15
  162. package/dist/types/tools/output-meta.d.ts +1 -0
  163. package/dist/types/tools/read.d.ts +3 -0
  164. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  165. package/dist/types/tools/subagent-render.d.ts +1 -0
  166. package/dist/types/tools/subagent.d.ts +6 -1
  167. package/dist/types/utils/event-bus.d.ts +1 -1
  168. package/dist/types/web/search/index.d.ts +3 -1
  169. package/package.json +21 -9
  170. package/scripts/build-sdk-package-smoke.ts +39 -1
  171. package/scripts/generate-hotkeys-docs.ts +74 -0
  172. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  173. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  174. package/src/async/job-manager.ts +455 -69
  175. package/src/cli/config-cli.ts +75 -2
  176. package/src/cli/notify-cli.ts +92 -26
  177. package/src/cli/session-picker.ts +20 -3
  178. package/src/cli/setup-cli.ts +1 -1
  179. package/src/cli.ts +1 -1
  180. package/src/commands/config.ts +1 -1
  181. package/src/commands/harness.ts +34 -106
  182. package/src/commands/launch.ts +6 -4
  183. package/src/commands/mcp-serve.ts +85 -8
  184. package/src/commands/mcp.ts +2 -2
  185. package/src/commands/notify.ts +8 -4
  186. package/src/commands/sdk.ts +61 -40
  187. package/src/commands/ultragoal.ts +4 -1
  188. package/src/config/atomic-yaml-patch.ts +80 -13
  189. package/src/config/file-lock.ts +105 -43
  190. package/src/config/keybindings.ts +64 -20
  191. package/src/config/model-auth.ts +9 -0
  192. package/src/config/model-bindings-applier.ts +139 -0
  193. package/src/config/model-discovery-manager.ts +224 -0
  194. package/src/config/model-equivalence.ts +66 -0
  195. package/src/config/model-registry.ts +169 -302
  196. package/src/config/model-resolver.ts +361 -369
  197. package/src/config/models-config-schema.ts +5 -4
  198. package/src/config/settings-schema.ts +159 -2
  199. package/src/config/settings.ts +458 -482
  200. package/src/coordinator-mcp/policy.ts +76 -11
  201. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  202. package/src/coordinator-mcp/question-state.ts +778 -0
  203. package/src/coordinator-mcp/server.ts +1702 -238
  204. package/src/debug/index.ts +1 -1
  205. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  206. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  207. package/src/discovery/helpers.ts +1 -50
  208. package/src/edit/index.ts +64 -0
  209. package/src/eval/py/env.ts +26 -0
  210. package/src/eval/py/kernel.ts +5 -3
  211. package/src/extensibility/extensions/loader.ts +9 -1
  212. package/src/extensibility/extensions/runner.ts +12 -3
  213. package/src/extensibility/extensions/types.ts +39 -1
  214. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  215. package/src/extensibility/hooks/runner.ts +2 -2
  216. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  217. package/src/extensibility/shared-events.ts +2 -0
  218. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  219. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  220. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  221. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  222. package/src/gjc-runtime/launch-tmux.ts +2 -5
  223. package/src/gjc-runtime/launch-worktree.ts +5 -5
  224. package/src/gjc-runtime/linux-proc.ts +71 -0
  225. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  226. package/src/gjc-runtime/session-resolution.ts +18 -3
  227. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  228. package/src/gjc-runtime/state-migrations.ts +1 -1
  229. package/src/gjc-runtime/state-runtime.ts +320 -249
  230. package/src/gjc-runtime/state-writer.ts +36 -40
  231. package/src/gjc-runtime/team-launch.ts +319 -0
  232. package/src/gjc-runtime/team-notify.ts +326 -0
  233. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  234. package/src/gjc-runtime/team-store.ts +828 -0
  235. package/src/gjc-runtime/team-workers.ts +962 -0
  236. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  237. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  238. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  239. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  240. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  241. package/src/gjc-runtime/ultragoal-guard.ts +83 -12
  242. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  243. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  244. package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
  245. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  246. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  247. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  248. package/src/goals/continuation-timeout-guard.ts +79 -0
  249. package/src/goals/runtime.ts +7 -5
  250. package/src/goals/state.ts +18 -0
  251. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  252. package/src/harness-control-plane/phase-rollup.ts +4 -7
  253. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  254. package/src/harness-control-plane/receipt-spool.ts +51 -7
  255. package/src/harness-control-plane/session-lease.ts +12 -130
  256. package/src/hindsight/backend.ts +26 -27
  257. package/src/hindsight/state.ts +101 -32
  258. package/src/hooks/skill-state.ts +17 -11
  259. package/src/internal-urls/docs-index.generated.ts +21 -18
  260. package/src/lsp/client.ts +1 -1
  261. package/src/lsp/config.ts +145 -26
  262. package/src/lsp/index.ts +1 -1
  263. package/src/lsp/lspmux.ts +39 -10
  264. package/src/lsp/path-trust.ts +81 -0
  265. package/src/main.ts +28 -8
  266. package/src/memories/index.ts +1 -0
  267. package/src/memories/storage.ts +6 -2
  268. package/src/modes/action-registry.ts +165 -0
  269. package/src/modes/components/chrome.ts +40 -0
  270. package/src/modes/components/command-palette.ts +168 -0
  271. package/src/modes/components/custom-editor.ts +1 -1
  272. package/src/modes/components/custom-provider-wizard.ts +51 -14
  273. package/src/modes/components/index.ts +2 -1
  274. package/src/modes/components/pet-selector.ts +11 -17
  275. package/src/modes/components/plan-preview-overlay.ts +295 -0
  276. package/src/modes/components/queue-mode-selector.ts +10 -27
  277. package/src/modes/components/queue-pane.ts +79 -0
  278. package/src/modes/components/read-tool-group.ts +13 -0
  279. package/src/modes/components/session-observer-overlay.ts +207 -772
  280. package/src/modes/components/sessions-dashboard.ts +181 -0
  281. package/src/modes/components/settings-selector.ts +1 -1
  282. package/src/modes/components/show-images-selector.ts +10 -24
  283. package/src/modes/components/status-line/types.ts +1 -1
  284. package/src/modes/components/status-line.ts +1 -969
  285. package/src/modes/components/tasks-pane.ts +73 -0
  286. package/src/modes/components/theme-selector.ts +11 -31
  287. package/src/modes/components/tool-execution.ts +22 -0
  288. package/src/modes/components/tool-status-header.ts +1056 -0
  289. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  290. package/src/modes/controllers/btw-controller.ts +1 -0
  291. package/src/modes/controllers/command-controller.ts +9 -15
  292. package/src/modes/controllers/event-controller.ts +3 -2
  293. package/src/modes/controllers/extension-ui-controller.ts +72 -25
  294. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  295. package/src/modes/controllers/input-controller.ts +764 -229
  296. package/src/modes/controllers/mode-gate.ts +24 -0
  297. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  298. package/src/modes/controllers/selector-controller.ts +225 -10
  299. package/src/modes/controllers/stt-controller.ts +125 -0
  300. package/src/modes/interactive-mode.ts +302 -1151
  301. package/src/modes/jobs-observer.ts +3 -0
  302. package/src/modes/runtime-init.ts +8 -0
  303. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  304. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
  305. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  306. package/src/modes/tasks-aggregator.ts +199 -0
  307. package/src/modes/transcript-item-registry.ts +229 -0
  308. package/src/modes/types.ts +47 -20
  309. package/src/modes/utils/ui-helpers.ts +17 -9
  310. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  311. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  312. package/src/prompts/agents/architect.md +2 -8
  313. package/src/prompts/agents/critic.md +2 -8
  314. package/src/prompts/agents/executor.md +3 -0
  315. package/src/prompts/agents/planner.md +4 -12
  316. package/src/prompts/system/custom-system-prompt.md +1 -23
  317. package/src/prompts/system/plan-mode-approved.md +8 -0
  318. package/src/prompts/system/system-prompt.md +17 -96
  319. package/src/prompts/tools/bash.md +4 -4
  320. package/src/prompts/tools/browser.md +15 -52
  321. package/src/prompts/tools/find.md +2 -2
  322. package/src/prompts/tools/hashline.md +13 -55
  323. package/src/prompts/tools/read.md +3 -6
  324. package/src/prompts/tools/search-tool-bm25.md +0 -2
  325. package/src/prompts/tools/search.md +2 -4
  326. package/src/prompts/tools/skill-discovery.md +1 -2
  327. package/src/prompts/tools/task.md +1 -1
  328. package/src/runtime-mcp/manager.ts +1 -1
  329. package/src/sdk/broker/broker.ts +206 -78
  330. package/src/sdk/broker/discovery.ts +21 -2
  331. package/src/sdk/broker/ensure.ts +85 -25
  332. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  333. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  334. package/src/sdk/broker/lifecycle.ts +1399 -142
  335. package/src/sdk/broker/process-incarnation.ts +5 -13
  336. package/src/sdk/broker/session-index.ts +145 -56
  337. package/src/sdk/broker/state-version.ts +21 -0
  338. package/src/sdk/broker/transport.ts +1 -1
  339. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  340. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  341. package/src/sdk/bus/chat-effect-journal.ts +14 -4
  342. package/src/sdk/bus/config-commands.ts +5 -4
  343. package/src/sdk/bus/config.ts +98 -0
  344. package/src/sdk/bus/conversation-store.ts +5 -29
  345. package/src/sdk/bus/daemon-paths.ts +2 -0
  346. package/src/sdk/bus/discord-daemon.ts +25 -8
  347. package/src/sdk/bus/html-format.ts +20 -3
  348. package/src/sdk/bus/index.ts +834 -113
  349. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  350. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  351. package/src/sdk/bus/natives-augment.d.ts +26 -1
  352. package/src/sdk/bus/notification-service.ts +168 -20
  353. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  354. package/src/sdk/bus/recent-activity.ts +188 -74
  355. package/src/sdk/bus/rich-document.ts +258 -0
  356. package/src/sdk/bus/rich-render.ts +62 -70
  357. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  358. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  359. package/src/sdk/bus/telegram-daemon.ts +1491 -247
  360. package/src/sdk/bus/threaded-inbound.ts +8 -3
  361. package/src/sdk/bus/threaded-render.ts +57 -1
  362. package/src/sdk/bus/topic-registry.ts +120 -13
  363. package/src/sdk/host/control/dispatch.ts +14 -2
  364. package/src/sdk/host/control/operations.ts +6 -1
  365. package/src/sdk/host/host.ts +31 -1
  366. package/src/sdk/host/query/handlers.ts +20 -22
  367. package/src/sdk/host/query/revision-store.ts +80 -57
  368. package/src/sdk/index.ts +1 -0
  369. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  370. package/src/sdk/session-directory.ts +141 -0
  371. package/src/sdk/session.ts +458 -479
  372. package/src/session/agent-session.ts +1901 -637
  373. package/src/session/agent-storage.ts +18 -0
  374. package/src/session/cache-economics.ts +25 -8
  375. package/src/session/fallback-chain-controller.ts +53 -3
  376. package/src/session/internal/managed-session-scope.ts +2226 -0
  377. package/src/session/internal/managed-session-storage.ts +453 -0
  378. package/src/session/messages.ts +91 -2
  379. package/src/session/session-manager-internal.ts +33 -0
  380. package/src/session/session-manager.ts +1112 -333
  381. package/src/session/session-storage.ts +444 -39
  382. package/src/session/volatile-context-pruning.ts +78 -0
  383. package/src/setup/credential-auto-import.ts +2 -1
  384. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  385. package/src/setup/provider-onboarding.ts +36 -61
  386. package/src/setup/provider-presets.json +5 -0
  387. package/src/skill-state/active-state.ts +3 -13
  388. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  389. package/src/skill-state/workflow-state-contract.ts +6 -41
  390. package/src/slash-commands/builtin-registry.ts +46 -11
  391. package/src/system-prompt.ts +37 -44
  392. package/src/task/agents.ts +19 -1
  393. package/src/task/executor.ts +90 -6
  394. package/src/task/index.ts +62 -37
  395. package/src/thinking.ts +21 -6
  396. package/src/tool-discovery/tool-index.ts +2 -134
  397. package/src/tools/ask.ts +157 -21
  398. package/src/tools/ast-edit.ts +1 -1
  399. package/src/tools/bash.ts +38 -0
  400. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  401. package/src/tools/browser/tab-supervisor.ts +282 -36
  402. package/src/tools/computer.ts +10 -3
  403. package/src/tools/cron.ts +30 -16
  404. package/src/tools/eval.ts +1 -2
  405. package/src/tools/fetch.ts +11 -4
  406. package/src/tools/index.ts +76 -24
  407. package/src/tools/output-meta.ts +13 -5
  408. package/src/tools/read.ts +37 -1
  409. package/src/tools/resource-gc.ts +2 -2
  410. package/src/tools/search-tool-bm25.ts +11 -74
  411. package/src/tools/subagent-render.ts +19 -8
  412. package/src/tools/subagent.ts +84 -32
  413. package/src/utils/event-bus.ts +9 -4
  414. package/src/web/search/index.ts +3 -3
  415. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  416. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  417. package/src/discovery/plugin-dir-roots.ts +0 -28
  418. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,7 +1,12 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { resolveResumableSession, SessionManager } from "../../session/session-manager";
4
+ import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
5
+ import {
6
+ type DirectoryMigrationPolicy,
7
+ listManagedSessionCandidates,
8
+ resolveManagedSessionScope,
9
+ } from "../session-directory";
5
10
  import {
6
11
  BROKER_HEARTBEAT_TTL_MS,
7
12
  type BrokerDiscovery,
@@ -9,6 +14,7 @@ import {
9
14
  brokerProcessIncarnation,
10
15
  isPidAlive,
11
16
  newBrokerToken,
17
+ type RedactedBrokerDiscovery,
12
18
  readBrokerDiscovery,
13
19
  redactBrokerDiscovery,
14
20
  writeBrokerDiscovery,
@@ -20,6 +26,7 @@ import {
20
26
  type LifecycleDurableEffectsReceipt,
21
27
  LifecycleLedger,
22
28
  type LifecycleStartupFailureReceipt,
29
+ type LifecycleState,
23
30
  } from "./lifecycle-ledger";
24
31
  import { type IndexedSession, SessionIndex } from "./session-index";
25
32
  import { BrokerTransport } from "./transport";
@@ -29,7 +36,18 @@ export interface BrokerSettings {
29
36
  packageGeneration?: string;
30
37
  port?: number;
31
38
  heartbeatTtlMs?: number;
39
+ /** Broker-owned migration policy. Client lifecycle frames cannot select it. */
40
+ resolveDirectoryMigration?: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
32
41
  }
42
+
43
+ type ResolvedBrokerSettings = {
44
+ agentDir: string;
45
+ packageGeneration: string;
46
+ port: number;
47
+ heartbeatTtlMs: number;
48
+ resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
49
+ };
50
+
33
51
  export type BrokerErrorCode =
34
52
  | "idempotency_conflict"
35
53
  | "terminal_uncertain"
@@ -46,10 +64,73 @@ export type BrokerErrorCode =
46
64
  | "cleanup_pending"
47
65
  | (string & {});
48
66
 
67
+ export type BrokerCleanupIdentity = {
68
+ dev: string;
69
+ ino: string;
70
+ size: number;
71
+ mtimeNs: string;
72
+ sha256: string;
73
+ };
74
+
75
+ /** Exact retry evidence; detached paths are managed-receipt references, never caller authority. */
76
+ export type BrokerLifecycleCleanupFile = {
77
+ /** Original lifecycle-owned path, retained only for exact identity validation. */
78
+ path: string;
79
+ identity: BrokerCleanupIdentity;
80
+ /** Monotonic append-only cleanup attempt. */
81
+ attempt?: number;
82
+ /** Immutable no-replace quarantine destination persisted before native detach. */
83
+ plannedPath: string;
84
+ /** Native-returned detached path, persisted after a failed post-detach cleanup. */
85
+ detachedPath?: string;
86
+ /** Append-only terminal proof for this exact artifact; completed entries are never retried. */
87
+ completed?: true;
88
+ };
89
+
90
+ /** Durable root-tree authority for broker artifact cleanup. */
91
+ export type BrokerArtifactTree = {
92
+ identity: BrokerCleanupIdentity;
93
+ snapshot: NativeDirectoryTreeSnapshot;
94
+ plannedPath: string;
95
+ detachedPath?: string;
96
+ completed?: true;
97
+ };
98
+
49
99
  export type BrokerCleanupEvidence = {
50
- phase: "artifacts" | "transcript" | "metadata";
51
- artifactsIdentity?: { dev: string; ino: string };
52
- transcriptIdentity?: { dev: string; ino: string };
100
+ phase: "artifacts" | "transcript" | "metadata" | "lifecycle";
101
+ /** Ledger-bound deletion target; never reconstructed from a retry request. */
102
+ sessionsRoot?: string;
103
+ transcriptPath?: string;
104
+ cwd?: string;
105
+ metadataRoot?: string;
106
+ sessionId?: string;
107
+ artifactsIdentity?: BrokerCleanupIdentity;
108
+ transcriptIdentity?: BrokerCleanupIdentity;
109
+ /** Identity-bound lifecycle metadata marker retained when exact cleanup is deferred. */
110
+ metadataIdentity?: BrokerCleanupIdentity;
111
+ metadataPath?: string;
112
+ /** Monotonic append-only cleanup attempt. */
113
+ metadataAttempt?: number;
114
+ /** No-replace quarantine destination persisted before lifecycle metadata detach. */
115
+ plannedMetadataPath?: string;
116
+ /** Native-returned metadata quarantine path retained until identity-bound reconciliation succeeds. */
117
+ detachedMetadataPath?: string;
118
+ /** Append-only terminal proof for lifecycle metadata cleanup. */
119
+ metadataCompleted?: true;
120
+ detachedArtifactsPath?: string;
121
+ detachedTranscriptPath?: string;
122
+ /** Durable proof that artifact cleanup completed before transcript mutation. */
123
+ artifactsRemoved?: boolean;
124
+ /** Preauthorized no-replace artifact quarantine path persisted before detach. */
125
+ plannedArtifactsPath?: string;
126
+ /** Identity-bound artifact tree authority persisted before broker detach and replayed exactly. */
127
+ artifactTree?: BrokerArtifactTree;
128
+ /** Preauthorized no-replace transcript quarantine path persisted before detach. */
129
+ plannedTranscriptPath?: string;
130
+ /** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */
131
+ lifecycleFiles?: BrokerLifecycleCleanupFile[];
132
+ /** Delete metadata receipts authorize only the canonical marker/ready sibling pair. */
133
+ lifecycleDeleteMetadata?: true;
53
134
  };
54
135
  export type BrokerResponse =
55
136
  | { ok: true; result?: unknown; indexSeq?: number }
@@ -67,6 +148,16 @@ export type BrokerResponse =
67
148
  };
68
149
  const error = (code: BrokerErrorCode, message: string): BrokerResponse => ({ ok: false, error: { code, message } });
69
150
 
151
+ function isCleanupPending(response: BrokerResponse): boolean {
152
+ return !response.ok && response.error.code === "cleanup_pending" && response.error.cleanup !== undefined;
153
+ }
154
+
155
+ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
156
+ if (response.ok) return "terminal_ok";
157
+ if (isCleanupPending(response)) return "effect_started";
158
+ return response.error.code === "terminal_uncertain" ? "terminal_uncertain" : "terminal_error";
159
+ }
160
+
70
161
  type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
71
162
 
72
163
  function isBrokerResponse(value: InputNormalization): value is BrokerResponse {
@@ -106,6 +197,8 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
106
197
  normalized.sourceSessionId = source.value;
107
198
  delete normalized.sourceId;
108
199
  }
200
+ if (input.directoryMigration !== undefined)
201
+ return error("invalid_input", "directoryMigration is broker-managed and cannot be selected by clients.");
109
202
 
110
203
  if (operation === "session.list") {
111
204
  const resolved = input.resolveSessionId;
@@ -272,7 +365,7 @@ type BrokerLockSnapshot = {
272
365
  const terminalPersistenceHooksForTest = new WeakMap<Broker, () => void>();
273
366
 
274
367
  export class Broker {
275
- readonly settings: Required<BrokerSettings>;
368
+ readonly settings: ResolvedBrokerSettings;
276
369
  readonly index: SessionIndex;
277
370
  readonly ledger: LifecycleLedger;
278
371
  discovery: BrokerDiscovery | null = null;
@@ -281,10 +374,16 @@ export class Broker {
281
374
  #chains = new Map<string, Promise<void>>();
282
375
  #stopping = false;
283
376
  #transport: BrokerTransport | null = null;
284
- #heartbeatTimer: ReturnType<typeof setInterval> | null = null;
377
+ #heartbeatTimer: NodeJS.Timeout | null = null;
285
378
  #heartbeatWrite: Promise<void> = Promise.resolve();
286
379
  constructor(settings: BrokerSettings) {
287
- this.settings = { packageGeneration: "unknown", port: 0, heartbeatTtlMs: BROKER_HEARTBEAT_TTL_MS, ...settings };
380
+ this.settings = {
381
+ agentDir: settings.agentDir,
382
+ packageGeneration: settings.packageGeneration ?? "unknown",
383
+ port: settings.port ?? 0,
384
+ heartbeatTtlMs: settings.heartbeatTtlMs ?? BROKER_HEARTBEAT_TTL_MS,
385
+ resolveDirectoryMigration: settings.resolveDirectoryMigration ?? (async () => "copy-retain"),
386
+ };
288
387
  this.index = new SessionIndex(settings.agentDir);
289
388
  this.ledger = new LifecycleLedger(settings.agentDir);
290
389
  this.#lock = path.join(settings.agentDir, "sdk", "broker.lock");
@@ -379,13 +478,20 @@ export class Broker {
379
478
  throw e;
380
479
  }
381
480
  }
481
+ async #releaseOwnedLock(): Promise<void> {
482
+ try {
483
+ const lock = await this.#readLock();
484
+ if (lock?.ownerId !== this.#owner) return;
485
+ await fs.unlink(this.#lockRecordPath());
486
+ await fs.rmdir(this.#lock);
487
+ } catch (error) {
488
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
489
+ }
490
+ }
491
+
382
492
  async start(): Promise<BrokerDiscovery> {
383
493
  this.#stopping = false;
384
- await Promise.all([
385
- this.index.assertSupportedStateVersions(),
386
- this.ledger.assertSupportedStateVersions(),
387
- readBrokerDiscovery(this.settings.agentDir),
388
- ]);
494
+ await Promise.all([this.ledger.assertSupportedStateVersions(), readBrokerDiscovery(this.settings.agentDir)]);
389
495
  await fs.mkdir(path.dirname(this.#lock), { recursive: true, mode: 0o700 });
390
496
  for (;;) {
391
497
  try {
@@ -415,39 +521,47 @@ export class Broker {
415
521
  }
416
522
  await this.#reclaimStaleLock(snapshot);
417
523
  }
418
- await this.index.open();
419
- await this.ledger.open();
420
- const now = Date.now();
421
- const incarnation = brokerProcessIncarnation(process.pid);
422
- if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
423
- const token = newBrokerToken();
424
- this.#transport = new BrokerTransport(this, token, this.settings.port);
425
- const port = await this.#transport.start();
426
- this.discovery = {
427
- version: 1,
428
- protocolVersion: 3,
429
- packageGeneration: this.settings.packageGeneration,
430
- ownerId: this.#owner,
431
- pid: process.pid,
432
- incarnation,
433
- host: "127.0.0.1",
434
- port,
435
- url: `ws://127.0.0.1:${port}`,
436
- token,
437
- startedAt: now,
438
- heartbeatAt: now,
439
- };
440
- await writeBrokerDiscovery(this.settings.agentDir, this.discovery);
441
- this.#heartbeatTimer = setInterval(
442
- () => void this.heartbeat(),
443
- Math.max(1, Math.floor(this.settings.heartbeatTtlMs / 3)),
444
- );
445
- return this.discovery;
524
+ try {
525
+ await this.index.open();
526
+ await this.ledger.open();
527
+ const now = Date.now();
528
+ const incarnation = brokerProcessIncarnation(process.pid);
529
+ if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
530
+ const token = newBrokerToken();
531
+ this.#transport = new BrokerTransport(this, token, this.settings.port);
532
+ const port = await this.#transport.start();
533
+ this.discovery = {
534
+ version: 1,
535
+ protocolVersion: 3,
536
+ packageGeneration: this.settings.packageGeneration,
537
+ ownerId: this.#owner,
538
+ pid: process.pid,
539
+ incarnation,
540
+ host: "127.0.0.1",
541
+ port,
542
+ url: `ws://127.0.0.1:${port}`,
543
+ token,
544
+ startedAt: now,
545
+ heartbeatAt: now,
546
+ };
547
+ await writeBrokerDiscovery(this.settings.agentDir, this.discovery);
548
+ this.#heartbeatTimer = setInterval(
549
+ () => void this.heartbeat(),
550
+ Math.max(1, Math.floor(this.settings.heartbeatTtlMs / 3)),
551
+ );
552
+ return this.discovery;
553
+ } catch (error) {
554
+ await this.#transport?.stop();
555
+ this.#transport = null;
556
+ this.discovery = null;
557
+ await this.#releaseOwnedLock();
558
+ throw error;
559
+ }
446
560
  }
447
561
  get ownsDiscovery(): boolean {
448
562
  return this.discovery?.ownerId === this.#owner;
449
563
  }
450
- status(): ReturnType<typeof redactBrokerDiscovery> | null {
564
+ status(): RedactedBrokerDiscovery | null {
451
565
  return this.discovery ? redactBrokerDiscovery(this.discovery) : null;
452
566
  }
453
567
  async heartbeat(): Promise<void> {
@@ -476,15 +590,7 @@ export class Broker {
476
590
  } catch (e) {
477
591
  if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
478
592
  }
479
- try {
480
- const lock = await this.#readLock();
481
- if (lock?.ownerId === this.#owner) {
482
- await fs.unlink(this.#lockRecordPath());
483
- await fs.rmdir(this.#lock);
484
- }
485
- } catch (e) {
486
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
487
- }
593
+ await this.#releaseOwnedLock();
488
594
  }
489
595
  this.discovery = null;
490
596
  }
@@ -543,15 +649,21 @@ export class Broker {
543
649
  const resolveSessionId = typeof input.resolveSessionId === "string" ? input.resolveSessionId : undefined;
544
650
  const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
545
651
  if (resolveSessionId && cwd) {
546
- const sessionDir = SessionManager.getDefaultSessionDir(cwd, this.settings.agentDir);
547
- const match = await resolveResumableSession(resolveSessionId, cwd, sessionDir);
652
+ const scope = await resolveManagedSessionScope({ cwd, agentDir: this.settings.agentDir });
653
+ const listed =
654
+ scope.kind === "resolved" ? await listManagedSessionCandidates({ scope: scope.scope }) : undefined;
655
+ const matches =
656
+ listed?.kind === "complete"
657
+ ? listed.owned.filter(candidate => candidate.sessionId === resolveSessionId)
658
+ : [];
659
+ const match = matches.length === 1 ? matches[0] : undefined;
548
660
  return {
549
661
  ok: true,
550
662
  result: {
551
663
  ...result,
552
664
  savedSession:
553
- match && match.session.id === resolveSessionId
554
- ? { id: match.session.id, path: match.session.path }
665
+ match && match.sessionId === resolveSessionId
666
+ ? { id: match.sessionId, path: match.path }
555
667
  : undefined,
556
668
  },
557
669
  indexSeq: result.indexSeq,
@@ -575,37 +687,53 @@ export class Broker {
575
687
  );
576
688
  await prev;
577
689
  try {
690
+ const beforeBegin = this.ledger.get(identity);
578
691
  const begun = await this.ledger.begin(identity, requestHash);
579
- if (begun.kind === "replay") return begun.entry.response as BrokerResponse;
692
+ if (begun.kind === "replay") {
693
+ const replay = begun.entry.response as BrokerResponse;
694
+ if (!(!replay.ok && replay.error.cleanup)) return replay;
695
+ const cleanup = replay.error.cleanup;
696
+ const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
697
+ const response = outcome.response;
698
+ await this.ledger.transition(identity, lifecycleResponseState(response), {
699
+ response,
700
+ responseDigest: createHash("sha256").update(canonicalJson(response)).digest("hex"),
701
+ ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
702
+ ...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
703
+ });
704
+ return response;
705
+ }
580
706
  if (begun.kind === "idempotency_conflict")
581
707
  return error("idempotency_conflict", "idempotency key was used with a different request");
582
- if (begun.kind === "terminal_uncertain")
583
- return begun.entry.response
584
- ? (begun.entry.response as BrokerResponse)
585
- : error("terminal_uncertain", "prior lifecycle operation outcome is uncertain");
586
- if (begun.kind === "in_progress") return error("broker_restarting", "lifecycle operation is in progress");
587
- const outcome = await executeLifecycle(this, operation, input, identity);
588
- const response = outcome.response;
589
- await this.ledger.transition(
590
- identity,
591
- response.ok
592
- ? "terminal_ok"
593
- : response.error.code === "terminal_uncertain"
594
- ? "terminal_uncertain"
595
- : "terminal_error",
596
- {
597
- resultSessionId:
598
- response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
599
- ? (response.result as { sessionId: string }).sessionId
600
- : undefined,
708
+ if (begun.kind === "terminal_uncertain") {
709
+ const replay = (begun.entry.response ?? beforeBegin?.response) as BrokerResponse | undefined;
710
+ if (!replay || replay.ok || !replay.error.cleanup)
711
+ return replay ?? error("terminal_uncertain", "prior lifecycle operation outcome is uncertain");
712
+ const outcome = await executeLifecycle(this, operation, input, identity, replay.error.cleanup);
713
+ const response = outcome.response;
714
+ await this.ledger.transition(identity, lifecycleResponseState(response), {
601
715
  response,
602
716
  responseDigest: createHash("sha256").update(canonicalJson(response)).digest("hex"),
603
717
  ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
604
718
  ...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
605
- },
606
- );
607
- const reopenedLedger = await new LifecycleLedger(this.settings.agentDir).open();
608
- const persisted = reopenedLedger.get(identity);
719
+ });
720
+ return response;
721
+ }
722
+ if (begun.kind === "in_progress") return error("broker_restarting", "lifecycle operation is in progress");
723
+ const outcome = await executeLifecycle(this, operation, input, identity);
724
+ const response = outcome.response;
725
+ await this.ledger.transition(identity, lifecycleResponseState(response), {
726
+ resultSessionId:
727
+ response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
728
+ ? (response.result as { sessionId: string }).sessionId
729
+ : undefined,
730
+ response,
731
+ responseDigest: createHash("sha256").update(canonicalJson(response)).digest("hex"),
732
+ ...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
733
+ ...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
734
+ });
735
+ if (isCleanupPending(response)) return response;
736
+ const persisted = await this.ledger.readTerminal(identity, requestHash);
609
737
  const expectedResponseDigest = createHash("sha256").update(canonicalJson(response)).digest("hex");
610
738
  const persistenceVerified =
611
739
  persisted?.responseDigest === expectedResponseDigest &&
@@ -33,13 +33,32 @@ export function isPidAlive(pid: number): boolean {
33
33
  }
34
34
  }
35
35
  async function syncFile(file: string): Promise<void> {
36
- const handle = await fs.open(file, "r");
36
+ const handle = await fs.open(file, "r+");
37
37
  try {
38
38
  await handle.sync();
39
39
  } finally {
40
40
  await handle.close();
41
41
  }
42
42
  }
43
+
44
+ async function syncDirectory(directory: string): Promise<void> {
45
+ let handle: fs.FileHandle;
46
+ try {
47
+ handle = await fs.open(directory, "r");
48
+ } catch (error) {
49
+ const code = (error as NodeJS.ErrnoException).code;
50
+ if (process.platform === "win32" && (code === "EPERM" || code === "EACCES")) return;
51
+ throw error;
52
+ }
53
+ try {
54
+ await handle.sync();
55
+ } catch (error) {
56
+ const code = (error as NodeJS.ErrnoException).code;
57
+ if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES")) throw error;
58
+ } finally {
59
+ await handle.close();
60
+ }
61
+ }
43
62
  export async function writeBrokerDiscovery(agentDir: string, discovery: BrokerDiscoveryWrite): Promise<void> {
44
63
  const incarnation = discovery.incarnation ?? brokerProcessIncarnation(discovery.pid);
45
64
  if (!incarnation) throw new Error(`Broker process incarnation is unavailable for pid ${discovery.pid}.`);
@@ -54,7 +73,7 @@ export async function writeBrokerDiscovery(agentDir: string, discovery: BrokerDi
54
73
  await fs.chmod(temp, 0o600);
55
74
  await syncFile(temp);
56
75
  await fs.rename(temp, file);
57
- await syncFile(dir);
76
+ await syncDirectory(dir);
58
77
  } finally {
59
78
  await fs.rm(temp, { force: true });
60
79
  }
@@ -13,17 +13,38 @@ export interface EnsureBrokerSettings {
13
13
  }
14
14
 
15
15
  const DISCOVERY_TIMEOUT_MS = 10_000;
16
+ const FIXTURE_DISCOVERY_TIMEOUT_MS = 30_000;
16
17
  // Bounded grace windows for reaping a spawned broker on failure, mirroring the
17
18
  // owned-process teardown convention (SIGTERM -> grace -> SIGKILL -> hard cap).
18
19
  const REAP_GRACEFUL_MS = 2_000;
19
20
  const REAP_SIGKILL_CAP_MS = 2_000;
21
+ export interface FixtureBrokerLease {
22
+ close(): Promise<void>;
23
+ }
24
+
25
+ export interface StartedFixtureBroker {
26
+ discovery: BrokerDiscovery;
27
+ lease: FixtureBrokerLease;
28
+ }
29
+
20
30
  interface BrokerOwner {
21
31
  stop(): Promise<void>;
22
32
  canReuse(discovery: BrokerDiscovery | null): boolean;
23
33
  markReady(discovery: BrokerDiscovery): boolean;
24
34
  }
35
+ type EnsureInitiator = "discovery" | "fixture-lease";
36
+ type EnsureOutcome =
37
+ | { kind: "external-discovery"; discovery: BrokerDiscovery }
38
+ | { kind: "prior-local-owner"; discovery: BrokerDiscovery; owner: BrokerOwner }
39
+ | { kind: "local-started-discovery"; discovery: BrokerDiscovery }
40
+ | { kind: "local-started-fixture"; discovery: BrokerDiscovery; owner: BrokerOwner };
41
+ interface EnsureInFlight {
42
+ initiator: EnsureInitiator;
43
+ promise: Promise<EnsureOutcome>;
44
+ discovery: Promise<BrokerDiscovery>;
45
+ }
25
46
  const owners = new Map<string, BrokerOwner>();
26
- const ensureInFlight = new Map<string, Promise<BrokerDiscovery>>();
47
+ const ensureInFlight = new Map<string, EnsureInFlight>();
27
48
  const reapErrorGuards = new WeakSet<ChildProcess>();
28
49
  interface ReapTiming {
29
50
  gracefulMs: number;
@@ -142,18 +163,44 @@ function brokerSpawnEnvironment(command: SdkInternalSpawnCommand, override?: Nod
142
163
  return environment;
143
164
  }
144
165
 
145
- async function ensureBrokerOnce(settings: EnsureBrokerSettings): Promise<BrokerDiscovery> {
166
+ function fixtureLeaseUnavailable(): Error {
167
+ return new Error("fixture_broker_lease_unavailable");
168
+ }
169
+
170
+ function createFixtureLease(owner: BrokerOwner): FixtureBrokerLease {
171
+ let closeAttempt: Promise<void> | undefined;
172
+ let closed = false;
173
+ return {
174
+ close(): Promise<void> {
175
+ if (closed) return Promise.resolve();
176
+ if (closeAttempt) return closeAttempt;
177
+ closeAttempt = owner.stop().then(
178
+ () => {
179
+ closed = true;
180
+ },
181
+ (error: unknown) => {
182
+ closeAttempt = undefined;
183
+ throw error;
184
+ },
185
+ );
186
+ return closeAttempt;
187
+ },
188
+ };
189
+ }
190
+
191
+ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: EnsureInitiator): Promise<EnsureOutcome> {
146
192
  const priorOwner = owners.get(settings.agentDir);
147
193
  const existing = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
194
+ if (initiator === "fixture-lease" && (priorOwner || existing)) throw fixtureLeaseUnavailable();
148
195
  if (priorOwner) {
149
196
  // A retained cleanup failure fences every discovery record. Only a ready
150
197
  // record bound to this exact child incarnation may be reused.
151
- if (priorOwner.canReuse(existing)) return existing!;
198
+ if (priorOwner.canReuse(existing)) return { kind: "prior-local-owner", discovery: existing!, owner: priorOwner };
152
199
  await priorOwner.stop();
153
200
  const discoveredAfterCleanup = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
154
- if (discoveredAfterCleanup) return discoveredAfterCleanup;
201
+ if (discoveredAfterCleanup) return { kind: "external-discovery", discovery: discoveredAfterCleanup };
155
202
  } else if (existing) {
156
- return existing;
203
+ return { kind: "external-discovery", discovery: existing };
157
204
  }
158
205
 
159
206
  const command = resolveSdkInternalSpawnCommand("broker-internal");
@@ -169,30 +216,27 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings): Promise<BrokerD
169
216
  spawnError = error;
170
217
  });
171
218
  const owner = registerBrokerOwner(settings.agentDir, child);
172
- const deadline = Date.now() + DISCOVERY_TIMEOUT_MS;
219
+ const discoveryTimeoutMs = initiator === "fixture-lease" ? FIXTURE_DISCOVERY_TIMEOUT_MS : DISCOVERY_TIMEOUT_MS;
220
+ const deadline = Date.now() + discoveryTimeoutMs;
173
221
  let discoveryError: unknown;
174
222
  while (Date.now() < deadline) {
175
- // Fail fast: if the spawn failed or the child already exited, discovery can
176
- // never succeed — stop (a no-op once dead) and surface the failure instead
177
- // of running out the discovery timeout with an orphaned child.
178
223
  if (spawnError || child.exitCode !== null || child.signalCode !== null) break;
179
224
  try {
180
225
  const discovered = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
181
226
  if (discovered) {
182
- if (owner.markReady(discovered)) return discovered;
227
+ if (owner.markReady(discovered)) {
228
+ return initiator === "fixture-lease"
229
+ ? { kind: "local-started-fixture", discovery: discovered, owner }
230
+ : { kind: "local-started-discovery", discovery: discovered };
231
+ }
183
232
  await owner.stop();
184
- return discovered;
233
+ return { kind: "external-discovery", discovery: discovered };
185
234
  }
186
235
  } catch (error) {
187
- // A transient read failure (e.g. a half-written record) must not orphan the
188
- // child; remember it and keep polling. It is surfaced if discovery fails.
189
236
  discoveryError = error;
190
237
  }
191
238
  await sleep(50);
192
239
  }
193
- // Discovery failed (timeout, early child exit, spawn error, or unreadable
194
- // discovery): the spawned broker never became reachable. Terminate and reap it
195
- // so the failure cannot leave a detached orphan behind, then surface why.
196
240
  const exitedBeforeDiscovery = child.exitCode !== null || child.signalCode !== null;
197
241
  const failure = spawnError
198
242
  ? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
@@ -211,17 +255,33 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings): Promise<BrokerD
211
255
  throw failure;
212
256
  }
213
257
 
214
- /** Starts the detached broker entrypoint when discovery has no live owner. */
215
- export function ensureBroker(settings: EnsureBrokerSettings): Promise<BrokerDiscovery> {
216
- const existing = ensureInFlight.get(settings.agentDir);
217
- if (existing) return existing;
218
- const pending = ensureBrokerOnce(settings);
219
- ensureInFlight.set(settings.agentDir, pending);
258
+ function startEnsure(settings: EnsureBrokerSettings, initiator: EnsureInitiator): EnsureInFlight {
259
+ const promise = ensureBrokerOnce(settings, initiator);
260
+ const discovery = promise.then(outcome => outcome.discovery);
261
+ void discovery.catch(() => {});
262
+ const entry = { initiator, promise, discovery };
263
+ ensureInFlight.set(settings.agentDir, entry);
220
264
  const clear = (): void => {
221
- if (ensureInFlight.get(settings.agentDir) === pending) ensureInFlight.delete(settings.agentDir);
265
+ if (ensureInFlight.get(settings.agentDir) === entry) ensureInFlight.delete(settings.agentDir);
222
266
  };
223
- void pending.then(clear, clear);
224
- return pending;
267
+ void promise.then(clear, clear);
268
+ return entry;
269
+ }
270
+
271
+ /** Starts the detached broker entrypoint when discovery has no live owner. */
272
+ export function ensureBroker(settings: EnsureBrokerSettings): Promise<BrokerDiscovery> {
273
+ const inFlight = ensureInFlight.get(settings.agentDir) ?? startEnsure(settings, "discovery");
274
+ return inFlight.discovery;
275
+ }
276
+
277
+ /** Starts one fresh fixture broker and returns its sole exact-child close lease. */
278
+ export function startFixtureBrokerWithLeaseForTest(settings: EnsureBrokerSettings): Promise<StartedFixtureBroker> {
279
+ if (ensureInFlight.has(settings.agentDir)) return Promise.reject(fixtureLeaseUnavailable());
280
+ const inFlight = startEnsure(settings, "fixture-lease");
281
+ return inFlight.promise.then(outcome => {
282
+ if (outcome.kind !== "local-started-fixture") throw fixtureLeaseUnavailable();
283
+ return { discovery: outcome.discovery, lease: createFixtureLease(outcome.owner) };
284
+ });
225
285
  }
226
286
 
227
287
  /** Test hook: returns a stop handle for the detached broker this process spawned. */
@@ -0,0 +1,11 @@
1
+ const lifecycleUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
2
+
3
+ /** Decodes persisted lifecycle bytes without replacement characters before JSON parsing. */
4
+ export function decodeLifecycleUtf8(bytes: Uint8Array): string {
5
+ return lifecycleUtf8Decoder.decode(bytes);
6
+ }
7
+
8
+ /** Parses persisted lifecycle JSON only after fatal UTF-8 decoding. */
9
+ export function parseLifecycleJson(bytes: Uint8Array): unknown {
10
+ return JSON.parse(decodeLifecycleUtf8(bytes));
11
+ }