@gajae-code/coding-agent 0.13.2 → 0.14.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 (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -28,6 +28,7 @@ import {
28
28
  type GjcLaunchWorktreePlan,
29
29
  planLaunchWorktree,
30
30
  } from "../../gjc-runtime/launch-worktree";
31
+ import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
31
32
  import {
32
33
  GJC_COORDINATOR_SESSION_BRANCH_ENV,
33
34
  GJC_COORDINATOR_SESSION_ID_ENV,
@@ -44,6 +45,7 @@ import {
44
45
  } from "../../session/session-storage";
45
46
  import type { SessionLifecycleMcpServer } from "../acp/mcp";
46
47
  import { SdkClient, SdkClientError } from "../client/client";
48
+ import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
47
49
  import { SESSION_PREPARED_EVENT } from "../host/host";
48
50
  import {
49
51
  type LogicalSessionCandidate,
@@ -72,6 +74,7 @@ import {
72
74
  processIncarnation,
73
75
  } from "./process-incarnation";
74
76
  import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
77
+ import { isSessionAuthorityEligible } from "./session-index";
75
78
  import {
76
79
  cancellableSleep,
77
80
  DEFAULT_READINESS_TIMEOUT_MS,
@@ -89,8 +92,6 @@ export {
89
92
 
90
93
  const POLL_MS = 50;
91
94
  const CLOSE_TIMEOUT_MS = 2_000;
92
- const STARTUP_CLEANUP_PROOF_GRACE_MS = 500;
93
-
94
95
  const MAX_RECEIVED_AT_SKEW_MS = 5_000;
95
96
  const MAX_LIFECYCLE_METADATA_BYTES = 4096;
96
97
  const MAX_EFFECT_MARKER_LENGTH = 128;
@@ -557,6 +558,7 @@ type LiveResumeRecord = {
557
558
  pid: number;
558
559
  endpointMtimeMs?: number;
559
560
  live: boolean;
561
+ ambiguous: boolean;
560
562
  };
561
563
  type ResumeScope = {
562
564
  cwd: string;
@@ -591,6 +593,7 @@ function sameResumeSessionIdentity(left: ResumeScope, right: ResumeScope): boole
591
593
  function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRecord): boolean {
592
594
  return (
593
595
  current.live &&
596
+ isSessionAuthorityEligible(current) &&
594
597
  current.endpointGeneration === expected.endpointGeneration &&
595
598
  current.pid === expected.pid &&
596
599
  current.endpointMtimeMs === expected.endpointMtimeMs &&
@@ -747,6 +750,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
747
750
  // published about its own process has to survive it: dropping the incarnation
748
751
  // here would silently disarm the teardown fence for every lifecycle session.
749
752
  ...(record.processIncarnation === undefined ? {} : { processIncarnation: record.processIncarnation }),
753
+ ...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
754
+ ...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
750
755
  endpointMtimeMs: record.endpointMtimeMs,
751
756
  });
752
757
  }
@@ -795,7 +800,8 @@ type ReadinessResult =
795
800
  | { kind: "startup_failed"; failure: SdkStartupFailure }
796
801
  | { kind: "child_exited" }
797
802
  | { kind: "timeout" };
798
- const processIncarnationReadersForTest = new WeakMap<Broker, (pid: number) => string | undefined>();
803
+ type BrokerIndex = Pick<Broker, "index">;
804
+ const processIncarnationReadersForTest = new WeakMap<BrokerIndex, (pid: number) => string | undefined>();
799
805
 
800
806
  export function setProcessIncarnationForTest(
801
807
  broker: Broker,
@@ -805,7 +811,7 @@ export function setProcessIncarnationForTest(
805
811
  else processIncarnationReadersForTest.delete(broker);
806
812
  }
807
813
 
808
- function processIncarnationForBroker(broker: Broker, pid: number): string | undefined {
814
+ function processIncarnationForBroker(broker: BrokerIndex, pid: number): string | undefined {
809
815
  const reader = processIncarnationReadersForTest.get(broker);
810
816
  return reader ? reader(pid) : processIncarnation(pid);
811
817
  }
@@ -826,6 +832,11 @@ function observeProcess(
826
832
  } catch (error) {
827
833
  return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
828
834
  }
835
+ if (process.platform === "linux") {
836
+ const probe = probeLinuxProcPidSync(pid);
837
+ if (probe.kind === "live" && probe.state === "Z" && expectedIncarnation === `linux:${probe.startTime}`)
838
+ return "exited";
839
+ }
829
840
  if (!expectedIncarnation) return "uncertain";
830
841
  const actualIncarnation = readIncarnation(pid);
831
842
  if (!actualIncarnation) return "uncertain";
@@ -841,6 +852,41 @@ function hasObservedProcessExit(pid: number): boolean {
841
852
  }
842
853
  }
843
854
 
855
+ type StableProcessExitObserver = {
856
+ hasExited(): boolean;
857
+ };
858
+
859
+ /**
860
+ * Pin the indexed process before shutdown so exit remains observable after it
861
+ * becomes a zombie. `kill(pid, 0)` and `/proc/<pid>/stat` continue to describe
862
+ * an unreaped zombie as present, which otherwise makes a timely SIGTERM exit
863
+ * look unverifiable and incorrectly escalates the close to terminal uncertainty.
864
+ */
865
+ function stableProcessExitObserver(record: CloseRecord): StableProcessExitObserver | undefined {
866
+ if (!record.processIncarnation) return undefined;
867
+ try {
868
+ const reference = nativeLifecycle().Process.fromPid(record.pid);
869
+ if (!reference || reference.incarnation !== record.processIncarnation) return undefined;
870
+ return { hasExited: () => reference.status() === "exited" };
871
+ } catch {
872
+ return undefined;
873
+ }
874
+ }
875
+
876
+ function observedProcessExited(
877
+ pid: number,
878
+ expectedIncarnation: string | undefined,
879
+ observer: StableProcessExitObserver | undefined,
880
+ ): boolean {
881
+ try {
882
+ if (observer?.hasExited()) return true;
883
+ } catch {}
884
+ if (hasObservedProcessExit(pid)) return true;
885
+ if (process.platform !== "linux" || !expectedIncarnation?.startsWith("linux:")) return false;
886
+ const probe = probeLinuxProcPidSync(pid);
887
+ return probe.kind === "live" && probe.state === "Z" && `linux:${probe.startTime}` === expectedIncarnation;
888
+ }
889
+
844
890
  function isEffectMarker(value: unknown): value is EffectMarker {
845
891
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
846
892
  const marker = value as Record<string, unknown>;
@@ -1959,7 +2005,6 @@ type SignalTarget = {
1959
2005
  pid: number;
1960
2006
  lifecycleRequestId?: string;
1961
2007
  processIncarnation?: string;
1962
- hostIncarnation?: string;
1963
2008
  };
1964
2009
 
1965
2010
  /**
@@ -1982,11 +2027,7 @@ async function hasDurableProcessIdentity(target: SignalTarget, id: string, expec
1982
2027
  marker.incarnation === processIncarnation(target.pid)
1983
2028
  );
1984
2029
  if (expected && (expected.pid !== target.pid || target.lifecycleRequestId !== expected.effectMarker)) return false;
1985
- // Hosts that pass an explicit incarnation record it as `processIncarnation`;
1986
- // the index's append path auto-stamps `hostIncarnation` for everything else.
1987
- // Either binding is the same pid-to-incarnation proof.
1988
- const recorded = target.hostIncarnation ?? target.processIncarnation;
1989
- return recorded !== undefined && recorded === processIncarnation(target.pid);
2030
+ return target.processIncarnation !== undefined && target.processIncarnation === processIncarnation(target.pid);
1990
2031
  }
1991
2032
 
1992
2033
  async function hasOwnedReadinessEvidence(
@@ -2074,7 +2115,13 @@ function captureLifecycleFile(file: string, requireRegular = false, bounded = fa
2074
2115
  }
2075
2116
  }
2076
2117
 
2077
- async function removeOwnedLifecycleArtifacts(root: string, id: string, expected: EffectMarker): Promise<boolean> {
2118
+ async function removeOwnedLifecycleArtifacts(
2119
+ root: string,
2120
+ id: string,
2121
+ expected: EffectMarker,
2122
+ onRetainedUnknown?: () => void,
2123
+ onDurablePlaceholder?: (file: string) => void,
2124
+ ): Promise<boolean> {
2078
2125
  const marker = await readEffectMarker(lifecycleMarkerPath(root, id));
2079
2126
  if (!marker || !sameEffectMarker(marker, expected)) return false;
2080
2127
  const endpointPath = path.join(root, "sdk", `${id}.json`);
@@ -2106,7 +2153,7 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
2106
2153
  } catch {
2107
2154
  return false;
2108
2155
  }
2109
- if (parsed.pid !== expected.pid || !hasObservedProcessExit(expected.pid)) return false;
2156
+ if (parsed.pid !== expected.pid || observeProcess(expected.pid, expected.incarnation) !== "exited") return false;
2110
2157
  if (createHash("sha256").update(endpoint.bytes).digest("hex") !== endpoint.digest) return false;
2111
2158
  const endpointRemoval = exactUnlinkLifecycleFile(
2112
2159
  endpointSource,
@@ -2118,7 +2165,49 @@ async function removeOwnedLifecycleArtifacts(root: string, id: string, expected:
2118
2165
  : finalEndpointPath,
2119
2166
  { dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
2120
2167
  );
2121
- if (!endpointRemoval.ok) return false;
2168
+ if (!endpointRemoval.ok) {
2169
+ if (endpointRemoval.retainedUnknownPath) {
2170
+ onRetainedUnknown?.();
2171
+ return false;
2172
+ }
2173
+ if (endpointRemoval.code !== "cleanup_pending") return false;
2174
+ if (endpointRemoval.retainedPlaceholderPath) {
2175
+ if (endpointRemoval.payloadDurable !== true) return false;
2176
+ onDurablePlaceholder?.(path.resolve(endpointRemoval.retainedPlaceholderPath));
2177
+ for (const candidate of [endpointSource, plannedEndpointPath, retryEndpointPath, finalEndpointPath]) {
2178
+ try {
2179
+ if (fsSync.lstatSync(candidate).size === 0) onDurablePlaceholder?.(path.resolve(candidate));
2180
+ } catch {
2181
+ // Absent aliases carry no retained authority.
2182
+ }
2183
+ }
2184
+ }
2185
+ // A payload-durable POSIX detach has scrubbed its quarantine payload to a
2186
+ // zero-byte placeholder, so the original identity cannot authorize a retry.
2187
+ if (
2188
+ endpointRemoval.detachedPath &&
2189
+ endpointRemoval.payloadDurable !== true &&
2190
+ fsSync.existsSync(endpointRemoval.detachedPath)
2191
+ ) {
2192
+ const detachedPath = path.resolve(endpointRemoval.detachedPath);
2193
+ if (path.dirname(detachedPath) !== path.dirname(path.resolve(endpointPath))) return false;
2194
+ let detachedRemoval: NativeExactUnlinkResult;
2195
+ try {
2196
+ detachedRemoval = exactUnlinkLifecycleFile(
2197
+ detachedPath,
2198
+ endpoint.identity,
2199
+ path.join(
2200
+ path.dirname(detachedPath),
2201
+ `.gjc-delete-endpoint-detached-${expected.effectMarker}-${path.basename(endpointPath)}`,
2202
+ ),
2203
+ { dev: BigInt(endpointParent.dev), ino: BigInt(endpointParent.ino) },
2204
+ );
2205
+ } catch {
2206
+ return false;
2207
+ }
2208
+ if (!detachedRemoval.ok && detachedRemoval.code !== "not_found") return false;
2209
+ }
2210
+ }
2122
2211
  }
2123
2212
  const currentMarker = await readEffectMarker(lifecycleMarkerPath(root, id));
2124
2213
  if (!currentMarker || !sameEffectMarker(currentMarker, expected)) return false;
@@ -2139,6 +2228,8 @@ async function recordTerminalUncertain(broker: Broker, id: string, root: string,
2139
2228
  locator: registered.locator,
2140
2229
  endpointGeneration: registered.endpointGeneration,
2141
2230
  pid: registered.pid,
2231
+ ...(registered.processIncarnation === undefined ? {} : { processIncarnation: registered.processIncarnation }),
2232
+ ...(registered.hostIncarnation === undefined ? {} : { hostIncarnation: registered.hostIncarnation }),
2142
2233
  terminalUncertain: true,
2143
2234
  });
2144
2235
  else
@@ -2185,6 +2276,12 @@ async function terminateSpawnedChild(
2185
2276
  };
2186
2277
 
2187
2278
  let observation = observe();
2279
+ const recheckOwnedExitObservation = async (): Promise<void> => {
2280
+ if (observation !== "uncertain") return;
2281
+ // This direct ChildProcess is owned by this broker invocation. A process-incarnation
2282
+ // read can briefly lag its exit event, so recheck only this owned child before failing.
2283
+ observation = await waitForExit(deadline);
2284
+ };
2188
2285
  if (observation === "alive") {
2189
2286
  await waitUntil(timing, terminationStartDeadlineAt);
2190
2287
  observation = observe();
@@ -2192,6 +2289,7 @@ async function terminateSpawnedChild(
2192
2289
  if (observation === "alive") {
2193
2290
  if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGTERM", expected))) {
2194
2291
  observation = observe();
2292
+ await recheckOwnedExitObservation();
2195
2293
  if (observation !== "exited") {
2196
2294
  await recordTerminalUncertain(broker, id, root, pid);
2197
2295
  return false;
@@ -2205,6 +2303,7 @@ async function terminateSpawnedChild(
2205
2303
  if (observation === "alive") {
2206
2304
  if (!(await signalVerifiedSession({ locator: { stateRoot: root }, pid }, id, "SIGKILL", expected))) {
2207
2305
  observation = observe();
2306
+ await recheckOwnedExitObservation();
2208
2307
  if (observation !== "exited") {
2209
2308
  await recordTerminalUncertain(broker, id, root, pid);
2210
2309
  return false;
@@ -2213,6 +2312,7 @@ async function terminateSpawnedChild(
2213
2312
  observation = await waitForExit(deadline);
2214
2313
  }
2215
2314
  }
2315
+ await recheckOwnedExitObservation();
2216
2316
  if (observation !== "exited") {
2217
2317
  await recordTerminalUncertain(broker, id, root, pid);
2218
2318
  return false;
@@ -2284,14 +2384,186 @@ async function endpointRemoved(root: string, id: string): Promise<boolean> {
2284
2384
  }
2285
2385
  }
2286
2386
 
2387
+ async function removeExactDeadSessionEndpoint(
2388
+ broker: Broker,
2389
+ id: string,
2390
+ record: CloseRecord,
2391
+ attempt = 0,
2392
+ authorizedSource?: string,
2393
+ durablePlaceholders?: Set<string>,
2394
+ ): Promise<boolean> {
2395
+ if (record.endpointMtimeMs === undefined) return await endpointRemoved(record.locator.stateRoot, id);
2396
+ await broker.index.refresh();
2397
+ const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
2398
+ if (
2399
+ !current ||
2400
+ current.endpointGeneration !== record.endpointGeneration ||
2401
+ current.pid !== record.pid ||
2402
+ current.endpointMtimeMs !== record.endpointMtimeMs ||
2403
+ current.lifecycleRequestId !== record.lifecycleRequestId ||
2404
+ current.processIncarnation !== record.processIncarnation ||
2405
+ path.resolve(current.locator.stateRoot) !== path.resolve(record.locator.stateRoot)
2406
+ )
2407
+ return false;
2408
+ const endpointPath = path.join(record.locator.stateRoot, "sdk", `${id}.json`);
2409
+ const suffix = `${id}-${record.endpointGeneration}-${record.pid}-${String(record.endpointMtimeMs).replaceAll(".", "_")}.json`;
2410
+ const plannedEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-${suffix}`);
2411
+ const retryEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-retry-${suffix}`);
2412
+ const finalEndpointPath = path.join(path.dirname(endpointPath), `.gjc-dead-endpoint-final-${suffix}`);
2413
+ const ownedEndpointPaths = record.lifecycleRequestId
2414
+ ? [
2415
+ `.gjc-delete-endpoint-detached-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
2416
+ `.gjc-delete-endpoint-final-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
2417
+ `.gjc-delete-endpoint-retry-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
2418
+ `.gjc-delete-endpoint-${record.lifecycleRequestId}-${path.basename(endpointPath)}`,
2419
+ ].map(name => path.join(path.dirname(endpointPath), name))
2420
+ : [];
2421
+ const ownedPayloadPaths: string[] = [];
2422
+ for (const ownedPath of ownedEndpointPaths) {
2423
+ let size: number;
2424
+ try {
2425
+ size = fsSync.lstatSync(ownedPath).size;
2426
+ } catch (error) {
2427
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
2428
+ return false;
2429
+ }
2430
+ if (size > 0) ownedPayloadPaths.push(ownedPath);
2431
+ }
2432
+ const candidates = [...ownedPayloadPaths, finalEndpointPath, retryEndpointPath, plannedEndpointPath, endpointPath];
2433
+ let authorizedDetachedSource: string | undefined;
2434
+ if (authorizedSource && path.dirname(path.resolve(authorizedSource)) === path.dirname(path.resolve(endpointPath))) {
2435
+ try {
2436
+ if (fsSync.lstatSync(authorizedSource).size > 0) authorizedDetachedSource = authorizedSource;
2437
+ } catch {
2438
+ // The detached source was already retired; deterministic candidates remain authoritative.
2439
+ }
2440
+ }
2441
+ const endpointSource =
2442
+ authorizedDetachedSource && fsSync.existsSync(authorizedDetachedSource)
2443
+ ? authorizedDetachedSource
2444
+ : candidates.find(candidate => {
2445
+ try {
2446
+ return fsSync.lstatSync(candidate).isFile();
2447
+ } catch {
2448
+ return false;
2449
+ }
2450
+ });
2451
+ if (!endpointSource) return await endpointRemoved(record.locator.stateRoot, id);
2452
+ const plannedPath =
2453
+ endpointSource === endpointPath
2454
+ ? plannedEndpointPath
2455
+ : endpointSource === plannedEndpointPath
2456
+ ? retryEndpointPath
2457
+ : finalEndpointPath;
2458
+ let handle: fs.FileHandle | undefined;
2459
+ try {
2460
+ handle = await fs.open(endpointSource, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
2461
+ const metadata = await handle.stat({ bigint: true });
2462
+ if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
2463
+ const bytes = Buffer.alloc(Number(metadata.size) + 1);
2464
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
2465
+ if (bytesRead !== Number(metadata.size)) return false;
2466
+ const source = bytes.subarray(0, bytesRead);
2467
+ const endpoint = JSON.parse(source.toString("utf8")) as { sessionId?: unknown; pid?: unknown; stale?: unknown };
2468
+ const indexedEndpointMtimeMs = Math.trunc(record.endpointMtimeMs);
2469
+ if (
2470
+ endpoint.sessionId !== id ||
2471
+ endpoint.pid !== record.pid ||
2472
+ endpoint.stale === true ||
2473
+ !Number.isSafeInteger(indexedEndpointMtimeMs) ||
2474
+ metadata.mtimeNs / 1_000_000n !== BigInt(indexedEndpointMtimeMs)
2475
+ )
2476
+ return false;
2477
+ await handle.close();
2478
+ handle = undefined;
2479
+ const removed = exactUnlinkLifecycleFile(
2480
+ endpointSource,
2481
+ {
2482
+ dev: metadata.dev,
2483
+ ino: metadata.ino,
2484
+ nlink: metadata.nlink,
2485
+ size: metadata.size,
2486
+ mtimeNs: metadata.mtimeNs,
2487
+ sha256: createHash("sha256").update(source).digest("hex"),
2488
+ },
2489
+ plannedPath,
2490
+ (() => {
2491
+ const parent = lifecycleParentIdentity(path.dirname(endpointSource));
2492
+ return parent ? { dev: BigInt(parent.dev), ino: BigInt(parent.ino) } : undefined;
2493
+ })(),
2494
+ );
2495
+ if (removed.ok || removed.code === "not_found") return true;
2496
+ if (removed.code === "cleanup_pending" && removed.retainedUnknownPath) return false;
2497
+ if (removed.code === "cleanup_pending" && removed.retainedPlaceholderPath) {
2498
+ if (removed.payloadDurable !== true) return false;
2499
+ durablePlaceholders?.add(path.resolve(removed.retainedPlaceholderPath));
2500
+ for (const candidate of [endpointSource, plannedEndpointPath, retryEndpointPath, finalEndpointPath]) {
2501
+ try {
2502
+ if (fsSync.lstatSync(candidate).size === 0) durablePlaceholders?.add(path.resolve(candidate));
2503
+ } catch {
2504
+ // Absent aliases carry no retained authority.
2505
+ }
2506
+ }
2507
+ }
2508
+ if (removed.code === "cleanup_pending" && attempt < 4)
2509
+ return await removeExactDeadSessionEndpoint(
2510
+ broker,
2511
+ id,
2512
+ record,
2513
+ attempt + 1,
2514
+ removed.detachedPath,
2515
+ durablePlaceholders,
2516
+ );
2517
+ return false;
2518
+ } catch {
2519
+ return false;
2520
+ } finally {
2521
+ await handle?.close();
2522
+ }
2523
+ }
2524
+
2525
+ async function hasOwnedEndpointPayload(
2526
+ root: string,
2527
+ id: string,
2528
+ effectMarker: string,
2529
+ durablePlaceholders: ReadonlySet<string>,
2530
+ ): Promise<boolean> {
2531
+ const directory = path.join(root, "sdk");
2532
+ const endpointName = `${id}.json`;
2533
+ let names: string[];
2534
+ try {
2535
+ names = await fs.readdir(directory);
2536
+ } catch (error) {
2537
+ return (error as NodeJS.ErrnoException).code !== "ENOENT";
2538
+ }
2539
+ for (const name of names) {
2540
+ if (!name.startsWith(".gjc-delete-endpoint-") || !name.includes(effectMarker) || !name.includes(endpointName))
2541
+ continue;
2542
+ try {
2543
+ const candidate = path.join(directory, name);
2544
+ const metadata = await fs.lstat(candidate);
2545
+ if (
2546
+ !metadata.isFile() ||
2547
+ metadata.size > 0 ||
2548
+ (metadata.size === 0 && !durablePlaceholders.has(path.resolve(candidate)))
2549
+ )
2550
+ return true;
2551
+ } catch (error) {
2552
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") return true;
2553
+ }
2554
+ }
2555
+ return false;
2556
+ }
2287
2557
  async function waitForClose(
2288
2558
  broker: Broker,
2289
2559
  id: string,
2290
- record: { locator: { stateRoot: string }; endpointGeneration: number; pid: number; lifecycleRequestId?: string },
2560
+ record: CloseRecord,
2291
2561
  timeoutMs: number,
2562
+ processExitObserver?: StableProcessExitObserver,
2292
2563
  ): Promise<boolean> {
2293
2564
  const timing = lifecycleTiming(broker);
2294
2565
  const deadline = timing.now() + timeoutMs;
2566
+ const durablePlaceholders = new Set<string>();
2295
2567
  while (timing.now() < deadline) {
2296
2568
  await broker.index.refresh();
2297
2569
  const registration = broker.index.findHostRegistration(
@@ -2304,7 +2576,14 @@ async function waitForClose(
2304
2576
  registration &&
2305
2577
  broker.index.hostUnregisteredAfter(registration) &&
2306
2578
  (await endpointRemoved(record.locator.stateRoot, id)) &&
2307
- hasObservedProcessExit(record.pid)
2579
+ observedProcessExited(record.pid, record.processIncarnation, processExitObserver) &&
2580
+ (!record.lifecycleRequestId ||
2581
+ !(await hasOwnedEndpointPayload(
2582
+ record.locator.stateRoot,
2583
+ id,
2584
+ record.lifecycleRequestId,
2585
+ durablePlaceholders,
2586
+ )))
2308
2587
  )
2309
2588
  return true;
2310
2589
  // A host that dies before it can withdraw its own registration — killed
@@ -2313,17 +2592,36 @@ async function waitForClose(
2313
2592
  // That is exactly the evidence the dead-registration sweep retires records
2314
2593
  // on, so retire it here too instead of escalating signals at a pid that no
2315
2594
  // longer exists and then reporting the teardown as unfinished.
2316
- if (registration && hasObservedProcessExit(record.pid)) {
2317
- await broker.index.append({
2318
- type: "host_unregistered",
2319
- sessionId: id,
2320
- locator: registration.locator,
2321
- endpointGeneration: registration.endpointGeneration,
2322
- pid: registration.pid,
2323
- ...(registration.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: registration.endpointMtimeMs }),
2324
- ...(registration.lifecycleRequestId ? { lifecycleRequestId: registration.lifecycleRequestId } : {}),
2325
- });
2326
- return true;
2595
+ if (registration && observedProcessExited(record.pid, record.processIncarnation, processExitObserver)) {
2596
+ const expected =
2597
+ typeof record.lifecycleRequestId === "string" && typeof record.processIncarnation === "string"
2598
+ ? {
2599
+ pid: record.pid,
2600
+ effectMarker: record.lifecycleRequestId,
2601
+ incarnation: record.processIncarnation,
2602
+ }
2603
+ : undefined;
2604
+ let retainedUnknown = false;
2605
+ if (expected)
2606
+ await removeOwnedLifecycleArtifacts(
2607
+ record.locator.stateRoot,
2608
+ id,
2609
+ expected,
2610
+ () => {
2611
+ retainedUnknown = true;
2612
+ },
2613
+ file => durablePlaceholders.add(path.resolve(file)),
2614
+ );
2615
+ if (retainedUnknown) return false;
2616
+ if (!(await removeExactDeadSessionEndpoint(broker, id, record, 0, undefined, durablePlaceholders)))
2617
+ return false;
2618
+ if (
2619
+ expected &&
2620
+ (await hasOwnedEndpointPayload(record.locator.stateRoot, id, expected.effectMarker, durablePlaceholders))
2621
+ )
2622
+ return false;
2623
+ if (!(await broker.index.unregisterIfCurrent(registration))) return false;
2624
+ return await endpointRemoved(record.locator.stateRoot, id);
2327
2625
  }
2328
2626
  await timing.sleep(POLL_MS);
2329
2627
  }
@@ -2349,6 +2647,10 @@ async function currentReadyAuthority(
2349
2647
  token?: unknown;
2350
2648
  pid?: unknown;
2351
2649
  };
2650
+ // A new lifecycle registration has no heartbeat until the broker checkpoints it.
2651
+ // Publish that positive liveness evidence through the broker-owned checkpoint before
2652
+ // enforcing ready authority; endpoint, marker, incarnation, and generation checks below remain unchanged.
2653
+ await broker.heartbeatSessions();
2352
2654
  await broker.index.refresh();
2353
2655
  const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
2354
2656
  if (
@@ -2984,6 +3286,7 @@ type CloseRecord = {
2984
3286
  pid: number;
2985
3287
  endpointMtimeMs?: number;
2986
3288
  lifecycleRequestId?: string;
3289
+ processIncarnation?: string;
2987
3290
  };
2988
3291
 
2989
3292
  function endpointIncarnation(record: CloseRecord, sessionId: string): string | undefined {
@@ -3033,10 +3336,12 @@ function sameCloseAuthority(authority: CloseAuthority, record: CloseRecord, sess
3033
3336
  );
3034
3337
  }
3035
3338
 
3036
- function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
3339
+ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRecord): boolean {
3037
3340
  return (
3038
- current.live &&
3039
3341
  current.pid === expected.pid &&
3342
+ typeof expected.processIncarnation === "string" &&
3343
+ expected.processIncarnation.length > 0 &&
3344
+ current.processIncarnation === expected.processIncarnation &&
3040
3345
  typeof expected.lifecycleRequestId === "string" &&
3041
3346
  expected.lifecycleRequestId.length > 0 &&
3042
3347
  current.lifecycleRequestId === expected.lifecycleRequestId &&
@@ -3045,23 +3350,58 @@ function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord &
3045
3350
  );
3046
3351
  }
3047
3352
 
3353
+ /**
3354
+ * Endpoint control remains safe without a durable signal identity: the caller has
3355
+ * just read and re-read this exact generation before connecting. This narrower
3356
+ * identity is never sufficient for signal fallback, but permits the host's typed
3357
+ * close response to remain observable instead of being masked as stale.
3358
+ */
3359
+ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord): boolean {
3360
+ return (
3361
+ current.endpointGeneration === expected.endpointGeneration &&
3362
+ current.pid === expected.pid &&
3363
+ current.endpointMtimeMs !== undefined &&
3364
+ expected.endpointMtimeMs !== undefined &&
3365
+ current.lifecycleRequestId === expected.lifecycleRequestId &&
3366
+ path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
3367
+ path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
3368
+ );
3369
+ }
3370
+
3371
+ function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
3372
+ return current.live && sameCloseStoredProcessIdentity(expected, current);
3373
+ }
3374
+
3375
+ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
3376
+ return (
3377
+ current.live &&
3378
+ current.endpointGeneration === expected.endpointGeneration &&
3379
+ current.pid === expected.pid &&
3380
+ current.endpointMtimeMs === expected.endpointMtimeMs &&
3381
+ current.lifecycleRequestId === expected.lifecycleRequestId &&
3382
+ current.processIncarnation === expected.processIncarnation &&
3383
+ path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
3384
+ path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
3385
+ );
3386
+ }
3387
+
3048
3388
  async function revalidateCloseGeneration(
3049
3389
  broker: Broker,
3050
3390
  id: string,
3051
3391
  expected: CloseRecord,
3052
3392
  authority: CloseAuthority | undefined,
3053
- allowUnreachable = false,
3054
3393
  ): Promise<BrokerResponse | undefined> {
3055
3394
  await broker.index.refresh();
3056
3395
  const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3057
- return current &&
3058
- (allowUnreachable || current.live) &&
3059
- current.endpointGeneration === expected.endpointGeneration &&
3060
- current.pid === expected.pid &&
3061
- current.endpointMtimeMs === expected.endpointMtimeMs &&
3062
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
3063
- path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot) &&
3064
- (!authority || sameCloseAuthority(authority, current, id))
3396
+ if (
3397
+ !authority &&
3398
+ current &&
3399
+ (sameCloseEndpointIdentity(expected, current) || sameCloseStoredProcessIdentity(expected, current))
3400
+ ) {
3401
+ expected.endpointMtimeMs = current.endpointMtimeMs;
3402
+ return undefined;
3403
+ }
3404
+ return authority && current && sameCloseGeneration(expected, current) && sameCloseAuthority(authority, current, id)
3065
3405
  ? undefined
3066
3406
  : fail("endpoint_stale", "session endpoint is stale");
3067
3407
  }
@@ -3069,9 +3409,7 @@ async function revalidateCloseGeneration(
3069
3409
  function isTransportFailure(error: unknown): error is SdkClientError {
3070
3410
  return (
3071
3411
  error instanceof SdkClientError &&
3072
- ["unavailable", "timeout", "connection_closed", "uncertain_after_send", "reconnect_exhausted"].includes(
3073
- error.code,
3074
- )
3412
+ ["unavailable", "timeout", "connection_closed", "reconnect_exhausted"].includes(error.code)
3075
3413
  );
3076
3414
  }
3077
3415
 
@@ -3105,11 +3443,14 @@ async function executeLifecycleResponse(
3105
3443
  return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
3106
3444
  if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
3107
3445
  await broker.index.refresh();
3446
+ await broker.heartbeatSessions();
3108
3447
  if (operation === "session.resume") {
3109
3448
  const requestedSessionId = sessionId(input);
3110
3449
  const existing = requestedSessionId
3111
3450
  ? broker.index.listSessions().sessions.find(session => session.sessionId === requestedSessionId)
3112
3451
  : undefined;
3452
+ if (existing && !isSessionAuthorityEligible(existing))
3453
+ return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
3113
3454
  if (existing?.live) {
3114
3455
  const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
3115
3456
  if ("ok" in initialScope) return initialScope;
@@ -3136,12 +3477,16 @@ async function executeLifecycleResponse(
3136
3477
  if ("ok" in finalScope) return finalScope;
3137
3478
  if (!sameResumeSessionIdentity(initialScope, finalScope))
3138
3479
  return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
3480
+ if (current.endpointMtimeMs === undefined)
3481
+ return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
3139
3482
  return {
3140
3483
  ok: true,
3141
3484
  result: {
3142
3485
  sessionId: requestedSessionId,
3143
3486
  cwd: finalScope.cwd,
3144
3487
  endpointGeneration: current.endpointGeneration,
3488
+ pid: current.pid,
3489
+ endpointMtimeMs: current.endpointMtimeMs,
3145
3490
  endpoint: endpoint.result,
3146
3491
  reused: true,
3147
3492
  },
@@ -3433,6 +3778,9 @@ async function executeLifecycleResponse(
3433
3778
  result: {
3434
3779
  sessionId: launch.id,
3435
3780
  cwd: launch.cwd,
3781
+ endpointGeneration: verified.endpointGeneration,
3782
+ pid: verified.endpoint.pid,
3783
+ endpointMtimeMs: verified.endpointMtimeMs,
3436
3784
  endpoint: verified.endpoint,
3437
3785
  ...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
3438
3786
  ...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
@@ -3447,18 +3795,30 @@ async function executeLifecycleResponse(
3447
3795
  let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3448
3796
  if (operation === "session.close") {
3449
3797
  if (!record) return fail("not_found", "session is not indexed");
3450
- // DR-1 retains closed/unregistered rows for inspect and offline tail.
3451
- // Pre-DR-1 they vanished from the listing and close answered not_found;
3452
- // a terminal row is the same already-gone outcome, never an escalation
3453
- // target — signalling its recorded pid could hit a reused process.
3454
- if (record.terminal) return fail("resource_gone", "Session is already closed and unregistered.");
3455
3798
  if (record.terminalUncertain)
3456
3799
  return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
3800
+ if (!isSessionAuthorityEligible(record))
3801
+ return fail("endpoint_stale", "Session authority is ambiguous and cannot be closed safely.");
3802
+ if (!record.live && !record.terminal) {
3803
+ await broker.heartbeatSessions();
3804
+ await broker.index.refresh();
3805
+ record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3806
+ if (!record) return fail("not_found", "session is not indexed");
3807
+ if (record.terminalUncertain)
3808
+ return fail("terminal_uncertain", "Session ownership is uncertain and cannot be closed safely.");
3809
+ if (!isSessionAuthorityEligible(record))
3810
+ return fail("endpoint_stale", "Session authority is ambiguous and cannot be closed safely.");
3811
+ }
3457
3812
  const requestedAuthority = requestedCloseAuthority(input);
3458
3813
  if ("error" in requestedAuthority) return requestedAuthority.error;
3459
3814
  if (requestedAuthority.authority && !sameCloseAuthority(requestedAuthority.authority, record, id))
3460
3815
  return fail("endpoint_stale", "session endpoint is stale");
3461
3816
  await broker.ledger.transition(identity, "effect_started", { intendedSessionId: id, effectMarker: randomUUID() });
3817
+ const signalAuthority: EffectMarker | undefined =
3818
+ typeof record.lifecycleRequestId === "string" && typeof record.processIncarnation === "string"
3819
+ ? { pid: record.pid, effectMarker: record.lifecycleRequestId, incarnation: record.processIncarnation }
3820
+ : undefined;
3821
+ let processExitObserver = stableProcessExitObserver(record);
3462
3822
 
3463
3823
  let usedSignalFallback = false;
3464
3824
  let note: string | undefined;
@@ -3468,6 +3828,7 @@ async function executeLifecycleResponse(
3468
3828
  });
3469
3829
  if (!endpointResult.ok && endpointResult.error.code === "endpoint_stale" && !requestedAuthority.authority) {
3470
3830
  await broker.index.refresh();
3831
+ await broker.heartbeatSessions();
3471
3832
  const refreshed = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3472
3833
  if (refreshed && sameCloseProcessIdentity(record, refreshed)) {
3473
3834
  record = refreshed;
@@ -3478,13 +3839,9 @@ async function executeLifecycleResponse(
3478
3839
  }
3479
3840
  }
3480
3841
  if (!endpointResult.ok) {
3481
- if (endpointResult.error.code === "endpoint_stale") {
3482
- if (requestedAuthority.authority) return endpointResult;
3483
- usedSignalFallback = true;
3484
- } else {
3485
- if (endpointResult.error.code !== "resource_gone") return endpointResult;
3486
- usedSignalFallback = true;
3487
- }
3842
+ if (endpointResult.error.code === "endpoint_stale") return endpointResult;
3843
+ if (endpointResult.error.code !== "resource_gone") return endpointResult;
3844
+ usedSignalFallback = true;
3488
3845
  } else {
3489
3846
  const endpoint = closeEndpoint(endpointResult.result);
3490
3847
  if (!endpoint) return fail("close_refused", "Session endpoint is malformed.");
@@ -3494,11 +3851,35 @@ async function executeLifecycleResponse(
3494
3851
  timeoutMs: 2_000,
3495
3852
  reconnectAttempts: 0,
3496
3853
  });
3497
- const refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
3854
+ let refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
3498
3855
  sessionId: id,
3499
3856
  endpointGeneration: record.endpointGeneration,
3500
3857
  });
3858
+ if (
3859
+ !refreshedEndpointResult.ok &&
3860
+ refreshedEndpointResult.error.code === "endpoint_stale" &&
3861
+ !requestedAuthority.authority
3862
+ ) {
3863
+ await broker.index.refresh();
3864
+ const heartbeatRecord = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3865
+ if (
3866
+ heartbeatRecord &&
3867
+ heartbeatRecord.endpointGeneration === record.endpointGeneration &&
3868
+ sameCloseProcessIdentity(record, heartbeatRecord)
3869
+ ) {
3870
+ record = heartbeatRecord;
3871
+ refreshedEndpointResult = await broker.handleRequest("session.get_endpoint", {
3872
+ sessionId: id,
3873
+ endpointGeneration: record.endpointGeneration,
3874
+ });
3875
+ }
3876
+ }
3501
3877
  if (!refreshedEndpointResult.ok) return refreshedEndpointResult;
3878
+ await broker.index.refresh();
3879
+ const refreshedRecord = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3880
+ if (!refreshedRecord || !sameCloseEndpointIdentity(record, refreshedRecord))
3881
+ return fail("endpoint_stale", "session endpoint is stale");
3882
+ record = refreshedRecord;
3502
3883
  const refreshedEndpoint = closeEndpoint(refreshedEndpointResult.result);
3503
3884
  if (
3504
3885
  !refreshedEndpoint ||
@@ -3508,11 +3889,23 @@ async function executeLifecycleResponse(
3508
3889
  return fail("endpoint_stale", "session endpoint is stale");
3509
3890
  const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
3510
3891
  if (stale) return stale;
3511
- const response = await client.control("session.close");
3512
- if ((response as { ok?: unknown }).ok !== true)
3513
- return fail("close_refused", "Session endpoint rejected session.close.");
3892
+ if (record.lifecycleRequestId === undefined) {
3893
+ usedSignalFallback = true;
3894
+ } else {
3895
+ processExitObserver ??= stableProcessExitObserver(record);
3896
+ const response = await client.control("session.close", {
3897
+ [BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD]: record.lifecycleRequestId,
3898
+ });
3899
+ if ((response as { ok?: unknown }).ok !== true) {
3900
+ const closeError = (response as { error?: { code?: unknown } }).error;
3901
+ if (closeError?.code === "operation_prohibited") usedSignalFallback = true;
3902
+ else return fail("close_refused", "Session endpoint rejected session.close.");
3903
+ }
3904
+ }
3514
3905
  } catch (error) {
3515
3906
  if (isTransportFailure(error)) usedSignalFallback = true;
3907
+ else if (error instanceof SdkClientError && error.code === "operation_prohibited")
3908
+ usedSignalFallback = true;
3516
3909
  else if (error instanceof SdkClientError) return fail(error.code, error.message);
3517
3910
  else
3518
3911
  return fail(
@@ -3520,26 +3913,40 @@ async function executeLifecycleResponse(
3520
3913
  `Session endpoint close failed: ${error instanceof Error ? error.message : String(error)}`,
3521
3914
  );
3522
3915
  } finally {
3523
- await client?.close();
3916
+ await client
3917
+ ?.close()
3918
+ .catch(error =>
3919
+ logger.warn(
3920
+ `SDK session-close client cleanup failed after control dispatch: ${error instanceof Error ? error.message : String(error)}`,
3921
+ ),
3922
+ );
3524
3923
  }
3525
3924
  }
3526
3925
 
3527
3926
  if (usedSignalFallback) {
3528
- const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority, true);
3927
+ const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
3529
3928
  if (stale) return stale;
3530
- if (!(await signalVerifiedSession(record, id, "SIGTERM")))
3531
- return fail(
3532
- "close_refused",
3533
- "Session endpoint is unavailable and its durable process identity could not be verified.",
3534
- );
3535
- note = "Endpoint close was unreachable; sent SIGTERM to the durably identified session process.";
3929
+ processExitObserver ??= stableProcessExitObserver(record);
3930
+ const exited =
3931
+ typeof record.processIncarnation === "string" &&
3932
+ observeProcess(record.pid, record.processIncarnation, value =>
3933
+ processIncarnationForBroker(broker, value),
3934
+ ) === "exited";
3935
+ if (!exited) {
3936
+ if (!(await signalVerifiedSession(record, id, "SIGTERM", signalAuthority)))
3937
+ return fail(
3938
+ "close_refused",
3939
+ "Session endpoint is unavailable and its durable process identity could not be verified.",
3940
+ );
3941
+ note = "Endpoint close was unreachable; sent SIGTERM to the durably identified session process.";
3942
+ }
3536
3943
  }
3537
3944
 
3538
- let closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
3945
+ let closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
3539
3946
  if (!closed && !usedSignalFallback) {
3540
3947
  const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
3541
3948
  if (stale) return stale;
3542
- if (!(await signalVerifiedSession(record, id, "SIGTERM"))) {
3949
+ if (!(await signalVerifiedSession(record, id, "SIGTERM", signalAuthority))) {
3543
3950
  await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
3544
3951
  return fail(
3545
3952
  "terminal_uncertain",
@@ -3548,12 +3955,12 @@ async function executeLifecycleResponse(
3548
3955
  }
3549
3956
  note =
3550
3957
  "Session acknowledged session.close but graceful teardown did not complete within the bounded deadline; sent SIGTERM to the durably identified session process.";
3551
- closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
3958
+ closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
3552
3959
  }
3553
3960
  if (!closed) {
3554
3961
  const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
3555
3962
  if (stale) return stale;
3556
- if (!(await signalVerifiedSession(record, id, "SIGKILL"))) {
3963
+ if (!(await signalVerifiedSession(record, id, "SIGKILL", signalAuthority))) {
3557
3964
  await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
3558
3965
  return fail(
3559
3966
  "terminal_uncertain",
@@ -3562,7 +3969,7 @@ async function executeLifecycleResponse(
3562
3969
  }
3563
3970
  note =
3564
3971
  "Session teardown did not complete after SIGTERM within the bounded deadline; sent SIGKILL to the durably identified session process.";
3565
- closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS);
3972
+ closed = await waitForClose(broker, id, record, CLOSE_TIMEOUT_MS, processExitObserver);
3566
3973
  }
3567
3974
  if (!closed) {
3568
3975
  await recordTerminalUncertain(broker, id, record.locator.stateRoot, record.pid);
@@ -3577,6 +3984,8 @@ async function executeLifecycleResponse(
3577
3984
  if (operation === "session.delete") {
3578
3985
  if (record?.terminalUncertain)
3579
3986
  return fail("terminal_uncertain", "Session ownership is uncertain and cannot be deleted safely.");
3987
+ if (record && !isSessionAuthorityEligible(record))
3988
+ return fail("endpoint_stale", "Session authority is ambiguous and cannot be deleted safely.");
3580
3989
  if (record?.live) return fail("live_session", "Refusing to delete a live session; close it first.");
3581
3990
  if (cleanup === undefined) {
3582
3991
  const requestedTranscriptPath = text(input.sessionPath);
@@ -4191,7 +4600,7 @@ async function executeLifecycleResponse(
4191
4600
  }
4192
4601
  if (record)
4193
4602
  await broker.index.append({
4194
- type: "session_closed",
4603
+ type: "session_deleted",
4195
4604
  sessionId: id,
4196
4605
  locator: record.locator,
4197
4606
  endpointGeneration: record.endpointGeneration,
@@ -4262,23 +4671,6 @@ async function exactCleanupProof(
4262
4671
  : undefined;
4263
4672
  }
4264
4673
 
4265
- async function waitForExactCleanupProof(
4266
- broker: Broker,
4267
- root: string | undefined,
4268
- id: string | undefined,
4269
- expected: EffectMarker | undefined,
4270
- evidence: { artifact: LifecycleFailureArtifact } | undefined,
4271
- ): Promise<LifecycleCleanupProof | undefined> {
4272
- const timing = lifecycleTiming(broker);
4273
- const deadline = timing.now() + STARTUP_CLEANUP_PROOF_GRACE_MS;
4274
- for (;;) {
4275
- const proof = await exactCleanupProof(broker, root, id, expected, evidence);
4276
- if (proof) return proof;
4277
- const remaining = deadline - timing.now();
4278
- if (remaining <= 0) return undefined;
4279
- await timing.sleep(Math.min(POLL_MS, remaining));
4280
- }
4281
- }
4282
4674
  function validateLifecycleDeleteMetadataBinding(
4283
4675
  broker: Broker,
4284
4676
  operation: string,
@@ -4392,7 +4784,7 @@ export async function executeLifecycle(
4392
4784
  expected,
4393
4785
  )
4394
4786
  : undefined;
4395
- const cleanupProof = await waitForExactCleanupProof(broker, root, entry?.intendedSessionId, expected, evidence);
4787
+ const cleanupProof = await exactCleanupProof(broker, root, entry?.intendedSessionId, expected, evidence);
4396
4788
  const startupFailure: LifecycleStartupFailureReceipt | undefined = evidence
4397
4789
  ? {
4398
4790
  artifactDigest: evidence.digest,
@@ -4592,15 +4984,23 @@ export function sessionHostWorkInFlight(): boolean {
4592
4984
  * How often a live broker re-checks its own session registrations against OS
4593
4985
  * process liveness.
4594
4986
  *
4595
- * This sweep can never disturb healthy work: a registration is only dropped
4596
- * when `process.kill(pid, 0)` proves the exact published pid is gone, and a
4597
- * working host answers that probe for its entire life no matter how long a turn
4598
- * runs. One minute keeps `gjc_sessions`/`session.get_endpoint` from advertising
4599
- * a corpse for longer than a single poll while costing one index refresh per
4600
- * minute on an otherwise idle broker.
4987
+ * This sweep can never disturb healthy work: a registration is dropped only when
4988
+ * `observeProcess` proves its exact published process identity exited. A live
4989
+ * replacement at the same pid retires the stale registration without being
4990
+ * signaled. One minute keeps `gjc_sessions`/`session.get_endpoint` from
4991
+ * advertising a corpse for longer than a single poll while costing one index
4992
+ * refresh per minute on an otherwise idle broker.
4601
4993
  */
4602
4994
  export const BROKER_DEAD_REGISTRATION_SWEEP_MS = 60_000;
4603
4995
 
4996
+ /**
4997
+ * Registrations reaped per sweep. Every reap is its own locked index transaction,
4998
+ * so an uncapped sweep over a long-lived index turns one broker into a continuous
4999
+ * holder of the shared session-index lock and starves unrelated `gjc` launches out
5000
+ * of their bounded retry budget. Surplus dead rows are reaped by later sweeps.
5001
+ */
5002
+ export const BROKER_DEAD_REGISTRATION_SWEEP_LIMIT = 64;
5003
+
4604
5004
  /** One reaped registration, as recorded by {@link reapDeadSessionRegistrations}. */
4605
5005
  export interface ReapedSessionRegistration {
4606
5006
  sessionId: string;
@@ -4617,35 +5017,29 @@ export interface ReapedSessionRegistration {
4617
5017
  * missing heartbeat merely makes a session read as not-live — the host may
4618
5018
  * still be running ahead of the next heartbeat checkpoint pass, so it is never
4619
5019
  * grounds for a reap. EPERM and unreadable incarnations stay "uncertain", so an
4620
- * alien or unreadable process is never mistaken for a dead one. Registrations
4621
- * already terminal or marked `terminalUncertain` are left alone: their
4622
- * disposition belongs to the terminal-uncertainty reconciliation path, and
4623
- * silently dropping them would turn a fail-closed `session.close`/
4624
- * `session.delete` refusal into `not_found`.
5020
+ * alien or unreadable process is never mistaken for a dead one. Terminal and
5021
+ * terminal-uncertain identities are retained. Identity-level rows let the sweep
5022
+ * retire a dead losing root without disturbing the surviving authority.
4625
5023
  */
4626
5024
  export async function reapDeadSessionRegistrations(
4627
- broker: Pick<Broker, "index">,
5025
+ broker: BrokerIndex,
5026
+ limit = BROKER_DEAD_REGISTRATION_SWEEP_LIMIT,
4628
5027
  ): Promise<ReapedSessionRegistration[]> {
4629
5028
  await broker.index.refresh();
4630
5029
  const dead = broker.index
4631
- .listSessions()
4632
- .sessions.filter(
4633
- session =>
4634
- !session.terminal &&
4635
- !session.terminalUncertain &&
4636
- observeProcess(session.pid, session.hostIncarnation ?? session.processIncarnation) === "exited",
4637
- );
5030
+ .listSessionIdentities()
5031
+ .filter(session => {
5032
+ if (session.terminal || session.terminalUncertain) return false;
5033
+ const recordedIncarnation = session.hostIncarnation ?? session.processIncarnation;
5034
+ return (
5035
+ observeProcess(session.pid, recordedIncarnation, pid => processIncarnationForBroker(broker, pid)) ===
5036
+ "exited"
5037
+ );
5038
+ })
5039
+ .slice(0, Math.max(0, limit));
4638
5040
  const reaped: ReapedSessionRegistration[] = [];
4639
5041
  for (const session of dead) {
4640
- await broker.index.append({
4641
- type: "host_unregistered",
4642
- sessionId: session.sessionId,
4643
- locator: session.locator,
4644
- endpointGeneration: session.endpointGeneration,
4645
- pid: session.pid,
4646
- ...(session.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: session.endpointMtimeMs }),
4647
- ...(session.lifecycleRequestId ? { lifecycleRequestId: session.lifecycleRequestId } : {}),
4648
- });
5042
+ if (!(await broker.index.unregisterIfCurrent(session))) continue;
4649
5043
  const record = {
4650
5044
  sessionId: session.sessionId,
4651
5045
  pid: session.pid,