@gajae-code/coding-agent 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/cli/config-cli.d.ts +11 -1
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +56 -1
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  54. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  55. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  56. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  57. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  58. package/dist/types/goals/runtime.d.ts +1 -0
  59. package/dist/types/goals/state.d.ts +8 -0
  60. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  61. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  62. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  63. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  64. package/dist/types/hindsight/state.d.ts +11 -4
  65. package/dist/types/lsp/lspmux.d.ts +5 -3
  66. package/dist/types/lsp/path-trust.d.ts +1 -0
  67. package/dist/types/main.d.ts +3 -2
  68. package/dist/types/memories/storage.d.ts +1 -0
  69. package/dist/types/modes/action-registry.d.ts +29 -0
  70. package/dist/types/modes/components/chrome.d.ts +14 -0
  71. package/dist/types/modes/components/command-palette.d.ts +27 -0
  72. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +2 -1
  74. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  75. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  76. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  77. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  78. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  79. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  80. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  81. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  82. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  83. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  84. package/dist/types/modes/components/status-line.d.ts +1 -94
  85. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  86. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  87. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  88. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  89. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  90. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  91. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  92. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  93. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  94. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  95. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  96. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  97. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  98. package/dist/types/modes/interactive-mode.d.ts +21 -19
  99. package/dist/types/modes/jobs-observer.d.ts +2 -0
  100. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  101. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
  102. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  103. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  104. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  105. package/dist/types/modes/types.d.ts +22 -15
  106. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  107. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  108. package/dist/types/sdk/broker/broker.d.ts +78 -12
  109. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  110. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  111. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  112. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  113. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  114. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  115. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  116. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  117. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  120. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  121. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  122. package/dist/types/sdk/bus/index.d.ts +27 -0
  123. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  124. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  125. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  126. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  127. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  128. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  129. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  130. package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
  131. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  132. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  133. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  134. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  135. package/dist/types/sdk/host/host.d.ts +2 -0
  136. package/dist/types/sdk/index.d.ts +1 -0
  137. package/dist/types/sdk/session-directory.d.ts +56 -0
  138. package/dist/types/session/agent-session.d.ts +82 -47
  139. package/dist/types/session/agent-storage.d.ts +7 -0
  140. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  141. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  142. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  143. package/dist/types/session/messages.d.ts +16 -0
  144. package/dist/types/session/session-manager-internal.d.ts +14 -0
  145. package/dist/types/session/session-manager.d.ts +82 -12
  146. package/dist/types/session/session-storage.d.ts +43 -3
  147. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  148. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  149. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  150. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  151. package/dist/types/system-prompt.d.ts +2 -11
  152. package/dist/types/task/executor.d.ts +3 -1
  153. package/dist/types/thinking.d.ts +7 -0
  154. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  155. package/dist/types/tools/ask.d.ts +64 -2
  156. package/dist/types/tools/bash.d.ts +3 -0
  157. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  158. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  159. package/dist/types/tools/cron.d.ts +2 -0
  160. package/dist/types/tools/fetch.d.ts +1 -0
  161. package/dist/types/tools/index.d.ts +30 -15
  162. package/dist/types/tools/output-meta.d.ts +1 -0
  163. package/dist/types/tools/read.d.ts +3 -0
  164. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  165. package/dist/types/tools/subagent-render.d.ts +1 -0
  166. package/dist/types/tools/subagent.d.ts +6 -1
  167. package/dist/types/utils/event-bus.d.ts +1 -1
  168. package/dist/types/web/search/index.d.ts +3 -1
  169. package/package.json +21 -9
  170. package/scripts/build-sdk-package-smoke.ts +39 -1
  171. package/scripts/generate-hotkeys-docs.ts +74 -0
  172. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  173. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  174. package/src/async/job-manager.ts +455 -69
  175. package/src/cli/config-cli.ts +75 -2
  176. package/src/cli/notify-cli.ts +92 -26
  177. package/src/cli/session-picker.ts +20 -3
  178. package/src/cli/setup-cli.ts +1 -1
  179. package/src/cli.ts +1 -1
  180. package/src/commands/config.ts +1 -1
  181. package/src/commands/harness.ts +34 -106
  182. package/src/commands/launch.ts +6 -4
  183. package/src/commands/mcp-serve.ts +85 -8
  184. package/src/commands/mcp.ts +2 -2
  185. package/src/commands/notify.ts +8 -4
  186. package/src/commands/sdk.ts +61 -40
  187. package/src/commands/ultragoal.ts +4 -1
  188. package/src/config/atomic-yaml-patch.ts +80 -13
  189. package/src/config/file-lock.ts +105 -43
  190. package/src/config/keybindings.ts +64 -20
  191. package/src/config/model-auth.ts +9 -0
  192. package/src/config/model-bindings-applier.ts +139 -0
  193. package/src/config/model-discovery-manager.ts +224 -0
  194. package/src/config/model-equivalence.ts +66 -0
  195. package/src/config/model-registry.ts +169 -302
  196. package/src/config/model-resolver.ts +361 -369
  197. package/src/config/models-config-schema.ts +5 -4
  198. package/src/config/settings-schema.ts +159 -2
  199. package/src/config/settings.ts +458 -482
  200. package/src/coordinator-mcp/policy.ts +76 -11
  201. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  202. package/src/coordinator-mcp/question-state.ts +778 -0
  203. package/src/coordinator-mcp/server.ts +1702 -238
  204. package/src/debug/index.ts +1 -1
  205. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  206. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  207. package/src/discovery/helpers.ts +1 -50
  208. package/src/edit/index.ts +64 -0
  209. package/src/eval/py/env.ts +26 -0
  210. package/src/eval/py/kernel.ts +5 -3
  211. package/src/extensibility/extensions/loader.ts +9 -1
  212. package/src/extensibility/extensions/runner.ts +12 -3
  213. package/src/extensibility/extensions/types.ts +39 -1
  214. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  215. package/src/extensibility/hooks/runner.ts +2 -2
  216. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  217. package/src/extensibility/shared-events.ts +2 -0
  218. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  219. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  220. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  221. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  222. package/src/gjc-runtime/launch-tmux.ts +2 -5
  223. package/src/gjc-runtime/launch-worktree.ts +5 -5
  224. package/src/gjc-runtime/linux-proc.ts +71 -0
  225. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  226. package/src/gjc-runtime/session-resolution.ts +18 -3
  227. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  228. package/src/gjc-runtime/state-migrations.ts +1 -1
  229. package/src/gjc-runtime/state-runtime.ts +320 -249
  230. package/src/gjc-runtime/state-writer.ts +36 -40
  231. package/src/gjc-runtime/team-launch.ts +319 -0
  232. package/src/gjc-runtime/team-notify.ts +326 -0
  233. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  234. package/src/gjc-runtime/team-store.ts +828 -0
  235. package/src/gjc-runtime/team-workers.ts +962 -0
  236. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  237. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  238. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  239. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  240. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  241. package/src/gjc-runtime/ultragoal-guard.ts +83 -12
  242. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  243. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  244. package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
  245. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  246. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  247. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  248. package/src/goals/continuation-timeout-guard.ts +79 -0
  249. package/src/goals/runtime.ts +7 -5
  250. package/src/goals/state.ts +18 -0
  251. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  252. package/src/harness-control-plane/phase-rollup.ts +4 -7
  253. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  254. package/src/harness-control-plane/receipt-spool.ts +51 -7
  255. package/src/harness-control-plane/session-lease.ts +12 -130
  256. package/src/hindsight/backend.ts +26 -27
  257. package/src/hindsight/state.ts +101 -32
  258. package/src/hooks/skill-state.ts +17 -11
  259. package/src/internal-urls/docs-index.generated.ts +21 -18
  260. package/src/lsp/client.ts +1 -1
  261. package/src/lsp/config.ts +145 -26
  262. package/src/lsp/index.ts +1 -1
  263. package/src/lsp/lspmux.ts +39 -10
  264. package/src/lsp/path-trust.ts +81 -0
  265. package/src/main.ts +28 -8
  266. package/src/memories/index.ts +1 -0
  267. package/src/memories/storage.ts +6 -2
  268. package/src/modes/action-registry.ts +165 -0
  269. package/src/modes/components/chrome.ts +40 -0
  270. package/src/modes/components/command-palette.ts +168 -0
  271. package/src/modes/components/custom-editor.ts +1 -1
  272. package/src/modes/components/custom-provider-wizard.ts +51 -14
  273. package/src/modes/components/index.ts +2 -1
  274. package/src/modes/components/pet-selector.ts +11 -17
  275. package/src/modes/components/plan-preview-overlay.ts +295 -0
  276. package/src/modes/components/queue-mode-selector.ts +10 -27
  277. package/src/modes/components/queue-pane.ts +79 -0
  278. package/src/modes/components/read-tool-group.ts +13 -0
  279. package/src/modes/components/session-observer-overlay.ts +207 -772
  280. package/src/modes/components/sessions-dashboard.ts +181 -0
  281. package/src/modes/components/settings-selector.ts +1 -1
  282. package/src/modes/components/show-images-selector.ts +10 -24
  283. package/src/modes/components/status-line/types.ts +1 -1
  284. package/src/modes/components/status-line.ts +1 -969
  285. package/src/modes/components/tasks-pane.ts +73 -0
  286. package/src/modes/components/theme-selector.ts +11 -31
  287. package/src/modes/components/tool-execution.ts +22 -0
  288. package/src/modes/components/tool-status-header.ts +1056 -0
  289. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  290. package/src/modes/controllers/btw-controller.ts +1 -0
  291. package/src/modes/controllers/command-controller.ts +9 -15
  292. package/src/modes/controllers/event-controller.ts +3 -2
  293. package/src/modes/controllers/extension-ui-controller.ts +72 -25
  294. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  295. package/src/modes/controllers/input-controller.ts +764 -229
  296. package/src/modes/controllers/mode-gate.ts +24 -0
  297. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  298. package/src/modes/controllers/selector-controller.ts +225 -10
  299. package/src/modes/controllers/stt-controller.ts +125 -0
  300. package/src/modes/interactive-mode.ts +302 -1151
  301. package/src/modes/jobs-observer.ts +3 -0
  302. package/src/modes/runtime-init.ts +8 -0
  303. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  304. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
  305. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  306. package/src/modes/tasks-aggregator.ts +199 -0
  307. package/src/modes/transcript-item-registry.ts +229 -0
  308. package/src/modes/types.ts +47 -20
  309. package/src/modes/utils/ui-helpers.ts +17 -9
  310. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  311. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  312. package/src/prompts/agents/architect.md +2 -8
  313. package/src/prompts/agents/critic.md +2 -8
  314. package/src/prompts/agents/executor.md +3 -0
  315. package/src/prompts/agents/planner.md +4 -12
  316. package/src/prompts/system/custom-system-prompt.md +1 -23
  317. package/src/prompts/system/plan-mode-approved.md +8 -0
  318. package/src/prompts/system/system-prompt.md +17 -96
  319. package/src/prompts/tools/bash.md +4 -4
  320. package/src/prompts/tools/browser.md +15 -52
  321. package/src/prompts/tools/find.md +2 -2
  322. package/src/prompts/tools/hashline.md +13 -55
  323. package/src/prompts/tools/read.md +3 -6
  324. package/src/prompts/tools/search-tool-bm25.md +0 -2
  325. package/src/prompts/tools/search.md +2 -4
  326. package/src/prompts/tools/skill-discovery.md +1 -2
  327. package/src/prompts/tools/task.md +1 -1
  328. package/src/runtime-mcp/manager.ts +1 -1
  329. package/src/sdk/broker/broker.ts +206 -78
  330. package/src/sdk/broker/discovery.ts +21 -2
  331. package/src/sdk/broker/ensure.ts +85 -25
  332. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  333. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  334. package/src/sdk/broker/lifecycle.ts +1399 -142
  335. package/src/sdk/broker/process-incarnation.ts +5 -13
  336. package/src/sdk/broker/session-index.ts +145 -56
  337. package/src/sdk/broker/state-version.ts +21 -0
  338. package/src/sdk/broker/transport.ts +1 -1
  339. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  340. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  341. package/src/sdk/bus/chat-effect-journal.ts +14 -4
  342. package/src/sdk/bus/config-commands.ts +5 -4
  343. package/src/sdk/bus/config.ts +98 -0
  344. package/src/sdk/bus/conversation-store.ts +5 -29
  345. package/src/sdk/bus/daemon-paths.ts +2 -0
  346. package/src/sdk/bus/discord-daemon.ts +25 -8
  347. package/src/sdk/bus/html-format.ts +20 -3
  348. package/src/sdk/bus/index.ts +834 -113
  349. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  350. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  351. package/src/sdk/bus/natives-augment.d.ts +26 -1
  352. package/src/sdk/bus/notification-service.ts +168 -20
  353. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  354. package/src/sdk/bus/recent-activity.ts +188 -74
  355. package/src/sdk/bus/rich-document.ts +258 -0
  356. package/src/sdk/bus/rich-render.ts +62 -70
  357. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  358. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  359. package/src/sdk/bus/telegram-daemon.ts +1491 -247
  360. package/src/sdk/bus/threaded-inbound.ts +8 -3
  361. package/src/sdk/bus/threaded-render.ts +57 -1
  362. package/src/sdk/bus/topic-registry.ts +120 -13
  363. package/src/sdk/host/control/dispatch.ts +14 -2
  364. package/src/sdk/host/control/operations.ts +6 -1
  365. package/src/sdk/host/host.ts +31 -1
  366. package/src/sdk/host/query/handlers.ts +20 -22
  367. package/src/sdk/host/query/revision-store.ts +80 -57
  368. package/src/sdk/index.ts +1 -0
  369. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  370. package/src/sdk/session-directory.ts +141 -0
  371. package/src/sdk/session.ts +458 -479
  372. package/src/session/agent-session.ts +1901 -637
  373. package/src/session/agent-storage.ts +18 -0
  374. package/src/session/cache-economics.ts +25 -8
  375. package/src/session/fallback-chain-controller.ts +53 -3
  376. package/src/session/internal/managed-session-scope.ts +2226 -0
  377. package/src/session/internal/managed-session-storage.ts +453 -0
  378. package/src/session/messages.ts +91 -2
  379. package/src/session/session-manager-internal.ts +33 -0
  380. package/src/session/session-manager.ts +1112 -333
  381. package/src/session/session-storage.ts +444 -39
  382. package/src/session/volatile-context-pruning.ts +78 -0
  383. package/src/setup/credential-auto-import.ts +2 -1
  384. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  385. package/src/setup/provider-onboarding.ts +36 -61
  386. package/src/setup/provider-presets.json +5 -0
  387. package/src/skill-state/active-state.ts +3 -13
  388. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  389. package/src/skill-state/workflow-state-contract.ts +6 -41
  390. package/src/slash-commands/builtin-registry.ts +46 -11
  391. package/src/system-prompt.ts +37 -44
  392. package/src/task/agents.ts +19 -1
  393. package/src/task/executor.ts +90 -6
  394. package/src/task/index.ts +62 -37
  395. package/src/thinking.ts +21 -6
  396. package/src/tool-discovery/tool-index.ts +2 -134
  397. package/src/tools/ask.ts +157 -21
  398. package/src/tools/ast-edit.ts +1 -1
  399. package/src/tools/bash.ts +38 -0
  400. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  401. package/src/tools/browser/tab-supervisor.ts +282 -36
  402. package/src/tools/computer.ts +10 -3
  403. package/src/tools/cron.ts +30 -16
  404. package/src/tools/eval.ts +1 -2
  405. package/src/tools/fetch.ts +11 -4
  406. package/src/tools/index.ts +76 -24
  407. package/src/tools/output-meta.ts +13 -5
  408. package/src/tools/read.ts +37 -1
  409. package/src/tools/resource-gc.ts +2 -2
  410. package/src/tools/search-tool-bm25.ts +11 -74
  411. package/src/tools/subagent-render.ts +19 -8
  412. package/src/tools/subagent.ts +84 -32
  413. package/src/utils/event-bus.ts +9 -4
  414. package/src/web/search/index.ts +3 -3
  415. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  416. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  417. package/src/discovery/plugin-dir-roots.ts +0 -28
  418. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -11,6 +11,8 @@ import * as fs from "node:fs";
11
11
  import * as fsPromises from "node:fs/promises";
12
12
  import * as path from "node:path";
13
13
  import type { NotificationControlServer as NativeNotificationControlServer } from "@gajae-code/natives";
14
+ import { logger } from "@gajae-code/utils";
15
+ import { readLinuxProcStartTime } from "../../gjc-runtime/linux-proc";
14
16
  import { tmuxRuntimeSessionPath } from "../../gjc-runtime/session-layout";
15
17
  import {
16
18
  GJC_COORDINATOR_SESSION_ID_ENV,
@@ -116,6 +118,20 @@ function isLifecycleErrorReason(value: unknown): value is LifecycleErrorReason {
116
118
  }
117
119
 
118
120
  /** Atomic + fsynced file-backed idempotency ledger store. */
121
+ function hasNonDirectoryLedgerParent(idempotencyFile: string): boolean {
122
+ let parent = path.dirname(idempotencyFile);
123
+ while (true) {
124
+ try {
125
+ return !fs.statSync(parent).isDirectory();
126
+ } catch (error) {
127
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
128
+ const nextParent = path.dirname(parent);
129
+ if (nextParent === parent) return false;
130
+ parent = nextParent;
131
+ }
132
+ }
133
+ }
134
+
119
135
  function ledgerReadError(error: unknown): Error {
120
136
  const errorCode = (error as NodeJS.ErrnoException | undefined)?.code;
121
137
  const code = typeof errorCode === "string" ? errorCode : "invalid";
@@ -181,27 +197,55 @@ function migrateLegacyLedger(doc: LedgerDoc): LedgerDoc {
181
197
  return doc;
182
198
  }
183
199
 
200
+ const LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT = 1;
201
+ const LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT = 10;
202
+ let lifecycleCompatibilityDiagnosticCount = 0;
203
+ let lifecycleControlDiagnosticCount = 0;
204
+
184
205
  function isUnsupportedDirectorySyncError(error: unknown): boolean {
185
206
  const code = (error as NodeJS.ErrnoException | undefined)?.code;
186
- return code === "EINVAL" || code === "ENOTSUP" || code === "EOPNOTSUPP";
207
+ return (
208
+ code === "EINVAL" ||
209
+ code === "ENOTSUP" ||
210
+ code === "EOPNOTSUPP" ||
211
+ (process.platform === "win32" && code === "EPERM")
212
+ );
187
213
  }
188
214
 
189
- function recordDirectorySyncCompatibilityDiagnostic(): void {
215
+ function recordBoundedLifecycleDiagnostic(message: string, compatibility = false): void {
216
+ if (compatibility) {
217
+ if (lifecycleCompatibilityDiagnosticCount >= LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT) return;
218
+ lifecycleCompatibilityDiagnosticCount++;
219
+ } else {
220
+ if (lifecycleControlDiagnosticCount >= LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT) return;
221
+ lifecycleControlDiagnosticCount++;
222
+ }
190
223
  try {
191
- process.stderr.write("gjc lifecycle ledger directory sync unsupported\n");
224
+ logger.warn(message);
192
225
  } catch {
193
- // Compatibility diagnostics must not alter ledger durability semantics.
226
+ // Diagnostics are best-effort and must not poison the lifecycle queue.
194
227
  }
195
228
  }
196
229
 
197
230
  function fsyncLedgerParentDirectory(directory: string): void {
198
- const dirFd = fs.openSync(directory, "r");
231
+ let dirFd: number;
232
+ try {
233
+ dirFd = fs.openSync(directory, "r");
234
+ } catch (error) {
235
+ if (process.platform === "win32" && isUnsupportedDirectorySyncError(error)) {
236
+ recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
237
+ return;
238
+ }
239
+ throw error;
240
+ }
241
+
199
242
  let syncFailure: unknown;
200
243
  let closeFailure: unknown;
201
244
  try {
202
245
  fs.fsyncSync(dirFd);
203
246
  } catch (error) {
204
- if (isUnsupportedDirectorySyncError(error)) recordDirectorySyncCompatibilityDiagnostic();
247
+ if (process.platform === "win32" && isUnsupportedDirectorySyncError(error))
248
+ recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
205
249
  else syncFailure = error;
206
250
  } finally {
207
251
  try {
@@ -224,7 +268,16 @@ export function fileLedgerStore(idempotencyFile: string): LedgerStore {
224
268
  contents = fs.readFileSync(idempotencyFile, "utf8");
225
269
  } catch (error) {
226
270
  const code = (error as NodeJS.ErrnoException).code;
227
- if (code === "ENOENT") return { version: 1, entries: {} };
271
+ if (code === "ENOENT") {
272
+ let hasNonDirectoryParent: boolean;
273
+ try {
274
+ hasNonDirectoryParent = hasNonDirectoryLedgerParent(idempotencyFile);
275
+ } catch (parentError) {
276
+ throw ledgerReadError(parentError);
277
+ }
278
+ if (hasNonDirectoryParent) throw ledgerReadError({ code: "ENOTDIR" });
279
+ return { version: 1, entries: {} };
280
+ }
228
281
  throw ledgerReadError(error);
229
282
  }
230
283
  try {
@@ -373,15 +426,11 @@ function lifecycleOwnerIsolationProbe(tmux: string, env: NodeJS.ProcessEnv): Own
373
426
  cgroup: { classification: "not_applicable" },
374
427
  sessionNames,
375
428
  };
376
- const [cgroupText, stat] = await Promise.all([
429
+ const [cgroupText, startTime] = await Promise.all([
377
430
  fsPromises.readFile(`/proc/${pid}/cgroup`, "utf8").catch(() => null),
378
- fsPromises.readFile(`/proc/${pid}/stat`, "utf8").catch(() => null),
431
+ readLinuxProcStartTime(pid),
379
432
  ]);
380
433
  const cgroup = classifyCgroup({ platform: process.platform, cgroupText });
381
- const startTime = stat
382
- ?.slice(stat.lastIndexOf(")") + 2)
383
- .trim()
384
- .split(/\s+/)[19];
385
434
  if (!startTime) return { state: "unverifiable", pid, cgroup, sessionNames };
386
435
  return {
387
436
  state:
@@ -931,6 +980,8 @@ export function daemonCloseSession(
931
980
  export function daemonResumeSession(
932
981
  env: NodeJS.ProcessEnv = process.env,
933
982
  opts: {
983
+ agentDir?: string;
984
+ /** Explicit managed root for isolated tests. */
934
985
  sessionsRoot?: string;
935
986
  listSessions?: (env: NodeJS.ProcessEnv) => GjcTmuxSessionStatus[];
936
987
  ownerIsolationProbe?: OwnerIsolationProbe;
@@ -971,21 +1022,28 @@ export function daemonResumeSession(
971
1022
  // blindly spawning `gjc --resume <prefix>` against a non-authoritative id.
972
1023
  let resumeId = target.sessionIdOrPrefix;
973
1024
  let resumeCwd = target.path;
974
- if (opts.sessionsRoot) {
975
- const saved = listRecentSessions({ sessionsRoot: opts.sessionsRoot, limit: 1000 });
976
- const prefixed = saved.filter(
977
- s => s.sessionId === target.sessionIdOrPrefix || s.sessionId.startsWith(target.sessionIdOrPrefix),
978
- );
979
- const exact = prefixed.filter(s => s.sessionId === target.sessionIdOrPrefix);
980
- const resolved = exact.length > 0 ? exact : prefixed;
981
- if (resolved.length === 0) return { notFound: true };
982
- if (resolved.length > 1) {
983
- return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
984
- }
985
- const selected = resolved[0]!;
986
- resumeId = selected.sessionId;
987
- resumeCwd = selected.path;
1025
+ if (!target.path && !opts.agentDir && !opts.sessionsRoot) return { notFound: true };
1026
+ const recent = await listRecentSessions({
1027
+ cwd: target.path ?? opts.agentDir ?? opts.sessionsRoot ?? "",
1028
+ agentDir: opts.agentDir,
1029
+ sessionsRoot: opts.sessionsRoot,
1030
+ allWorkspaces: target.path === undefined,
1031
+ limit: 1000,
1032
+ });
1033
+ if (recent.kind === "error") throw new Error(`gjc_lifecycle_saved_sessions_unavailable: ${recent.message}`);
1034
+ const saved = recent.entries;
1035
+ const prefixed = saved.filter(
1036
+ s => s.sessionId === target.sessionIdOrPrefix || s.sessionId.startsWith(target.sessionIdOrPrefix),
1037
+ );
1038
+ const exact = prefixed.filter(s => s.sessionId === target.sessionIdOrPrefix);
1039
+ const resolved = exact.length > 0 ? exact : prefixed;
1040
+ if (resolved.length === 0) return { notFound: true };
1041
+ if (resolved.length > 1) {
1042
+ return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
988
1043
  }
1044
+ const selected = resolved[0]!;
1045
+ resumeId = selected.sessionId;
1046
+ resumeCwd = selected.path;
989
1047
  const resolvedResumeCwd = resumeCwd ? path.resolve(resumeCwd) : undefined;
990
1048
  const resumeCwdStat = resolvedResumeCwd ? fs.statSync(resolvedResumeCwd, { throwIfNoEntry: false }) : undefined;
991
1049
  if (typeof resolvedResumeCwd !== "string" || !resumeCwdStat?.isDirectory()) {
@@ -1139,11 +1197,7 @@ function lifecycleFailureResponse(requestId: unknown): SessionLifecycleResponse
1139
1197
  }
1140
1198
 
1141
1199
  function recordLifecycleControlDiagnostic(): void {
1142
- try {
1143
- process.stderr.write("gjc lifecycle control request failed\n");
1144
- } catch {
1145
- // Diagnostics must not suppress the fixed wire failure response or recovery.
1146
- }
1200
+ recordBoundedLifecycleDiagnostic("GJC lifecycle control request failed");
1147
1201
  }
1148
1202
 
1149
1203
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -1267,6 +1321,8 @@ export function buildOrchestratorDeps(input: {
1267
1321
  auditRedactionKey: Uint8Array;
1268
1322
  /** Root of saved session histories (`<agentDir>/sessions`), for resume resolution. */
1269
1323
  sessionsRoot?: string;
1324
+ /** Agent directory used for readonly managed-session resume resolution. */
1325
+ agentDir?: string;
1270
1326
  env?: NodeJS.ProcessEnv;
1271
1327
  }): OrchestratorDeps {
1272
1328
  if (input.auditRedactionKey.byteLength !== 32) throw new Error("invalid_audit_redaction_key");
@@ -1285,7 +1341,9 @@ export function buildOrchestratorDeps(input: {
1285
1341
  },
1286
1342
  spawnCreate: daemonSpawnCreate(env),
1287
1343
  closeSession: daemonCloseSession(env),
1288
- resumeSession: daemonResumeSession(env, { sessionsRoot: input.sessionsRoot }),
1344
+ resumeSession: daemonResumeSession(env, {
1345
+ agentDir: input.agentDir ?? path.dirname(input.agentNotificationsDir),
1346
+ }),
1289
1347
  };
1290
1348
  }
1291
1349
 
@@ -1,4 +1,4 @@
1
- const REQUIRED_WORKFLOW_ARBITRATION_METHODS = ["registerArbitratedAsk", "retireIfUnclaimed"] as const;
1
+ const REQUIRED_WORKFLOW_ARBITRATION_METHODS = ["registerArbitratedAsk", "retireIfUnclaimed", "stopAndWait"] as const;
2
2
 
3
3
  export class NativeRuntimeCompatibilityError extends Error {
4
4
  readonly code = "native_runtime_incompatible";
@@ -26,7 +26,11 @@ declare module "@gajae-code/natives" {
26
26
  onSdkFrame(callback: (err: null | Error, frame: SdkFrameEvent) => void): void;
27
27
  /** Register the connection-close callback. Must be called before start. */
28
28
  onConnectionClose(callback: (err: null | Error, connectionId: string) => void): void;
29
- /** Directed send of a JSON text frame to one connection. */
29
+ /** Register negotiated v3 client capabilities. Must be called before start. */
30
+ onNegotiatedCapabilities(
31
+ callback: (err: null | Error, connectionId: string, capabilities: string[]) => void,
32
+ ): void;
33
+ /** Directed delivery of a validated, bounded JSON v3 envelope to one connection. */
30
34
  sendTo(connectionId: string, json: string): void;
31
35
  /** Register a correlated workflow-gate action_needed frame. */
32
36
  registerWorkflowGateAsk(workflowJson: string, repliable: boolean): void;
@@ -34,5 +38,26 @@ declare module "@gajae-code/natives" {
34
38
  registerArbitratedAsk(actionJson: string, repliable: boolean): PresentationLease;
35
39
  /** Retire an exact private lease unless a generic reply has already claimed it. */
36
40
  retireIfUnclaimed(lease: PresentationLease): RetireIfUnclaimedResult;
41
+ /** Broadcast a TypeScript-constructed turn frame without JSON serde validation. */
42
+ pushTurnStreamUnchecked(
43
+ sessionId: string,
44
+ phase: "live" | "finalized",
45
+ text: string,
46
+ finalAnswer?: boolean,
47
+ messageRef?: string,
48
+ ): void;
49
+ /** Broadcast raw file bytes; Rust encodes the unchanged base64 wire field. */
50
+ pushFileAttachmentUnchecked(
51
+ sessionId: string,
52
+ name: string,
53
+ mime: string | undefined,
54
+ data: Buffer,
55
+ caption: string | undefined,
56
+ ): void;
57
+ /** Counters for known-good N-API frame crossings. */
58
+ knownGoodFrameStats(): {
59
+ knownGoodTurnStreamFrames: number;
60
+ turnStreamSerdeValidationParses: number;
61
+ };
37
62
  }
38
63
  }
@@ -10,8 +10,11 @@
10
10
  * artifacts belonging to a DEAD owner (dead-PID / explicitly-stale). It never
11
11
  * touches a live owner's lock/state and never kills a process.
12
12
  */
13
+ import * as crypto from "node:crypto";
14
+ import * as fsSync from "node:fs";
13
15
  import * as fsPromises from "node:fs/promises";
14
16
  import * as path from "node:path";
17
+ import * as native from "@gajae-code/natives";
15
18
  import type { Settings } from "../../config/settings";
16
19
  import {
17
20
  getNotificationConfig,
@@ -46,10 +49,32 @@ export function sanitizeDiagnostic(text: string, token?: string): string {
46
49
  return out.replace(TELEGRAM_TOKEN_PATTERN, "<redacted>");
47
50
  }
48
51
 
52
+ /** Identity evidence required to remove precisely the endpoint that was inspected. */
53
+ export interface NotificationEndpointFileIdentity {
54
+ dev: bigint;
55
+ ino: bigint;
56
+ size: bigint;
57
+ mtimeNs: bigint;
58
+ sha256: string;
59
+ }
60
+
61
+ export interface NotificationEndpointFile {
62
+ bytes: Buffer;
63
+ identity: NotificationEndpointFileIdentity;
64
+ }
65
+
66
+ export interface NotificationExactUnlinkResult {
67
+ ok: boolean;
68
+ code?: string;
69
+ detachedPath?: string;
70
+ }
71
+
49
72
  /** Minimal filesystem surface the service needs; injectable for tests. */
50
73
  export interface NotificationServiceFs {
51
74
  readdir(dir: string): Promise<string[]>;
52
75
  readFile(file: string, encoding: "utf8"): Promise<string>;
76
+ readEndpointFile(file: string): Promise<NotificationEndpointFile>;
77
+ exactUnlink(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
53
78
  unlink(file: string): Promise<void>;
54
79
  /**
55
80
  * Atomically create `file` with O_EXCL semantics: resolves `true` when this
@@ -63,6 +88,47 @@ export interface NotificationServiceFs {
63
88
  const nodeServiceFs: NotificationServiceFs = {
64
89
  readdir: dir => fsPromises.readdir(dir),
65
90
  readFile: (file, encoding) => fsPromises.readFile(file, encoding),
91
+ readEndpointFile: async file => {
92
+ const before = await fsPromises.lstat(file, { bigint: true });
93
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Endpoint is not a regular file");
94
+ const noFollow = fsSync.constants.O_NOFOLLOW;
95
+ const handle = await fsPromises.open(file, fsSync.constants.O_RDONLY | (noFollow ?? 0));
96
+ try {
97
+ const opened = await handle.stat({ bigint: true });
98
+ if (!opened.isFile() || !sameEndpointFileMetadata(before, opened))
99
+ throw new Error("Endpoint changed before it was opened");
100
+ const bytes = await handle.readFile();
101
+ const after = await handle.stat({ bigint: true });
102
+ const pathname = await fsPromises.lstat(file, { bigint: true });
103
+ if (
104
+ !after.isFile() ||
105
+ !pathname.isFile() ||
106
+ pathname.isSymbolicLink() ||
107
+ !sameEndpointFileMetadata(opened, after) ||
108
+ !sameEndpointFileMetadata(opened, pathname)
109
+ )
110
+ throw new Error("Endpoint changed while it was read");
111
+ return {
112
+ bytes,
113
+ identity: {
114
+ dev: opened.dev,
115
+ ino: opened.ino,
116
+ size: opened.size,
117
+ mtimeNs: opened.mtimeNs,
118
+ sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
119
+ },
120
+ };
121
+ } finally {
122
+ await handle.close();
123
+ }
124
+ },
125
+ exactUnlink: async (file, identity) => {
126
+ const result = native.exactUnlink(file, {
127
+ ...identity,
128
+ quarantineName: `.gjc-delete-notification-endpoint-${crypto.randomUUID()}.json`,
129
+ });
130
+ return { ok: result.ok, code: result.code, detachedPath: result.detachedPath };
131
+ },
66
132
  unlink: file => fsPromises.unlink(file),
67
133
  createExclusive: async file => {
68
134
  try {
@@ -99,7 +165,7 @@ function defaultStateRoot(): string {
99
165
  }
100
166
 
101
167
  function endpointDir(stateRoot: string): string {
102
- return path.join(stateRoot, "notifications");
168
+ return path.join(stateRoot, "sdk");
103
169
  }
104
170
 
105
171
  // --- status -------------------------------------------------------------
@@ -179,6 +245,11 @@ interface EndpointView {
179
245
  stale: boolean;
180
246
  }
181
247
 
248
+ type EndpointFileRead =
249
+ | { kind: "endpoint"; view: EndpointView; identity: NotificationEndpointFileIdentity }
250
+ | { kind: "non-endpoint" }
251
+ | { kind: "unreadable" };
252
+
182
253
  type EndpointLiveness = "live" | "dead" | "unknown";
183
254
 
184
255
  /**
@@ -194,25 +265,74 @@ function endpointLiveness(view: EndpointView, pidAlive: (pid: number) => boolean
194
265
  return pidAlive(view.pid) ? "live" : "dead";
195
266
  }
196
267
 
197
- async function readEndpointView(fs: NotificationServiceFs, file: string): Promise<EndpointView | undefined> {
268
+ function sameEndpointFileMetadata(
269
+ left: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint },
270
+ right: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint },
271
+ ): boolean {
272
+ return (
273
+ left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
274
+ );
275
+ }
276
+
277
+ function isLifecycleArtifact(record: Record<string, unknown>): boolean {
278
+ const keys = Object.keys(record);
279
+ const isEffectMarker =
280
+ typeof record.pid === "number" &&
281
+ Number.isSafeInteger(record.pid) &&
282
+ record.pid > 0 &&
283
+ typeof record.effectMarker === "string" &&
284
+ record.effectMarker.length > 0 &&
285
+ typeof record.incarnation === "string" &&
286
+ record.incarnation.length > 0;
287
+ if (isEffectMarker && keys.length === 3) return true;
288
+ return (
289
+ isEffectMarker &&
290
+ typeof record.phase === "string" &&
291
+ typeof record.reason === "string" &&
292
+ typeof record.message === "string" &&
293
+ typeof record.rollback === "object" &&
294
+ record.rollback !== null
295
+ );
296
+ }
297
+ function isCanonicalLifecycleArtifactName(name: string): boolean {
298
+ return (
299
+ name.endsWith(".lifecycle.json") ||
300
+ name.endsWith(".lifecycle.ready.json") ||
301
+ /^.+\.lifecycle\.failure\.[A-Za-z0-9._-]{1,128}\.json$/.test(name)
302
+ );
303
+ }
304
+
305
+ function unreadableEndpointResult(file: string): EndpointFileRead {
306
+ return isCanonicalLifecycleArtifactName(path.basename(file)) ? { kind: "non-endpoint" } : { kind: "unreadable" };
307
+ }
308
+
309
+ async function readEndpointView(fs: NotificationServiceFs, file: string): Promise<EndpointFileRead> {
310
+ let endpoint: NotificationEndpointFile;
198
311
  let raw: string;
199
312
  try {
200
- raw = await fs.readFile(file, "utf8");
313
+ endpoint = await fs.readEndpointFile(file);
314
+ raw = endpoint.bytes.toString("utf8");
201
315
  } catch {
202
- return undefined;
316
+ return unreadableEndpointResult(file);
203
317
  }
204
318
  let parsed: unknown;
205
319
  try {
206
320
  parsed = JSON.parse(raw);
207
321
  } catch {
208
- return undefined;
322
+ return unreadableEndpointResult(file);
209
323
  }
210
- if (!parsed || typeof parsed !== "object") return undefined;
324
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "non-endpoint" };
211
325
  const rec = parsed as Record<string, unknown>;
326
+ if (isLifecycleArtifact(rec) || typeof rec.url !== "string" || typeof rec.token !== "string")
327
+ return { kind: "non-endpoint" };
212
328
  return {
213
- sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
214
- pid: typeof rec.pid === "number" ? rec.pid : undefined,
215
- stale: rec.stale === true,
329
+ kind: "endpoint",
330
+ view: {
331
+ sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
332
+ pid: safePositiveInteger(rec.pid),
333
+ stale: rec.stale === true,
334
+ },
335
+ identity: endpoint.identity,
216
336
  };
217
337
  }
218
338
 
@@ -288,9 +408,13 @@ async function readDaemonStateFile(
288
408
  }
289
409
  }
290
410
 
411
+ function isSharedSdkArtifact(name: string): boolean {
412
+ return name === "broker.json";
413
+ }
414
+
291
415
  async function listEndpointFiles(fs: NotificationServiceFs, dir: string): Promise<string[]> {
292
416
  try {
293
- return (await fs.readdir(dir)).filter(name => name.endsWith(".json"));
417
+ return (await fs.readdir(dir)).filter(name => name.endsWith(".json") && !isSharedSdkArtifact(name));
294
418
  } catch {
295
419
  return [];
296
420
  }
@@ -468,11 +592,13 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
468
592
  let unknownEndpoints = 0;
469
593
  let unreadable = 0;
470
594
  for (const name of files) {
471
- const view = await readEndpointView(fs, path.join(dir, name));
472
- if (!view) {
595
+ const record = await readEndpointView(fs, path.join(dir, name));
596
+ if (record.kind === "non-endpoint") continue;
597
+ if (record.kind === "unreadable") {
473
598
  unreadable += 1;
474
599
  continue;
475
600
  }
601
+ const view = record.view;
476
602
  switch (endpointLiveness(view, pidAlive)) {
477
603
  case "live":
478
604
  live += 1;
@@ -485,12 +611,18 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
485
611
  break;
486
612
  }
487
613
  }
488
- const endpoints: EndpointHealth = { total: files.length, live, dead, unknown: unknownEndpoints, unreadable };
614
+ const endpoints: EndpointHealth = {
615
+ total: live + dead + unknownEndpoints + unreadable,
616
+ live,
617
+ dead,
618
+ unknown: unknownEndpoints,
619
+ unreadable,
620
+ };
489
621
  if (dead > 0 || unreadable > 0) {
490
622
  checks.push({
491
623
  name: "endpoints",
492
624
  level: "warn",
493
- detail: `${dead} dead / ${unreadable} unreadable of ${files.length} endpoint file(s); run recovery`,
625
+ detail: `${dead} dead / ${unreadable} unreadable of ${endpoints.total} endpoint file(s); run recovery`,
494
626
  });
495
627
  } else {
496
628
  checks.push({
@@ -633,6 +765,7 @@ export interface NotificationRecoveryReport {
633
765
  endpointsRemoved: RecoveredEndpoint[];
634
766
  endpointsKept: number;
635
767
  endpointsUnreadable: number;
768
+ endpointsDetached?: string[];
636
769
  daemon: {
637
770
  action: DaemonRecoveryAction;
638
771
  detail: string;
@@ -699,15 +832,19 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
699
832
  const dir = endpointDir(stateRoot);
700
833
  const files = await listEndpointFiles(fs, dir);
701
834
  const removed: RecoveredEndpoint[] = [];
835
+ const detached: string[] = [];
702
836
  let kept = 0;
703
837
  let unreadable = 0;
704
838
  for (const name of files) {
705
- const view = await readEndpointView(fs, path.join(dir, name));
706
- if (!view) {
839
+ const file = path.join(dir, name);
840
+ const record = await readEndpointView(fs, file);
841
+ if (record.kind === "non-endpoint") continue;
842
+ if (record.kind === "unreadable") {
707
843
  // Leave unparseable files untouched: they may be mid-write by a live server.
708
844
  unreadable += 1;
709
845
  continue;
710
846
  }
847
+ const view = record.view;
711
848
  if (endpointLiveness(view, pidAlive) !== "dead") {
712
849
  // Keep live AND unknown (PID-less) endpoints: only positive proof of
713
850
  // death (a stale tombstone or a dead pid) authorizes removal.
@@ -715,8 +852,17 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
715
852
  continue;
716
853
  }
717
854
  try {
718
- await fs.unlink(path.join(dir, name));
719
- removed.push({ sessionId: view.sessionId, pid: view.pid, reason: view.stale ? "stale-flag" : "dead-pid" });
855
+ const result = await fs.exactUnlink(file, record.identity);
856
+ if (!result.ok) {
857
+ if (result.detachedPath) detached.push(result.detachedPath);
858
+ else kept += 1;
859
+ continue;
860
+ }
861
+ removed.push({
862
+ sessionId: view.sessionId,
863
+ pid: view.pid,
864
+ reason: view.stale ? "stale-flag" : "dead-pid",
865
+ });
720
866
  } catch {
721
867
  kept += 1;
722
868
  }
@@ -782,10 +928,11 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
782
928
  }
783
929
 
784
930
  return {
785
- endpointsScanned: files.length,
931
+ endpointsScanned: removed.length + kept + unreadable + detached.length,
786
932
  endpointsRemoved: removed,
787
933
  endpointsKept: kept,
788
934
  endpointsUnreadable: unreadable,
935
+ endpointsDetached: detached,
789
936
  daemon,
790
937
  };
791
938
  }
@@ -794,11 +941,12 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
794
941
  export function formatNotificationRecoveryReport(report: NotificationRecoveryReport): string {
795
942
  const lines = ["Notification recovery"];
796
943
  lines.push(
797
- ` endpoints: scanned ${report.endpointsScanned}, removed ${report.endpointsRemoved.length}, kept ${report.endpointsKept}, unreadable ${report.endpointsUnreadable}`,
944
+ ` endpoints: scanned ${report.endpointsScanned}, removed ${report.endpointsRemoved.length}, kept ${report.endpointsKept}, unreadable ${report.endpointsUnreadable}, detached ${report.endpointsDetached?.length ?? 0}`,
798
945
  );
799
946
  for (const ep of report.endpointsRemoved) {
800
947
  lines.push(` - removed ${ep.sessionId} (pid ${ep.pid ?? "?"}, ${ep.reason})`);
801
948
  }
949
+ for (const detached of report.endpointsDetached ?? []) lines.push(` - retained detached endpoint ${detached}`);
802
950
  lines.push(` daemon: ${report.daemon.action} — ${report.daemon.detail}`);
803
951
  return lines.join("\n");
804
952
  }