@gajae-code/coding-agent 0.13.3 → 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 (599) hide show
  1. package/CHANGELOG.md +184 -2
  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 +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  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 +1 -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/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { logger, resolveEquivalentPath } from "@gajae-code/utils";
4
5
  import { withFileLock } from "../../config/file-lock";
5
6
  import { processIncarnation } from "./process-incarnation";
6
7
  import {
@@ -83,11 +84,16 @@ export interface IndexedSession {
83
84
  activity?: SessionActivity;
84
85
  /** Wall-clock timestamp of the latest admitted heartbeat, when one exists. */
85
86
  lastHeartbeatAt?: number;
86
- /** True when the same sessionId maps to more than one stateRoot (cross-repo duplicate). */
87
+ /** True when more than one unresolved authority-fencing state-root identity claims this session id. */
87
88
  ambiguous: boolean;
88
- /** True when the identity's latest event is host_unregistered or session_closed (DR-1: stopped rows are retained). */
89
+ /** True when the identity's latest event is terminal (DR-1 retains stopped rows for inspection/offline tail). */
89
90
  terminal: boolean;
90
91
  }
92
+
93
+ /** A session can grant endpoint or lifecycle authority only when one authority-fencing state root claims its id. */
94
+ export function isSessionAuthorityEligible(session: Pick<IndexedSession, "ambiguous">): boolean {
95
+ return session.ambiguous !== true;
96
+ }
91
97
  export interface SessionList {
92
98
  indexSeq: number;
93
99
  sessions: IndexedSession[];
@@ -272,101 +278,324 @@ function auditRecords(events: SessionIndexEvent[], ts: number): SessionIndexAudi
272
278
  }));
273
279
  }
274
280
 
281
+ interface SessionIdentityState {
282
+ identity: string;
283
+ latest: SessionIndexEvent;
284
+ heartbeat: SessionIndexEvent | undefined;
285
+ }
286
+
287
+ interface SessionIndexProjection {
288
+ identities: IndexedSession[];
289
+ sessions: IndexedSession[];
290
+ }
291
+
292
+ function isTerminalEvent(event: SessionIndexEvent): boolean {
293
+ return event.type === "host_unregistered" || event.type === "session_closed" || event.type === "session_deleted";
294
+ }
295
+
296
+ function isUnresolvedEvent(event: SessionIndexEvent): boolean {
297
+ return !isTerminalEvent(event);
298
+ }
299
+
275
300
  /**
276
- * Total-order projection (C5/C6): operate only on admitted events keyed by composite
277
- * identity; the cross-identity winner per sessionId is the admitted event with the
278
- * highest (generation, indexSeq); only `session_deleted` hides a row (DR-1 retains
279
- * stopped/terminal credential-free rows so inspect/offline tail can work). Heartbeats
280
- * inherit locator/endpoint metadata from their identity's prior event. Liveness (C2)
281
- * requires host-written evidence — a heartbeat, or the host's own `host_registered`
282
- * append — observed within 2x the checkpoint interval AND a live host whose OS
283
- * incarnation still matches: a missing or stale evidence trail (e.g. after a broker
284
- * restart) is unknown, never fresh forever.
301
+ * True only for the proven direct-session GC fence row: the `host_registered`
302
+ * event `main.ts` appends at endpoint generation 0 with the agent dir itself as
303
+ * the state root. It has never published an endpoint and can never be attached
304
+ * by SessionRouter (which requires a positive generation and a readable
305
+ * endpoint), so it must not fence a real endpoint root into ambiguity —
306
+ * otherwise every interactive session reads ambiguous and no chat daemon can
307
+ * attach anything.
308
+ *
309
+ * The locator is required provenance, not decoration. Without it any admitted
310
+ * generation-0 `host_registered` from a foreign root would silently stop
311
+ * fencing, which is fail-open relative to the symmetric fence. Generation 0 is
312
+ * also emitted by `recordTerminalUncertain` as an unproven `lifecycle_terminal`
313
+ * claim, and malformed generations are not proof of anything: everything that
314
+ * is not this exact shape keeps fencing (C5/C6).
315
+ *
316
+ * The comparison is symlink-equivalent because the writer and the reader can
317
+ * legitimately spell the same agent dir differently (one via a symlink, one via
318
+ * its realpath). A plain `path.resolve` match would reject the genuine fence
319
+ * row under a symlinked agent dir and re-fence every session — the exact
320
+ * outage this predicate exists to prevent. `rootIdentity` resolves a stateRoot
321
+ * string once per scan so repeated sessions sharing one root never re-run
322
+ * `realpath` on the same spelling.
285
323
  */
286
- function reduceEvents(events: SessionIndexEvent[], now: number): IndexedSession[] {
324
+ function isDirectSessionGcFenceRow(
325
+ event: SessionIndexEvent,
326
+ agentDirIdentity: string,
327
+ rootIdentity: (stateRoot: string) => string,
328
+ ): boolean {
329
+ return (
330
+ event.type === "host_registered" &&
331
+ event.endpointGeneration === 0 &&
332
+ rootIdentity(event.locator.stateRoot) === agentDirIdentity
333
+ );
334
+ }
335
+
336
+ function preferredIdentity(states: Iterable<SessionIdentityState>): SessionIdentityState | undefined {
337
+ let preferred: SessionIdentityState | undefined;
338
+ for (const state of states) {
339
+ if (
340
+ preferred === undefined ||
341
+ state.latest.endpointGeneration > preferred.latest.endpointGeneration ||
342
+ (state.latest.endpointGeneration === preferred.latest.endpointGeneration &&
343
+ state.latest.indexSeq > preferred.latest.indexSeq)
344
+ )
345
+ preferred = state;
346
+ }
347
+ return preferred;
348
+ }
349
+
350
+ function projectIdentity(
351
+ state: SessionIdentityState,
352
+ ambiguous: boolean,
353
+ now: number,
354
+ probedIncarnations?: ReadonlyMap<string, string | undefined>,
355
+ ): IndexedSession {
356
+ const { latest, heartbeat } = state;
357
+ const terminal = isTerminalEvent(latest);
358
+ const terminalUncertain = latest.type === "lifecycle_terminal" || latest.terminalUncertain === true;
359
+ const pidAlive = alive(latest.pid);
360
+ // Liveness evidence is host-written: a checkpointed heartbeat, or the
361
+ // `host_registered` event the host appended itself. Counting registration
362
+ // closes the up-to-one-interval window where a just-registered session
363
+ // would read not-live before the first C2 pass — without weakening the
364
+ // pid-reuse fence, because the incarnation match below is still required.
365
+ // Broker-written events (reconciliation, terminal records) are not host
366
+ // evidence and never refresh liveness.
367
+ const evidenceTs = Math.max(heartbeat?.ts ?? 0, latest.type === "host_registered" ? latest.ts : 0);
368
+ const heartbeatFresh = evidenceTs > 0 && now - evidenceTs < 2 * SESSION_HEARTBEAT_INTERVAL_MS;
369
+ const recordedIncarnation = effectiveIncarnation(latest);
370
+ // OS incarnation probes are never taken while the machine-global index lock is
371
+ // held (#4544): callers that already probed pass the observation in, and
372
+ // unlocked callers probe directly. On Windows a probe can spawn powershell.exe
373
+ // — an unbounded OS operation a machine-global critical section must not await.
374
+ // Dead pids are additionally never probed: the probe exists to fence pid reuse
375
+ // for a LIVE process, and for a dead pid it can only return undefined while
376
+ // costing a powershell.exe spawn per stale registration on Windows.
377
+ const probeKey = `${latest.sessionId}\u0000${latest.endpointGeneration}\u0000${latest.pid}`;
378
+ const currentIncarnation =
379
+ recordedIncarnation === undefined || !pidAlive
380
+ ? undefined
381
+ : probedIncarnations !== undefined
382
+ ? probedIncarnations.get(probeKey)
383
+ : processIncarnation(latest.pid);
384
+ const incarnationMatches = currentIncarnation !== undefined && currentIncarnation === recordedIncarnation;
385
+ return {
386
+ sessionId: latest.sessionId,
387
+ locator: latest.locator,
388
+ endpointGeneration: latest.endpointGeneration,
389
+ pid: latest.pid,
390
+ processIncarnation: latest.processIncarnation,
391
+ endpointMtimeMs: latest.endpointMtimeMs,
392
+ lifecycleRequestId: latest.lifecycleRequestId,
393
+ terminalUncertain,
394
+ indexSeq: latest.indexSeq,
395
+ hostIncarnation: latest.hostIncarnation,
396
+ identityProvenance: recordedIncarnation === undefined ? "legacy" : "composite",
397
+ activity: heartbeat?.activity,
398
+ lastHeartbeatAt: heartbeat?.ts,
399
+ ambiguous,
400
+ terminal,
401
+ live:
402
+ isSessionAuthorityEligible({ ambiguous }) &&
403
+ !terminal &&
404
+ !terminalUncertain &&
405
+ pidAlive &&
406
+ heartbeatFresh &&
407
+ incarnationMatches,
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Total-order projection (C5/C6): first retain every current composite-identity
413
+ * row, then select one public row per session. More than one unresolved
414
+ * authority-fencing state root fences authority; the direct-session GC fence
415
+ * row never fences, but still survives as public authority when it is the only
416
+ * unresolved root left. Once exactly one authority-fencing root remains, its
417
+ * current identity becomes the public authority even when a terminated identity
418
+ * from another root has a higher generation. Heartbeats inherit
419
+ * locator/endpoint metadata from their identity's prior event.
420
+ */
421
+ function reduceEvents(
422
+ events: SessionIndexEvent[],
423
+ now: number,
424
+ agentDir: string,
425
+ probedIncarnations?: ReadonlyMap<string, string | undefined>,
426
+ ): SessionIndexProjection {
427
+ // Resolved once: the fence-row check runs per root, and only this side of the
428
+ // comparison is ours to normalize.
429
+ const agentDirIdentity = resolveEquivalentPath(agentDir);
430
+ // Sessions routinely share one stateRoot spelling (agent dir, worktree, ...);
431
+ // resolve each distinct spelling once per scan instead of per root so the
432
+ // symlink-equivalence check stays a cheap map hit on every repeated session.
433
+ const rootIdentities = new Map<string, string>();
434
+ const rootIdentity = (stateRoot: string): string => {
435
+ let identity = rootIdentities.get(stateRoot);
436
+ if (identity === undefined) {
437
+ identity = resolveEquivalentPath(stateRoot);
438
+ rootIdentities.set(stateRoot, identity);
439
+ }
440
+ return identity;
441
+ };
287
442
  const { admitted } = admitEvents(events);
288
443
  const latestByIdentity = new Map<string, SessionIndexEvent>();
289
444
  const latestHeartbeatByIdentity = new Map<string, SessionIndexEvent>();
290
445
  for (const event of admitted) {
291
- const key = identityKey(event);
446
+ const identity = identityKey(event);
292
447
  if (event.type === "host_heartbeat") {
293
- latestHeartbeatByIdentity.set(key, event);
448
+ latestHeartbeatByIdentity.set(identity, event);
294
449
  continue;
295
450
  }
296
- const previous = latestByIdentity.get(key);
297
- if (previous === undefined || event.indexSeq > previous.indexSeq) latestByIdentity.set(key, event);
451
+ const previous = latestByIdentity.get(identity);
452
+ if (previous === undefined || event.indexSeq > previous.indexSeq) latestByIdentity.set(identity, event);
298
453
  }
299
- const winner = new Map<string, { gen: number; seq: number; identity: string }>();
300
- const roots = new Map<string, Set<string>>();
301
- for (const event of admitted) {
302
- let group = roots.get(event.sessionId);
303
- if (group === undefined) {
304
- group = new Set();
305
- roots.set(event.sessionId, group);
454
+ const statesBySession = new Map<string, SessionIdentityState[]>();
455
+ const rootsBySession = new Map<string, Map<string, SessionIdentityState[]>>();
456
+ for (const [identity, latest] of latestByIdentity) {
457
+ const state: SessionIdentityState = { identity, latest, heartbeat: latestHeartbeatByIdentity.get(identity) };
458
+ let states = statesBySession.get(latest.sessionId);
459
+ if (states === undefined) {
460
+ states = [];
461
+ statesBySession.set(latest.sessionId, states);
306
462
  }
307
- group.add(event.locator.stateRoot);
308
- if (event.type === "host_heartbeat") continue;
309
- const current = winner.get(event.sessionId);
310
- if (
311
- current === undefined ||
312
- event.endpointGeneration > current.gen ||
313
- (event.endpointGeneration === current.gen && event.indexSeq > current.seq)
314
- ) {
315
- winner.set(event.sessionId, {
316
- gen: event.endpointGeneration,
317
- seq: event.indexSeq,
318
- identity: identityKey(event),
319
- });
463
+ states.push(state);
464
+ let roots = rootsBySession.get(latest.sessionId);
465
+ if (roots === undefined) {
466
+ roots = new Map<string, SessionIdentityState[]>();
467
+ rootsBySession.set(latest.sessionId, roots);
468
+ }
469
+ let rootStates = roots.get(latest.locator.stateRoot);
470
+ if (rootStates === undefined) {
471
+ rootStates = [];
472
+ roots.set(latest.locator.stateRoot, rootStates);
473
+ }
474
+ rootStates.push(state);
475
+ }
476
+ // Two views of the same roots. `fencingRoots` holds the roots that may claim
477
+ // authority: more than one fences the session, and exactly one is the sole
478
+ // surviving authority. `unresolvedRoots` additionally holds the
479
+ // direct-session GC fence row, which never fences and only becomes public
480
+ // authority when no fencing root is left at all.
481
+ const unresolvedRoots = new Map<string, Map<string, SessionIdentityState[]>>();
482
+ const fencingRoots = new Map<string, Map<string, SessionIdentityState[]>>();
483
+ for (const [sessionId, roots] of rootsBySession) {
484
+ for (const [root, rootStates] of roots) {
485
+ const current = preferredIdentity(rootStates);
486
+ if (!current || !isUnresolvedEvent(current.latest)) continue;
487
+ let unresolved = unresolvedRoots.get(sessionId);
488
+ if (unresolved === undefined) {
489
+ unresolved = new Map<string, SessionIdentityState[]>();
490
+ unresolvedRoots.set(sessionId, unresolved);
491
+ }
492
+ unresolved.set(root, rootStates);
493
+ if (isDirectSessionGcFenceRow(current.latest, agentDirIdentity, rootIdentity)) continue;
494
+ let fencing = fencingRoots.get(sessionId);
495
+ if (fencing === undefined) {
496
+ fencing = new Map<string, SessionIdentityState[]>();
497
+ fencingRoots.set(sessionId, fencing);
498
+ }
499
+ fencing.set(root, rootStates);
320
500
  }
321
501
  }
502
+ const identities: IndexedSession[] = [];
322
503
  const sessions: IndexedSession[] = [];
323
- for (const [sessionId, chosen] of winner) {
324
- const latest = latestByIdentity.get(chosen.identity);
325
- if (latest === undefined) continue;
326
- const terminal = latest.type === "host_unregistered" || latest.type === "session_closed";
327
- if (latest.type === "session_deleted") continue;
328
- const heartbeat = latestHeartbeatByIdentity.get(chosen.identity);
329
- const pidAlive = alive(latest.pid);
330
- // Liveness evidence is host-written: a checkpointed heartbeat, or the
331
- // `host_registered` event the host appended itself. Counting registration
332
- // closes the up-to-one-interval window where a just-registered session
333
- // would read not-live before the first C2 pass — without weakening the
334
- // pid-reuse fence, because the incarnation match below is still required.
335
- // Broker-written events (reconciliation, terminal records) are not host
336
- // evidence and never refresh liveness.
337
- const evidenceTs = Math.max(heartbeat?.ts ?? 0, latest.type === "host_registered" ? latest.ts : 0);
338
- const heartbeatFresh = evidenceTs > 0 && now - evidenceTs < 2 * SESSION_HEARTBEAT_INTERVAL_MS;
339
- const recordedIncarnation = effectiveIncarnation(latest);
340
- const currentIncarnation = recordedIncarnation === undefined ? undefined : processIncarnation(latest.pid);
341
- const incarnationMatches = currentIncarnation !== undefined && currentIncarnation === recordedIncarnation;
342
- sessions.push({
343
- sessionId,
344
- locator: latest.locator,
345
- endpointGeneration: latest.endpointGeneration,
346
- pid: latest.pid,
347
- processIncarnation: latest.processIncarnation,
348
- endpointMtimeMs: latest.endpointMtimeMs,
349
- lifecycleRequestId: latest.lifecycleRequestId,
350
- terminalUncertain: latest.type === "lifecycle_terminal" || latest.terminalUncertain === true,
351
- indexSeq: latest.indexSeq,
352
- hostIncarnation: latest.hostIncarnation,
353
- identityProvenance: recordedIncarnation === undefined ? "legacy" : "composite",
354
- activity: heartbeat?.activity,
355
- lastHeartbeatAt: heartbeat?.ts,
356
- ambiguous: (roots.get(sessionId)?.size ?? 0) > 1,
357
- terminal,
358
- live: !terminal && pidAlive && heartbeatFresh && incarnationMatches,
359
- });
504
+ for (const [sessionId, states] of statesBySession) {
505
+ const fencing = fencingRoots.get(sessionId);
506
+ const unresolved = unresolvedRoots.get(sessionId);
507
+ const ambiguous = (fencing?.size ?? 0) > 1;
508
+ for (const state of states) identities.push(projectIdentity(state, ambiguous, now, probedIncarnations));
509
+ const defaultAuthority = preferredIdentity(states);
510
+ if (defaultAuthority === undefined || defaultAuthority.latest.type === "session_deleted") continue;
511
+ let authority = defaultAuthority;
512
+ // A sole fencing root outranks any terminated identity from another root,
513
+ // including a higher-generation one. Only when nothing can claim authority
514
+ // does a lone GC fence row become the public survivor.
515
+ const survivingRoots = fencing?.size === 1 ? fencing : (fencing?.size ?? 0) === 0 ? unresolved : undefined;
516
+ if (survivingRoots?.size === 1) {
517
+ const onlyRoot = survivingRoots.values().next().value;
518
+ const survivingAuthority = onlyRoot ? preferredIdentity(onlyRoot) : undefined;
519
+ if (survivingAuthority !== undefined) authority = survivingAuthority;
520
+ }
521
+ sessions.push(projectIdentity(authority, ambiguous, now, probedIncarnations));
360
522
  }
361
- return sessions;
523
+ return { identities, sessions };
362
524
  }
363
525
 
364
526
  // Global launch bursts may queue behind legitimate long index transactions. Keep
365
527
  // this bounded at one minute while the shared lock's exact dead-owner recovery runs.
366
528
  const SESSION_INDEX_LOCK_OPTIONS = { retries: 600, retryDelayMs: 100 } as const;
367
529
 
368
- function withSessionIndexLock<T>(agentDir: string, callback: () => Promise<T>): Promise<T> {
369
- return withFileLock(logFor(agentDir), callback, SESSION_INDEX_LOCK_OPTIONS);
530
+ /**
531
+ * Observation window for one locked index transaction (#4544). This never times
532
+ * out or interrupts the holder — the machine-global lock's safety (never steal
533
+ * from a live owner) depends on holders finishing — it only names the operation
534
+ * still running once the window elapses, so a wedged Windows sync-family await
535
+ * is attributable from logs instead of surfacing as a bare 600-attempt
536
+ * lock-exhaustion crash on every later launch.
537
+ */
538
+ const SESSION_INDEX_OP_SLOW_MS = 10_000;
539
+ /**
540
+ * How long a completed incarnation-probe batch stays trustworthy while the
541
+ * heartbeat pass acquires the machine-global index lock (#4544 review). A pid
542
+ * can exit and be reused while the pass queues behind another holder, and
543
+ * `alive(pid)` alone would see the reused pid while the observation still
544
+ * matched the dead process. Beyond this bound the cycle writes no heartbeat
545
+ * (fail-closed); the OS is never probed under the lock. The bound only needs
546
+ * to cover an uncontended acquisition plus scheduler jitter — real contention
547
+ * queues for the retry delay (100ms) or far longer.
548
+ *
549
+ * Both freshness bounds are measured on the MONOTONIC clock
550
+ * (`performance.now()`), never `Date.now()`: a backward wall-clock step
551
+ * (manual clock fix, NTP slew, VM snapshot restore) would make a wall-clock
552
+ * interval negative and pass the bound, consuming an arbitrarily stale
553
+ * observation batch. Monotonic time never steps backward, so the fail-closed
554
+ * guarantee cannot be defeated by the system clock moving.
555
+ */
556
+ const SESSION_INDEX_PROBE_FRESHNESS_MS = 50;
557
+ /**
558
+ * The same trust bound re-checked AFTER the locked replay (#4544 review):
559
+ * replay re-reads the whole log (up to the 4 MiB rotation bound) and fsyncs
560
+ * pending audit rows, so a large index or a slow Windows sync-family await can
561
+ * stretch the interval between the probe batch and the heartbeat write past
562
+ * the acquisition bound above. A pid can exit and be reused across that
563
+ * awaited replay; consuming the cached observation then would checkpoint the
564
+ * replacement process as the dead row. The replay bound therefore covers the
565
+ * full locked replay cost envelope of a healthy machine instead of scheduler
566
+ * jitter alone; anything slower still fails closed (no heartbeat this cycle)
567
+ * and the next pass re-probes from scratch.
568
+ */
569
+ const SESSION_INDEX_REPLAY_FRESHNESS_MS = 2_000;
570
+
571
+ /**
572
+ * One locked session-index transaction (#4544): every critical section over the
573
+ * machine-global lock runs through this single choke point, so (a) the operation
574
+ * is named in a slow-operation warning when it holds the lock beyond the
575
+ * observation window and (b) the lock budget cannot silently drift per call
576
+ * site.
577
+ */
578
+ function withSessionIndexLock<T>(operation: string, agentDir: string, callback: () => Promise<T>): Promise<T> {
579
+ return withFileLock(
580
+ logFor(agentDir),
581
+ async () => {
582
+ // Armed only once the lock is actually held: queueing time behind another
583
+ // legitimate holder must not be attributed to this operation.
584
+ const note = setTimeout(
585
+ () => logger.warn(`sdk broker: session index "${operation}" still holds the index lock after 10s`),
586
+ SESSION_INDEX_OP_SLOW_MS,
587
+ );
588
+ // A diagnostic timer must never keep the process alive on an exit path
589
+ // that races the 10s window; fast paths always clear it in `finally`.
590
+ note.unref?.();
591
+ try {
592
+ return await callback();
593
+ } finally {
594
+ clearTimeout(note);
595
+ }
596
+ },
597
+ SESSION_INDEX_LOCK_OPTIONS,
598
+ );
370
599
  }
371
600
  function isValidSnapshot(snapshot: unknown): snapshot is { indexSeq: number; events: SessionIndexEvent[] } {
372
601
  if (!snapshot || typeof snapshot !== "object") return false;
@@ -574,7 +803,9 @@ export class SessionIndex {
574
803
  group.promise = SessionIndex.#enqueue(indexPath, () => this.#prepareOpenGroup(indexPath, group!));
575
804
  }
576
805
  await group.promise;
577
- await SessionIndex.#enqueue(indexPath, () => withSessionIndexLock(this.#agentDir, () => this.#replayUnderLock()));
806
+ await SessionIndex.#enqueue(indexPath, () =>
807
+ withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
808
+ );
578
809
  return this;
579
810
  }
580
811
  async #prepareOpenGroup(indexPath: string, group: SessionIndexOpenGroup): Promise<void> {
@@ -588,7 +819,9 @@ export class SessionIndex {
588
819
  }
589
820
  async replay(): Promise<void> {
590
821
  const indexPath = path.resolve(logFor(this.#agentDir));
591
- await SessionIndex.#enqueue(indexPath, () => withSessionIndexLock(this.#agentDir, () => this.#replayUnderLock()));
822
+ await SessionIndex.#enqueue(indexPath, () =>
823
+ withSessionIndexLock("replay", this.#agentDir, () => this.#replayUnderLock()),
824
+ );
592
825
  }
593
826
  async #replayUnderLock(): Promise<void> {
594
827
  const scan = await this.#scan();
@@ -779,14 +1012,14 @@ export class SessionIndex {
779
1012
  }),
780
1013
  );
781
1014
  if (!exists.some(Boolean)) return { status: "healthy", validPrefixSeq: 0, snapshotSeq: 0 };
782
- return await withSessionIndexLock(this.#agentDir, async () => (await this.#scan()).diagnosis);
1015
+ return await withSessionIndexLock("diagnose", this.#agentDir, async () => (await this.#scan()).diagnosis);
783
1016
  });
784
1017
  }
785
1018
  async repair(): Promise<SessionIndexRepairResult> {
786
1019
  const indexPath = path.resolve(logFor(this.#agentDir));
787
1020
  return await SessionIndex.#enqueue(indexPath, async () => {
788
1021
  await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
789
- return await withSessionIndexLock(this.#agentDir, async () => {
1022
+ return await withSessionIndexLock("repair", this.#agentDir, async () => {
790
1023
  const scan = await this.#scan();
791
1024
  if (scan.diagnosis.status === "unsupported") return { ...scan.diagnosis, repaired: false };
792
1025
  if (scan.diagnosis.status === "healthy") return { ...scan.diagnosis, repaired: false };
@@ -800,15 +1033,25 @@ export class SessionIndex {
800
1033
  await writeAndSync(path.join(quarantinePath, "index.snapshot.json"), scan.snapshotContents);
801
1034
  if (scan.logContents) await writeAndSync(path.join(quarantinePath, "index.jsonl"), scan.logContents);
802
1035
  await syncDirectory(path.join(quarantinePath, "index.jsonl"));
803
- const events = [...scan.snapshotEvents, ...scan.validLogEvents];
1036
+ // Repair republishes the surviving history as the new snapshot, so it must
1037
+ // apply the same retention the ordinary snapshot path applies. Writing the
1038
+ // raw survivor set instead let one repair of a long-lived index restore an
1039
+ // unbounded snapshot, and every later locked transaction then re-parsed that
1040
+ // whole history while holding the index lock — the broker burns CPU and
1041
+ // unrelated launches time out waiting for the lock.
1042
+ const events = compactEvents([...scan.snapshotEvents, ...scan.validLogEvents], this.#policy);
804
1043
  const snapshot = JSON.stringify({
805
1044
  version: SESSION_INDEX_SNAPSHOT_VERSION,
806
1045
  indexSeq: scan.diagnosis.validPrefixSeq,
807
1046
  events,
808
1047
  });
809
- const log = scan.validLogEvents.map(event => JSON.stringify(event)).join("\n");
810
1048
  await replaceAtomically(snapshotFor(this.#agentDir), snapshot);
811
- await replaceAtomically(logFor(this.#agentDir), log ? `${log}\n` : "");
1049
+ // The republished snapshot already carries the full surviving history (after
1050
+ // compaction), so the log must be truncated to match: leaving the original
1051
+ // events in place keeps every subsequent #scan() parsing them under the lock
1052
+ // — the same starvation the compaction above is meant to end. This mirrors
1053
+ // #rotate(), which writes an empty log after snapshotting.
1054
+ await replaceAtomically(logFor(this.#agentDir), "");
812
1055
  await this.#replayUnderLock();
813
1056
  return { ...scan.diagnosis, repaired: true, quarantinePath };
814
1057
  });
@@ -869,7 +1112,7 @@ export class SessionIndex {
869
1112
  async refresh(): Promise<void> {
870
1113
  const indexPath = path.resolve(logFor(this.#agentDir));
871
1114
  await SessionIndex.#enqueue(indexPath, () =>
872
- withSessionIndexLock(this.#agentDir, () => this.#refreshUnderLock()),
1115
+ withSessionIndexLock("refresh", this.#agentDir, () => this.#refreshUnderLock()),
873
1116
  );
874
1117
  }
875
1118
  async #refreshUnderLock(): Promise<void> {
@@ -886,7 +1129,19 @@ export class SessionIndex {
886
1129
  const indexPath = path.resolve(logFor(this.#agentDir));
887
1130
  return await SessionIndex.#enqueue(indexPath, async () => {
888
1131
  await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
889
- return await withSessionIndexLock(this.#agentDir, async () => {
1132
+ // Own-pid incarnation derivation runs BEFORE the lock (#4544): on Windows
1133
+ // the probe can spawn powershell.exe, and the machine-global index lock
1134
+ // must not be held across an unbounded OS spawn. It cannot race anything —
1135
+ // this process's own pid is fixed for its lifetime.
1136
+ let ownIncarnation: string | undefined;
1137
+ if (
1138
+ input.hostIncarnation === undefined &&
1139
+ input.pid === process.pid &&
1140
+ Number.isSafeInteger(input.pid) &&
1141
+ input.pid > 0
1142
+ )
1143
+ ownIncarnation = input.processIncarnation ?? processIncarnation(input.pid);
1144
+ return await withSessionIndexLock("append", this.#agentDir, async () => {
890
1145
  await this.#replayUnderLock();
891
1146
  if (this.#corruptSuffix)
892
1147
  throw new Error(
@@ -898,11 +1153,11 @@ export class SessionIndex {
898
1153
  indexSeq: this.indexSeq + 1,
899
1154
  ts: input.ts ?? Date.now(),
900
1155
  };
901
- // Use the registration's durable process identity when supplied; otherwise
902
- // derive one from the OS. Legacy records remain explicitly unbound.
903
- if (unsigned.hostIncarnation === undefined && Number.isSafeInteger(unsigned.pid) && unsigned.pid > 0) {
904
- unsigned.hostIncarnation = unsigned.processIncarnation ?? processIncarnation(unsigned.pid);
905
- }
1156
+ // The host's own OS identity was derived before the lock above;
1157
+ // broker-authored events for another process carry the registration's
1158
+ // persisted binding instead.
1159
+ if (ownIncarnation !== undefined && unsigned.hostIncarnation === undefined)
1160
+ unsigned.hostIncarnation = ownIncarnation;
906
1161
  const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
907
1162
  await appendSync(logFor(this.#agentDir), JSON.stringify(event));
908
1163
  await this.#refreshUnderLock();
@@ -911,6 +1166,93 @@ export class SessionIndex {
911
1166
  });
912
1167
  });
913
1168
  }
1169
+ async unregisterIfCurrent(expected: IndexedSession): Promise<boolean> {
1170
+ const indexPath = path.resolve(logFor(this.#agentDir));
1171
+ return await SessionIndex.#enqueue(indexPath, async () => {
1172
+ await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
1173
+ // OS incarnation probes run BEFORE the machine-global lock is taken
1174
+ // (#4544 review round 5): this pass projects every composite identity
1175
+ // to find the current authority row, and an unlocked projection probes
1176
+ // the OS once per identity — on Windows that can spawn powershell.exe,
1177
+ // which a machine-global critical section must never await. The
1178
+ // unregister decision below never consults the projected `live` flag —
1179
+ // it compares persisted identity fields against the replayed events —
1180
+ // so the pre-lock observations cannot change the outcome either way;
1181
+ // they exist only so the locked projection performs no OS probes.
1182
+ const probed = new Map<string, string | undefined>();
1183
+ for (const row of reduceEvents(this.#events, this.#policy.clock(), this.#agentDir).identities) {
1184
+ const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
1185
+ if (recordedIncarnation === undefined || !alive(row.pid)) continue;
1186
+ probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
1187
+ }
1188
+ return await withSessionIndexLock("conditional unregister", this.#agentDir, async () => {
1189
+ await this.#replayUnderLock();
1190
+ if (this.#corruptSuffix) throw new Error("Cannot conditionally unregister from a corrupt session index");
1191
+ const identities = this.listSessionIdentities(probed);
1192
+ const current = identities.find(
1193
+ session =>
1194
+ session.sessionId === expected.sessionId &&
1195
+ session.endpointGeneration === expected.endpointGeneration &&
1196
+ session.pid === expected.pid &&
1197
+ session.endpointMtimeMs === expected.endpointMtimeMs &&
1198
+ session.lifecycleRequestId === expected.lifecycleRequestId &&
1199
+ session.processIncarnation === expected.processIncarnation &&
1200
+ (session.hostIncarnation ?? session.processIncarnation) ===
1201
+ (expected.hostIncarnation ?? expected.processIncarnation) &&
1202
+ resolveEquivalentPath(session.locator.repo) === resolveEquivalentPath(expected.locator.repo) &&
1203
+ path.resolve(session.locator.stateRoot) === path.resolve(expected.locator.stateRoot),
1204
+ );
1205
+ let currentRoot: IndexedSession | undefined;
1206
+ for (const session of identities) {
1207
+ if (
1208
+ session.sessionId !== expected.sessionId ||
1209
+ session.terminal ||
1210
+ resolveEquivalentPath(session.locator.repo) !== resolveEquivalentPath(expected.locator.repo) ||
1211
+ path.resolve(session.locator.stateRoot) !== path.resolve(expected.locator.stateRoot)
1212
+ )
1213
+ continue;
1214
+ if (
1215
+ currentRoot === undefined ||
1216
+ session.endpointGeneration > currentRoot.endpointGeneration ||
1217
+ (session.endpointGeneration === currentRoot.endpointGeneration &&
1218
+ session.indexSeq > currentRoot.indexSeq)
1219
+ )
1220
+ currentRoot = session;
1221
+ }
1222
+ if (
1223
+ !current ||
1224
+ current !== currentRoot ||
1225
+ current.terminal ||
1226
+ current.terminalUncertain ||
1227
+ this.hostUnregisteredAfter(expected)
1228
+ )
1229
+ return false;
1230
+ const unsigned: Omit<SessionIndexEvent, "checksum"> = {
1231
+ version: SDK_STATE_VERSION,
1232
+ indexSeq: this.indexSeq + 1,
1233
+ ts: Date.now(),
1234
+ type: "host_unregistered",
1235
+ sessionId: expected.sessionId,
1236
+ locator: expected.locator,
1237
+ endpointGeneration: expected.endpointGeneration,
1238
+ pid: expected.pid,
1239
+ ...(expected.processIncarnation === undefined
1240
+ ? {}
1241
+ : { processIncarnation: expected.processIncarnation }),
1242
+ ...(expected.hostIncarnation === undefined ? {} : { hostIncarnation: expected.hostIncarnation }),
1243
+ ...(expected.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: expected.endpointMtimeMs }),
1244
+ ...(expected.lifecycleRequestId === undefined
1245
+ ? {}
1246
+ : { lifecycleRequestId: expected.lifecycleRequestId }),
1247
+ };
1248
+ const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
1249
+ await appendSync(logFor(this.#agentDir), JSON.stringify(event));
1250
+ await this.#refreshUnderLock();
1251
+ if ((await fs.stat(logFor(this.#agentDir))).size >= ROTATE_BYTES) await this.#rotate();
1252
+ return true;
1253
+ });
1254
+ });
1255
+ }
914
1256
 
915
1257
  /** Hold the canonical index lock across an authority-sensitive operation. */
916
1258
  async withLocked<T>(callback: () => Promise<T>): Promise<T> {
@@ -922,7 +1264,7 @@ export class SessionIndex {
922
1264
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return await callback();
923
1265
  throw error;
924
1266
  }
925
- return await withSessionIndexLock(this.#agentDir, async () => {
1267
+ return await withSessionIndexLock("authority operation", this.#agentDir, async () => {
926
1268
  await this.#replayUnderLock();
927
1269
  if (this.#corruptSuffix) throw new Error("Cannot use corrupt session index for artifact reclamation");
928
1270
  return await callback();
@@ -932,7 +1274,7 @@ export class SessionIndex {
932
1274
  async snapshot(): Promise<void> {
933
1275
  const indexPath = path.resolve(logFor(this.#agentDir));
934
1276
  await SessionIndex.#enqueue(indexPath, () =>
935
- withSessionIndexLock(this.#agentDir, () => this.#snapshotUnderLock()),
1277
+ withSessionIndexLock("snapshot", this.#agentDir, () => this.#snapshotUnderLock()),
936
1278
  );
937
1279
  }
938
1280
  async #snapshotUnderLock(): Promise<void> {
@@ -945,26 +1287,14 @@ export class SessionIndex {
945
1287
  if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
946
1288
  }
947
1289
  if (isValidSnapshot(current) && current.indexSeq > this.indexSeq) return;
948
- const tmp = `${file}.${process.pid}.tmp`;
949
- await fs.writeFile(
950
- tmp,
1290
+ await replaceAtomically(
1291
+ file,
951
1292
  JSON.stringify({
952
1293
  version: SESSION_INDEX_SNAPSHOT_VERSION,
953
1294
  indexSeq: this.indexSeq,
954
1295
  events: compactEvents(this.#events, this.#policy),
955
1296
  }),
956
- {
957
- mode: 0o600,
958
- },
959
1297
  );
960
- const h = await fs.open(tmp, "r");
961
- try {
962
- await h.sync();
963
- } finally {
964
- await h.close();
965
- }
966
- await fs.rename(tmp, file);
967
- await syncDirectory(file);
968
1298
  }
969
1299
  /**
970
1300
  * Broker-scheduled compaction (C3), independent of rotation size: applies the
@@ -973,7 +1303,7 @@ export class SessionIndex {
973
1303
  async compact(): Promise<void> {
974
1304
  const indexPath = path.resolve(logFor(this.#agentDir));
975
1305
  await SessionIndex.#enqueue(indexPath, () =>
976
- withFileLock(logFor(this.#agentDir), async () => {
1306
+ withSessionIndexLock("compact", this.#agentDir, async () => {
977
1307
  await this.#replayUnderLock();
978
1308
  await this.#rotate();
979
1309
  }),
@@ -992,18 +1322,26 @@ export class SessionIndex {
992
1322
  listSessions(): SessionList {
993
1323
  return {
994
1324
  indexSeq: this.indexSeq,
995
- sessions: reduceEvents(this.#events, this.#policy.clock()),
1325
+ sessions: reduceEvents(this.#events, this.#policy.clock(), this.#agentDir).sessions,
996
1326
  warnings: this.#warnings,
997
1327
  };
998
1328
  }
1329
+ /**
1330
+ * Broker-internal current composite-identity rows. Unlike {@link listSessions},
1331
+ * this retains losing roots so an exact dead registration can be retired without
1332
+ * disturbing the surviving authority.
1333
+ */
1334
+ listSessionIdentities(probedIncarnations?: ReadonlyMap<string, string | undefined>): IndexedSession[] {
1335
+ return reduceEvents(this.#events, this.#policy.clock(), this.#agentDir, probedIncarnations).identities;
1336
+ }
999
1337
 
1000
1338
  /**
1001
1339
  * Production coalesced heartbeat checkpoint pass (C2): appends one
1002
1340
  * `host_heartbeat` per session at most once per {@link SESSION_HEARTBEAT_INTERVAL_MS}.
1003
1341
  * The pass observes liveness the same way the projection does — the host process
1004
1342
  * must be alive and, for composite identities, still carry the recorded OS process
1005
- * incarnation (a reused PID is never checkpointed). Stopped/terminal rows and rows
1006
- * whose heartbeat is still fresh are skipped. After a broker restart, sessions whose
1343
+ * incarnation (a reused PID is never checkpointed). Stopped, terminal, and ambiguous rows
1344
+ * and rows whose heartbeat is still fresh are skipped. After a broker restart, sessions whose
1007
1345
  * host survived are re-observed as live on the first pass; sessions whose host died
1008
1346
  * while the broker was down keep their stale or missing heartbeat and read as
1009
1347
  * unknown/not-live (never fresh forever). Returns the number of checkpoints written.
@@ -1011,20 +1349,70 @@ export class SessionIndex {
1011
1349
  async checkpointLiveHeartbeats(now = Date.now()): Promise<number> {
1012
1350
  const indexPath = path.resolve(logFor(this.#agentDir));
1013
1351
  return await SessionIndex.#enqueue(indexPath, async () => {
1014
- await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
1015
- return await withFileLock(logFor(this.#agentDir), async () => {
1352
+ // An absent index holds no registration to checkpoint, so this pass must read
1353
+ // it as "nothing to do" instead of creating one. Recreating the directory
1354
+ // resurrects a state root its owner already retired: the broker's 5s
1355
+ // publication watch would rebuild a removed agent dir after shutdown.
1356
+ try {
1357
+ await fs.stat(dirFor(this.#agentDir));
1358
+ } catch (error) {
1359
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
1360
+ throw error;
1361
+ }
1362
+ // OS incarnation probes run BEFORE the machine-global lock is taken
1363
+ // (#4544): on Windows a probe can spawn powershell.exe, and holding the
1364
+ // index lock across an unbounded OS spawn starves every later launch.
1365
+ // The probe reads only the last replayed history to pick WHICH pids are
1366
+ // worth observing. The observations are then bound to the instant the
1367
+ // probe batch COMPLETED: if anything delays this pass past that instant —
1368
+ // lock contention queueing behind another holder, or simply time — the
1369
+ // observation set is discarded and this cycle writes no heartbeat. A pid
1370
+ // can exit and be reused in that window, and `alive(pid)` alone would see
1371
+ // the reused pid while a stale observation still matched the dead
1372
+ // process, checkpointing the wrong host. Failing closed is the only
1373
+ // revalidation that never probes the OS under the lock: the next pass
1374
+ // re-probes from scratch.
1375
+ const probed = new Map<string, string | undefined>();
1376
+ for (const row of reduceEvents(this.#events, now, this.#agentDir).sessions) {
1377
+ if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
1378
+ if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
1379
+ continue;
1380
+ if (!alive(row.pid)) continue;
1381
+ const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
1382
+ if (recordedIncarnation === undefined) continue;
1383
+ probed.set(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`, processIncarnation(row.pid));
1384
+ }
1385
+ const probedAt = performance.now();
1386
+ return await withSessionIndexLock("heartbeat checkpoint", this.#agentDir, async () => {
1387
+ // Contended or delayed acquisition invalidates the observation set:
1388
+ // never trust identity evidence gathered meaningfully before the
1389
+ // lock was held. The checkpoint is skipped this cycle (fail-closed);
1390
+ // the OS is never probed while the machine-global lock is held. The
1391
+ // bound tolerates scheduler jitter on an uncontended acquisition —
1392
+ // which is the only path where the evidence is trustworthy.
1393
+ if (performance.now() - probedAt > SESSION_INDEX_PROBE_FRESHNESS_MS) return 0;
1016
1394
  await this.#replayUnderLock();
1395
+ // Recheck AFTER the awaited replay too (#4544 review round 4): the
1396
+ // replay re-reads the whole log (up to the 4 MiB rotation bound) and
1397
+ // fsyncs pending audit rows, so the probe→write interval can stretch
1398
+ // well past the acquisition bound above while the lock is held. The
1399
+ // same pid-reuse window exists across that awaited replay; consuming
1400
+ // the cached observation then would checkpoint the replacement
1401
+ // process as the dead row. The replay bound covers the full locked
1402
+ // replay cost envelope of a healthy machine; anything slower fails
1403
+ // closed (no heartbeat this cycle) and the next pass re-probes.
1404
+ if (performance.now() - probedAt > SESSION_INDEX_REPLAY_FRESHNESS_MS) return 0;
1017
1405
  if (this.#corruptSuffix) return 0;
1018
1406
  const events: SessionIndexEvent[] = [];
1019
- const rows = reduceEvents(this.#events, now);
1407
+ const rows = reduceEvents(this.#events, now, this.#agentDir, probed).sessions;
1020
1408
  for (const row of rows) {
1021
- if (row.terminal) continue;
1409
+ if (!isSessionAuthorityEligible(row) || row.terminal || row.terminalUncertain) continue;
1022
1410
  if (row.lastHeartbeatAt !== undefined && now - row.lastHeartbeatAt < SESSION_HEARTBEAT_INTERVAL_MS)
1023
1411
  continue;
1024
1412
  if (!alive(row.pid)) continue;
1025
1413
  const recordedIncarnation = row.hostIncarnation ?? row.processIncarnation;
1026
1414
  if (recordedIncarnation === undefined) continue;
1027
- const current = processIncarnation(row.pid);
1415
+ const current = probed.get(`${row.sessionId}\u0000${row.endpointGeneration}\u0000${row.pid}`);
1028
1416
  if (current === undefined || current !== recordedIncarnation) continue;
1029
1417
  const unsigned: Omit<SessionIndexEvent, "checksum"> = {
1030
1418
  version: SDK_STATE_VERSION,
@@ -1053,10 +1441,18 @@ export class SessionIndex {
1053
1441
  hostUnregisteredAfter(
1054
1442
  registration: Pick<
1055
1443
  IndexedSession,
1056
- "sessionId" | "endpointGeneration" | "pid" | "indexSeq" | "lifecycleRequestId"
1444
+ | "sessionId"
1445
+ | "locator"
1446
+ | "endpointGeneration"
1447
+ | "pid"
1448
+ | "indexSeq"
1449
+ | "lifecycleRequestId"
1450
+ | "hostIncarnation"
1451
+ | "processIncarnation"
1057
1452
  >,
1058
1453
  ): { indexSeq: number; lifecycleRequestId?: string } | undefined {
1059
1454
  const lifecycleRequestId = registration.lifecycleRequestId;
1455
+ const incarnation = registration.hostIncarnation ?? registration.processIncarnation;
1060
1456
  const event = this.#events.findLast(
1061
1457
  item =>
1062
1458
  item.type === "host_unregistered" &&
@@ -1064,7 +1460,10 @@ export class SessionIndex {
1064
1460
  item.sessionId === registration.sessionId &&
1065
1461
  item.endpointGeneration === registration.endpointGeneration &&
1066
1462
  item.pid === registration.pid &&
1067
- (lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId),
1463
+ resolveEquivalentPath(item.locator.repo) === resolveEquivalentPath(registration.locator.repo) &&
1464
+ path.resolve(item.locator.stateRoot) === path.resolve(registration.locator.stateRoot) &&
1465
+ (lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId) &&
1466
+ (incarnation === undefined || (item.hostIncarnation ?? item.processIncarnation) === incarnation),
1068
1467
  );
1069
1468
  return event
1070
1469
  ? {