@gajae-code/coding-agent 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,1733 +0,0 @@
1
- /**
2
- * Wires the authenticated Rust control endpoint (NotificationControlServer) to
3
- * the lifecycle orchestrator with REAL daemon-side effects: a daemon-safe tmux
4
- * launcher (create / cold-restart), force-close, and reattach-or-cold-restart
5
- * resume. Kept separate from telegram-daemon.ts so the effects + wiring are
6
- * unit-testable; the daemon calls {@link attachLifecycleControl} once it owns
7
- * the control server.
8
- */
9
- import * as crypto from "node:crypto";
10
- import * as fs from "node:fs";
11
- import * as fsPromises from "node:fs/promises";
12
- import * as path from "node:path";
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";
16
- import {
17
- MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
18
- MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
19
- MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
20
- MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
21
- MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
22
- } from "../../gjc-runtime/managed-owner-admission";
23
- import {
24
- MANAGED_OWNER_CHILD_TOKEN_ENV,
25
- MANAGED_OWNER_COMMAND_ENV,
26
- MANAGED_OWNER_INCARNATION_ENV,
27
- MANAGED_OWNER_RUN_ID_ENV,
28
- MANAGED_OWNER_SUPERVISOR_ARG,
29
- } from "../../gjc-runtime/managed-owner-supervisor";
30
- import { tmuxRuntimeSessionPath } from "../../gjc-runtime/session-layout";
31
- import {
32
- GJC_COORDINATOR_SESSION_ID_ENV,
33
- GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
34
- GJC_TMUX_OWNER_GENERATION_ENV,
35
- GJC_TMUX_OWNER_SERVER_KEY_ENV,
36
- GJC_TMUX_OWNER_STATE_DIR_ENV,
37
- } from "../../gjc-runtime/session-state-sidecar";
38
- import {
39
- buildGjcTmuxProfileCommands,
40
- buildGjcTmuxSessionSlug,
41
- resolveGjcTmuxBinary,
42
- resolveGjcTmuxCommand,
43
- } from "../../gjc-runtime/tmux-common";
44
- import {
45
- captureOwnerGenerationBaseline,
46
- classifyCgroup,
47
- isExactScopedBootstrapSuccessReceipt,
48
- type OwnerGenerationBaseline,
49
- type OwnerIsolationProbe,
50
- planTmuxOwnerIsolation,
51
- replaceOwnerGeneration,
52
- resolveManagedOwnerPredecessorSync,
53
- type TmuxServerProof,
54
- } from "../../gjc-runtime/tmux-owner-isolation";
55
- import {
56
- findGjcTmuxSessionByName,
57
- forceCloseGjcTmuxSession,
58
- type GjcTmuxSessionStatus,
59
- listGjcTmuxSessions,
60
- } from "../../gjc-runtime/tmux-sessions";
61
- import { processIncarnation } from "../broker/process-incarnation";
62
- import type {
63
- LifecycleErrorReason,
64
- ResumeCandidate,
65
- SessionCreateFrame,
66
- SessionLifecycleRequest,
67
- SessionLifecycleResponse,
68
- } from "./index";
69
- import { normalizeLifecyclePath } from "./lifecycle-commands";
70
- import {
71
- type AuditEvent,
72
- type CreateEffectResult,
73
- handleLifecycleRequest,
74
- type LedgerDoc,
75
- type LedgerEntry,
76
- type LedgerStore,
77
- type LifecycleOutcome,
78
- type OrchestratorDeps,
79
- type ResumeEffectResult,
80
- } from "./lifecycle-orchestrator";
81
- import { listRecentSessions } from "./recent-activity";
82
-
83
- const directLifecycleManagedOwnerEnvUnsets = [
84
- GJC_TMUX_OWNER_GENERATION_ENV,
85
- GJC_TMUX_OWNER_STATE_DIR_ENV,
86
- GJC_TMUX_OWNER_SERVER_KEY_ENV,
87
- MANAGED_OWNER_COMMAND_ENV,
88
- MANAGED_OWNER_RUN_ID_ENV,
89
- MANAGED_OWNER_INCARNATION_ENV,
90
- MANAGED_OWNER_CHILD_TOKEN_ENV,
91
- MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
92
- MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
93
- MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
94
- MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
95
- MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
96
- ] as const;
97
- const directLifecycleStabilizationMs = 250;
98
-
99
- function directLifecycleEnvArguments(env: Record<string, string>): string {
100
- return [
101
- ...directLifecycleManagedOwnerEnvUnsets.flatMap(name => ["-u", shellQuote(name)]),
102
- ...Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`),
103
- ].join(" ");
104
- }
105
-
106
- type NativeControlServerConstructor = new (
107
- token: string,
108
- ownerId: string,
109
- agentDir?: string,
110
- ) => NativeNotificationControlServer;
111
-
112
- interface NativeControlServerModule {
113
- NotificationControlServer: NativeControlServerConstructor;
114
- }
115
-
116
- /** Minimal view of the native control server this runtime depends on. */
117
- export interface ControlServerLike {
118
- onLifecycleRequest(
119
- cb: (err: Error | null, req: { kind: string; requestId: string; payloadJson: string }) => void,
120
- ): void;
121
- respond(responseJson: string): void;
122
- }
123
-
124
- /**
125
- * A startable control server (the native NotificationControlServer, or a fake in
126
- * tests). Extends {@link ControlServerLike} with the start/stop lifecycle the
127
- * daemon owns.
128
- */
129
- export interface LifecycleControlServer extends ControlServerLike {
130
- start(): Promise<unknown>;
131
- stop(): void;
132
- }
133
-
134
- /** Factory the daemon uses to construct a control server bound to its ownership. */
135
- export type LifecycleControlServerFactory = (input: {
136
- token: string;
137
- ownerId: string;
138
- agentDir: string;
139
- }) => LifecycleControlServer;
140
- const lifecycleErrorReasons = new Set<LifecycleErrorReason>([
141
- "unauthorized",
142
- "rate_limited",
143
- "duplicate_conflict",
144
- "invalid_target",
145
- "ambiguous_target",
146
- "spawn_failed",
147
- "discovery_timeout",
148
- "readiness_timeout",
149
- "close_refused",
150
- "not_found",
151
- "terminal_uncertain",
152
- "unsupported_platform",
153
- ]);
154
-
155
- function isLifecycleErrorReason(value: unknown): value is LifecycleErrorReason {
156
- return typeof value === "string" && lifecycleErrorReasons.has(value as LifecycleErrorReason);
157
- }
158
-
159
- /** Atomic + fsynced file-backed idempotency ledger store. */
160
- function hasNonDirectoryLedgerParent(idempotencyFile: string): boolean {
161
- let parent = path.dirname(idempotencyFile);
162
- while (true) {
163
- try {
164
- return !fs.statSync(parent).isDirectory();
165
- } catch (error) {
166
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
167
- const nextParent = path.dirname(parent);
168
- if (nextParent === parent) return false;
169
- parent = nextParent;
170
- }
171
- }
172
- }
173
-
174
- function ledgerReadError(error: unknown): Error {
175
- const errorCode = (error as NodeJS.ErrnoException | undefined)?.code;
176
- const code = typeof errorCode === "string" ? errorCode : "invalid";
177
- return new Error(`gjc_lifecycle_ledger_read_failed:${code.slice(0, 32)}`);
178
- }
179
-
180
- function isLedgerDoc(value: unknown): value is LedgerDoc {
181
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
182
- const doc = value as { version?: unknown; entries?: unknown };
183
- if (doc.version !== 1 || !doc.entries || typeof doc.entries !== "object" || Array.isArray(doc.entries)) return false;
184
- return Object.values(doc.entries).every(entry => {
185
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
186
- const candidate = entry as Partial<LedgerEntry>;
187
- const isBaseEntry =
188
- typeof candidate.requestHash === "string" &&
189
- (candidate.state === "in_progress" ||
190
- candidate.state === "success" ||
191
- candidate.state === "failure" ||
192
- candidate.state === "terminal_uncertain") &&
193
- typeof candidate.requestId === "string" &&
194
- (candidate.verb === "session_create" ||
195
- candidate.verb === "session_close" ||
196
- candidate.verb === "session_resume") &&
197
- typeof candidate.createdAt === "number" &&
198
- typeof candidate.updatedAt === "number" &&
199
- !!candidate.targetSummary &&
200
- typeof candidate.targetSummary === "object" &&
201
- !Array.isArray(candidate.targetSummary);
202
- if (!isBaseEntry) return false;
203
- if (candidate.state === "failure" || candidate.state === "terminal_uncertain")
204
- return isLifecycleErrorReason(candidate.reason);
205
- if (candidate.state !== "success") return true;
206
- switch (candidate.verb) {
207
- case "session_create":
208
- return (
209
- typeof candidate.intendedSessionId === "string" &&
210
- typeof candidate.sessionId === "string" &&
211
- typeof candidate.tmuxSession === "string" &&
212
- typeof candidate.endpointUrl === "string"
213
- );
214
- case "session_close":
215
- return typeof candidate.sessionId === "string" && typeof candidate.processGone === "boolean";
216
- case "session_resume":
217
- return (
218
- typeof candidate.sessionId === "string" &&
219
- typeof candidate.tmuxSession === "string" &&
220
- typeof candidate.endpointUrl === "string" &&
221
- (candidate.resumeMode === undefined ||
222
- candidate.resumeMode === "reattached" ||
223
- candidate.resumeMode === "cold_restarted")
224
- );
225
- }
226
- return false;
227
- });
228
- }
229
-
230
- function migrateLegacyLedger(doc: LedgerDoc): LedgerDoc {
231
- for (const entry of Object.values(doc.entries)) {
232
- if (entry.state === "success" && entry.verb === "session_resume" && entry.resumeMode === undefined) {
233
- entry.resumeMode = "reattached";
234
- }
235
- }
236
- return doc;
237
- }
238
-
239
- const LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT = 1;
240
- const LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT = 10;
241
- let lifecycleCompatibilityDiagnosticCount = 0;
242
- let lifecycleControlDiagnosticCount = 0;
243
-
244
- function isUnsupportedDirectorySyncError(error: unknown): boolean {
245
- const code = (error as NodeJS.ErrnoException | undefined)?.code;
246
- return (
247
- code === "EINVAL" ||
248
- code === "ENOTSUP" ||
249
- code === "EOPNOTSUPP" ||
250
- (process.platform === "win32" && code === "EPERM")
251
- );
252
- }
253
-
254
- function recordBoundedLifecycleDiagnostic(message: string, compatibility = false): void {
255
- if (compatibility) {
256
- if (lifecycleCompatibilityDiagnosticCount >= LIFECYCLE_COMPATIBILITY_DIAGNOSTIC_LIMIT) return;
257
- lifecycleCompatibilityDiagnosticCount++;
258
- } else {
259
- if (lifecycleControlDiagnosticCount >= LIFECYCLE_CONTROL_DIAGNOSTIC_LIMIT) return;
260
- lifecycleControlDiagnosticCount++;
261
- }
262
- try {
263
- logger.warn(message);
264
- } catch {
265
- // Diagnostics are best-effort and must not poison the lifecycle queue.
266
- }
267
- }
268
-
269
- function fsyncLedgerParentDirectory(directory: string): void {
270
- let dirFd: number;
271
- try {
272
- dirFd = fs.openSync(directory, "r");
273
- } catch (error) {
274
- if (process.platform === "win32" && isUnsupportedDirectorySyncError(error)) {
275
- recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
276
- return;
277
- }
278
- throw error;
279
- }
280
-
281
- let syncFailure: unknown;
282
- let closeFailure: unknown;
283
- try {
284
- fs.fsyncSync(dirFd);
285
- } catch (error) {
286
- if (process.platform === "win32" && isUnsupportedDirectorySyncError(error))
287
- recordBoundedLifecycleDiagnostic("GJC lifecycle ledger directory sync unsupported", true);
288
- else syncFailure = error;
289
- } finally {
290
- try {
291
- fs.closeSync(dirFd);
292
- } catch (error) {
293
- closeFailure = error;
294
- }
295
- }
296
- if (syncFailure !== undefined && closeFailure !== undefined)
297
- throw new AggregateError([syncFailure, closeFailure], "ledger directory sync failed");
298
- if (syncFailure !== undefined) throw syncFailure;
299
- if (closeFailure !== undefined) throw closeFailure;
300
- }
301
-
302
- export function fileLedgerStore(idempotencyFile: string): LedgerStore {
303
- return {
304
- async read(): Promise<LedgerDoc> {
305
- let contents: string;
306
- try {
307
- contents = fs.readFileSync(idempotencyFile, "utf8");
308
- } catch (error) {
309
- const code = (error as NodeJS.ErrnoException).code;
310
- if (code === "ENOENT") {
311
- let hasNonDirectoryParent: boolean;
312
- try {
313
- hasNonDirectoryParent = hasNonDirectoryLedgerParent(idempotencyFile);
314
- } catch (parentError) {
315
- throw ledgerReadError(parentError);
316
- }
317
- if (hasNonDirectoryParent) throw ledgerReadError({ code: "ENOTDIR" });
318
- return { version: 1, entries: {} };
319
- }
320
- throw ledgerReadError(error);
321
- }
322
- try {
323
- const doc = JSON.parse(contents) as unknown;
324
- if (!isLedgerDoc(doc)) throw new Error("invalid");
325
- return migrateLegacyLedger(doc);
326
- } catch (error) {
327
- throw ledgerReadError(error);
328
- }
329
- },
330
- async write(doc: LedgerDoc): Promise<void> {
331
- fs.mkdirSync(path.dirname(idempotencyFile), { recursive: true });
332
- const tmp = `${idempotencyFile}.${process.pid}.${Date.now()}.tmp`;
333
- let fd: number | undefined;
334
- let writeFailure: unknown;
335
- let closeFailure: unknown;
336
- try {
337
- fd = fs.openSync(tmp, "w", 0o600);
338
- const encodedDoc = Buffer.from(JSON.stringify(doc), "utf8");
339
- let offset = 0;
340
- while (offset < encodedDoc.length) {
341
- const written = fs.writeSync(fd, encodedDoc, offset, encodedDoc.length - offset);
342
- if (written === 0) throw new Error("Short write");
343
- offset += written;
344
- }
345
- fs.fsyncSync(fd);
346
- } catch (error) {
347
- writeFailure = error;
348
- } finally {
349
- if (fd !== undefined) {
350
- try {
351
- fs.closeSync(fd);
352
- } catch (error) {
353
- closeFailure = error;
354
- }
355
- }
356
- }
357
- if (writeFailure !== undefined || closeFailure !== undefined) {
358
- try {
359
- fs.unlinkSync(tmp);
360
- } catch {
361
- // The failed temporary file is never published.
362
- }
363
- if (writeFailure !== undefined && closeFailure !== undefined)
364
- throw new AggregateError([writeFailure, closeFailure], "ledger temporary write failed");
365
- throw writeFailure ?? closeFailure;
366
- }
367
- fs.renameSync(tmp, idempotencyFile);
368
- // The temp-file fsync does not persist the rename's directory entry.
369
- fsyncLedgerParentDirectory(path.dirname(idempotencyFile));
370
- },
371
- };
372
- }
373
-
374
- /** Append-only JSONL audit sink (0600). Never receives tokens or raw prompts. */
375
- export function fileAudit(auditPath: string): (e: AuditEvent) => void {
376
- return (e: AuditEvent) => {
377
- fs.mkdirSync(path.dirname(auditPath), { recursive: true });
378
- fs.appendFileSync(auditPath, `${JSON.stringify(e)}\n`, { mode: 0o600 });
379
- };
380
- }
381
-
382
- /** Simple per-chat sliding-window create rate limiter. */
383
- export function createRateLimiter(maxPerWindow: number, windowMs: number): (chatId: string, nowMs: number) => boolean {
384
- const hits = new Map<string, number[]>();
385
- return (chatId: string, nowMs: number) => {
386
- const arr = (hits.get(chatId) ?? []).filter(t => nowMs - t < windowMs);
387
- if (arr.length >= maxPerWindow) {
388
- hits.set(chatId, arr);
389
- return false;
390
- }
391
- arr.push(nowMs);
392
- hits.set(chatId, arr);
393
- return true;
394
- };
395
- }
396
-
397
- function tmuxSessionNameFor(sessionId: string): string {
398
- return `gjc_lc_${sessionId}`;
399
- }
400
-
401
- /** Build the `gjc` argv for a create target (existing path / worktree / dir).
402
- *
403
- * The launched session id is carried via `GJC_SESSION_ID` in the child env (see
404
- * {@link daemonSpawnCreate}); the root `gjc` launcher has no `--session-id`
405
- * flag, so it must never appear in argv. Only flags the launch parser actually
406
- * supports are emitted (`--worktree <branch>` for worktree targets,
407
- * `--mpreset <profile>` for model presets). */
408
- export function buildCreateArgv(
409
- frame: SessionCreateFrame,
410
- _ids: { intendedSessionId: string; startupPromptRef?: string },
411
- ): { cwd: string; args: string[] } {
412
- const extraArgs: string[] = [];
413
- if (frame.modelPreset) {
414
- extraArgs.push("--mpreset", frame.modelPreset);
415
- }
416
- if (frame.target.kind === "worktree") {
417
- const cwd = normalizeLifecyclePath(frame.target.repo);
418
- if (!cwd) throw new Error("invalid_lifecycle_repo_path");
419
- // Use the `--worktree=<branch>` form so the branch is a single argv token:
420
- // a flag-shaped branch (e.g. `-x`) can never be mis-parsed as a separate
421
- // launcher flag / detached-mode trigger.
422
- return { cwd, args: [`--worktree=${frame.target.branch}`, ...extraArgs] };
423
- }
424
- const cwd = normalizeLifecyclePath(frame.target.path);
425
- if (!cwd) throw new Error("invalid_lifecycle_path");
426
- return { cwd, args: extraArgs };
427
- }
428
-
429
- function isKnownNoServerDiagnostic(stderr: string): boolean {
430
- const diagnostic = stderr.trim().toLowerCase();
431
- return (
432
- diagnostic.length > 0 &&
433
- diagnostic.length <= 512 &&
434
- /no server running|failed to connect to server|error connecting to/.test(diagnostic)
435
- );
436
- }
437
-
438
- function lifecycleOwnerIsolationProbe(tmux: string, env: NodeJS.ProcessEnv): OwnerIsolationProbe {
439
- return {
440
- readCallerCgroup: async () =>
441
- process.platform === "linux" ? await fsPromises.readFile("/proc/self/cgroup", "utf8").catch(() => null) : null,
442
- probeServer: async (serverKey: string): Promise<TmuxServerProof> => {
443
- const result = Bun.spawnSync([tmux, "-L", serverKey, "list-sessions", "-F", "#{pid}\t#{session_name}"], {
444
- stdout: "pipe",
445
- stderr: "pipe",
446
- env,
447
- });
448
- const stderr = result.stderr.toString();
449
- if (result.exitCode !== 0)
450
- return isKnownNoServerDiagnostic(stderr) ? { state: "absent" } : { state: "unverifiable" };
451
- const lines = result.stdout
452
- .toString()
453
- .split("\n")
454
- .map(line => line.trim())
455
- .filter(Boolean);
456
- const [pidText] = lines[0]?.split("\t") ?? [];
457
- const pid = Number(pidText);
458
- const sessionNames = lines.map(line => line.split("\t")[1]).filter((name): name is string => Boolean(name));
459
- if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
460
- if (process.platform !== "linux")
461
- return {
462
- state: "safe",
463
- pid,
464
- startTime: "not_applicable",
465
- cgroup: { classification: "not_applicable" },
466
- sessionNames,
467
- };
468
- const [cgroupText, startTime] = await Promise.all([
469
- fsPromises.readFile(`/proc/${pid}/cgroup`, "utf8").catch(() => null),
470
- readLinuxProcStartTime(pid),
471
- ]);
472
- const cgroup = classifyCgroup({ platform: process.platform, cgroupText });
473
- if (!startTime) return { state: "unverifiable", pid, cgroup, sessionNames };
474
- return {
475
- state:
476
- cgroup.classification === "safe"
477
- ? "safe"
478
- : cgroup.classification === "unsafe_service"
479
- ? "unsafe"
480
- : "unverifiable",
481
- pid,
482
- startTime,
483
- cgroup,
484
- sessionNames,
485
- };
486
- },
487
- };
488
- }
489
-
490
- function lifecycleRuntimeStateFile(cwd: string, sessionId: string, tmuxSession: string): string {
491
- return tmuxRuntimeSessionPath(cwd, sessionId, buildGjcTmuxSessionSlug(tmuxSession));
492
- }
493
-
494
- async function preflightLifecycleTmuxOwner(input: {
495
- tmux: string;
496
- env: NodeJS.ProcessEnv;
497
- ownerIsolationProbe?: OwnerIsolationProbe;
498
- }): Promise<void> {
499
- const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
500
- let server: TmuxServerProof;
501
- try {
502
- server = await probe.probeServer("default");
503
- } catch {
504
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
505
- }
506
- if (server.state === "unsafe") throw new Error("gjc_lifecycle_owner_server_unsafe");
507
- if (server.state === "unverifiable") throw new Error("gjc_lifecycle_owner_server_unverifiable");
508
- if (
509
- server.state === "safe" &&
510
- (!server.pid || !server.startTime || (process.platform === "linux" && server.cgroup?.classification !== "safe"))
511
- )
512
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
513
- if (server.state === "absent") {
514
- try {
515
- if (
516
- classifyCgroup({ platform: process.platform, cgroupText: await probe.readCallerCgroup() })
517
- .classification === "unverifiable"
518
- )
519
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
520
- } catch (error) {
521
- if (error instanceof Error && error.message === "gjc_lifecycle_owner_server_unverifiable") throw error;
522
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
523
- }
524
- }
525
- }
526
-
527
- interface LifecycleAttemptExecution {
528
- attemptCreated: true;
529
- serverKey: string;
530
- serverPid?: number;
531
- serverStartTime?: string;
532
- nativeSessionId?: string;
533
- attemptSession: string;
534
- }
535
-
536
- function nativeTmuxSessionIdFromSpawn(stdout: string): string | undefined {
537
- const value = stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout;
538
- return !value.includes("\n") && !value.includes("\r") && /^\$\d+$/.test(value) ? value : undefined;
539
- }
540
-
541
- async function isLifecycleGenerationUnchanged(
542
- stateDir: string,
543
- sessionId: string,
544
- previousBaseline: OwnerGenerationBaseline,
545
- ): Promise<boolean> {
546
- try {
547
- return (
548
- JSON.stringify(await captureOwnerGenerationBaseline(stateDir, sessionId)) === JSON.stringify(previousBaseline)
549
- );
550
- } catch {
551
- return false;
552
- }
553
- }
554
-
555
- async function executeLifecycleTmuxOwnerPlan(input: {
556
- tmux: string;
557
- env: NodeJS.ProcessEnv;
558
- sessionId: string;
559
- generation: string;
560
- stateDir: string;
561
- cwd: string;
562
- argv: string[];
563
- sessionName: string;
564
- ownerIsolationProbe?: OwnerIsolationProbe;
565
- prepareSpawn?: () => void;
566
- onAttemptCreated?: (attempt: LifecycleAttemptExecution) => void;
567
- previousBaseline: OwnerGenerationBaseline;
568
- }): Promise<LifecycleAttemptExecution> {
569
- const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
570
- await preflightLifecycleTmuxOwner(input);
571
- if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
572
- throw new Error("gjc_lifecycle_owner_generation_changed");
573
- const plan = await planTmuxOwnerIsolation(
574
- {
575
- schema_version: 1,
576
- op: "plan",
577
- platform: process.platform,
578
- session_id: input.sessionId,
579
- owner_generation: input.generation,
580
- baseline: input.previousBaseline,
581
- cwd: input.cwd,
582
- state_dir: input.stateDir,
583
- socket_key: "default",
584
- tmux_argv: input.argv,
585
- },
586
- probe,
587
- );
588
- if (!plan.ok) throw new Error(`gjc_lifecycle_owner_${plan.code}`);
589
- const preSpawnProof =
590
- plan.execution.mode === "direct" && plan.server_state === "safe"
591
- ? await probe.probeServer(plan.execution.server_key)
592
- : undefined;
593
- if (
594
- preSpawnProof &&
595
- (preSpawnProof.state !== "safe" ||
596
- !preSpawnProof.pid ||
597
- !preSpawnProof.startTime ||
598
- (process.platform === "linux" && preSpawnProof.cgroup?.classification !== "safe"))
599
- )
600
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
601
- input.prepareSpawn?.();
602
- if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
603
- throw new Error("gjc_lifecycle_owner_generation_changed");
604
- const created = Bun.spawnSync(plan.execution.argv, {
605
- stdout: "pipe",
606
- stderr: "pipe",
607
- env: input.env,
608
- ...(plan.execution.mode === "scoped" ? { stdin: new TextEncoder().encode(plan.execution.stdin_line) } : {}),
609
- });
610
- const rawStdout = created.stdout.toString();
611
- const rawScopedReceipt =
612
- plan.execution.mode === "scoped" && isExactScopedBootstrapSuccessReceipt(rawStdout)
613
- ? (JSON.parse(rawStdout.endsWith("\n") ? rawStdout.slice(0, -1) : rawStdout) as {
614
- native_session_id: string;
615
- server_pid: number;
616
- server_start_time: string;
617
- session_name: string;
618
- })
619
- : undefined;
620
- const scopedReceipt =
621
- rawScopedReceipt?.session_name === plan.execution.attempt_session ? rawScopedReceipt : undefined;
622
- const attempt: LifecycleAttemptExecution = {
623
- attemptCreated: true,
624
- serverKey: plan.execution.server_key,
625
- nativeSessionId:
626
- plan.execution.mode === "scoped" ? scopedReceipt?.native_session_id : nativeTmuxSessionIdFromSpawn(rawStdout),
627
- attemptSession: plan.execution.attempt_session,
628
- ...(scopedReceipt
629
- ? { serverPid: scopedReceipt.server_pid, serverStartTime: scopedReceipt.server_start_time }
630
- : preSpawnProof
631
- ? { serverPid: preSpawnProof.pid, serverStartTime: preSpawnProof.startTime }
632
- : {}),
633
- };
634
- input.onAttemptCreated?.(attempt);
635
- if (created.exitCode !== 0) {
636
- if (!attempt.nativeSessionId || attempt.serverPid === undefined || attempt.serverStartTime === undefined)
637
- throw new Error("gjc_lifecycle_spawn_failed_cleanup_uncertain");
638
- throw new Error("gjc_lifecycle_spawn_failed");
639
- }
640
- if (!attempt.nativeSessionId) throw new Error("gjc_lifecycle_spawn_failed_cleanup_uncertain");
641
- const proof = await probe.probeServer(plan.execution.server_key);
642
- if (
643
- proof.state !== "safe" ||
644
- !proof.pid ||
645
- !proof.startTime ||
646
- !proof.sessionNames?.includes(plan.execution.attempt_session) ||
647
- (process.platform === "linux" && proof.cgroup?.classification !== "safe") ||
648
- (preSpawnProof && (proof.pid !== preSpawnProof.pid || proof.startTime !== preSpawnProof.startTime)) ||
649
- (scopedReceipt && (proof.pid !== scopedReceipt.server_pid || proof.startTime !== scopedReceipt.server_start_time))
650
- )
651
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
652
- attempt.serverPid = proof.pid;
653
- attempt.serverStartTime = proof.startTime;
654
- if (
655
- !(await reproveLifecycleAttempt({
656
- tmux: input.tmux,
657
- env: input.env,
658
- serverKey: attempt.serverKey,
659
- nativeSessionId: attempt.nativeSessionId,
660
- attemptSession: attempt.attemptSession,
661
- expectedServerPid: attempt.serverPid,
662
- expectedServerStartTime: attempt.serverStartTime,
663
- ownerIsolationProbe: input.ownerIsolationProbe,
664
- }))
665
- )
666
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
667
-
668
- if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, input.previousBaseline)))
669
- throw new Error("gjc_lifecycle_owner_generation_changed");
670
- return attempt;
671
- }
672
-
673
- function hasExactNativeSessionBinding(input: {
674
- tmux: string;
675
- env: NodeJS.ProcessEnv;
676
- serverKey: string;
677
- nativeSessionId: string;
678
- attemptSession: string;
679
- }): boolean {
680
- const result = Bun.spawnSync(
681
- [
682
- input.tmux,
683
- "-L",
684
- input.serverKey,
685
- "display-message",
686
- "-p",
687
- "-t",
688
- input.nativeSessionId,
689
- "#{session_id}\t#{session_name}",
690
- ],
691
- { stdout: "pipe", stderr: "pipe", env: input.env },
692
- );
693
- if (result.exitCode !== 0) return false;
694
- const output = result.stdout.toString();
695
- const line = output.endsWith("\n") ? output.slice(0, -1) : output;
696
- return line === `${input.nativeSessionId}\t${input.attemptSession}`;
697
- }
698
-
699
- function cleanupUncertain(): Error {
700
- return new Error("gjc_lifecycle_cleanup_uncertain");
701
- }
702
-
703
- async function reproveLifecycleAttempt(input: {
704
- tmux: string;
705
- env: NodeJS.ProcessEnv;
706
- serverKey: string;
707
- nativeSessionId: string;
708
- attemptSession: string;
709
- expectedServerPid: number;
710
- expectedServerStartTime: string;
711
- ownerIsolationProbe?: OwnerIsolationProbe;
712
- }): Promise<boolean> {
713
- const probe = input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env);
714
- const isExpectedServer = (proof: TmuxServerProof): boolean =>
715
- proof.state === "safe" &&
716
- proof.pid === input.expectedServerPid &&
717
- proof.startTime === input.expectedServerStartTime &&
718
- proof.sessionNames?.includes(input.attemptSession) === true &&
719
- (process.platform !== "linux" || proof.cgroup?.classification === "safe");
720
- let proof: TmuxServerProof;
721
- try {
722
- proof = await probe.probeServer(input.serverKey);
723
- } catch {
724
- return false;
725
- }
726
- if (!isExpectedServer(proof) || !hasExactNativeSessionBinding(input)) return false;
727
- try {
728
- proof = await probe.probeServer(input.serverKey);
729
- } catch {
730
- return false;
731
- }
732
- return isExpectedServer(proof);
733
- }
734
-
735
- async function cleanupLifecycleAttempt(input: {
736
- tmux: string;
737
- env: NodeJS.ProcessEnv;
738
- serverKey: string;
739
- nativeSessionId?: string;
740
- attemptSession: string;
741
- expectedServerPid?: number;
742
- expectedServerStartTime?: string;
743
- ownerIsolationProbe?: OwnerIsolationProbe;
744
- }): Promise<void> {
745
- if (!input.nativeSessionId || input.expectedServerPid === undefined || input.expectedServerStartTime === undefined)
746
- throw cleanupUncertain();
747
- // External reproof narrows the window and detects obvious replacement before
748
- // mutation, but the server evaluates the identity predicate atomically.
749
- if (
750
- !(await reproveLifecycleAttempt({
751
- tmux: input.tmux,
752
- env: input.env,
753
- serverKey: input.serverKey,
754
- nativeSessionId: input.nativeSessionId,
755
- attemptSession: input.attemptSession,
756
- expectedServerPid: input.expectedServerPid,
757
- expectedServerStartTime: input.expectedServerStartTime,
758
- ownerIsolationProbe: input.ownerIsolationProbe,
759
- }))
760
- )
761
- throw cleanupUncertain();
762
- const guarded = Bun.spawnSync(
763
- [
764
- input.tmux,
765
- "-L",
766
- input.serverKey,
767
- "if-shell",
768
- "-t",
769
- input.nativeSessionId,
770
- "-F",
771
- lifecycleMetadataPredicate(input.expectedServerPid, input.nativeSessionId, input.attemptSession),
772
- `kill-session -t '${input.nativeSessionId}' ; display-message -p __gjc_lifecycle_cleanup_ok__`,
773
- "display-message -p __gjc_lifecycle_cleanup_refused__",
774
- ],
775
- { stdout: "pipe", stderr: "pipe", env: input.env },
776
- );
777
- if (guarded.exitCode !== 0 || guarded.stdout.toString().trim() !== "__gjc_lifecycle_cleanup_ok__")
778
- throw cleanupUncertain();
779
- }
780
-
781
- async function assertLifecycleTmuxServerSafe(input: {
782
- tmux: string;
783
- env: NodeJS.ProcessEnv;
784
- ownerIsolationProbe?: OwnerIsolationProbe;
785
- }): Promise<void> {
786
- const proof = await (input.ownerIsolationProbe ?? lifecycleOwnerIsolationProbe(input.tmux, input.env)).probeServer(
787
- "default",
788
- );
789
- if (
790
- proof.state !== "safe" ||
791
- !proof.pid ||
792
- !proof.startTime ||
793
- (process.platform === "linux" && proof.cgroup?.classification !== "safe")
794
- )
795
- throw new Error(`gjc_lifecycle_owner_${proof.state === "unsafe" ? "server_unsafe" : "server_unverifiable"}`);
796
- }
797
-
798
- async function completeLifecycleSpawnTransaction(input: {
799
- tmux: string;
800
- env: NodeJS.ProcessEnv;
801
- sessionId: string;
802
- generation: string;
803
- stateDir: string;
804
- cwd: string;
805
- sessionName: string;
806
- sessionStateFile: string;
807
- argv: string[];
808
- ownerIsolationProbe?: OwnerIsolationProbe;
809
- prepareSpawn?: () => void;
810
- previousBaseline?: OwnerGenerationBaseline;
811
- }): Promise<void> {
812
- const previousBaseline =
813
- input.previousBaseline ?? (await captureOwnerGenerationBaseline(input.stateDir, input.sessionId));
814
- let ownerExecution: LifecycleAttemptExecution | undefined;
815
- try {
816
- ownerExecution = await executeLifecycleTmuxOwnerPlan({
817
- ...input,
818
- previousBaseline,
819
- onAttemptCreated: attempt => {
820
- ownerExecution = attempt;
821
- },
822
- });
823
- if (
824
- !ownerExecution.nativeSessionId ||
825
- ownerExecution.serverPid === undefined ||
826
- ownerExecution.serverStartTime === undefined
827
- )
828
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
829
- await applyRequiredLifecycleTmuxMetadata(
830
- input.tmux,
831
- `${ownerExecution.nativeSessionId}:`,
832
- input.env,
833
- {
834
- sessionId: input.sessionId,
835
- sessionStateFile: input.sessionStateFile,
836
- project: input.cwd,
837
- ownerGeneration: input.generation,
838
- ownerServerKey: ownerExecution.serverKey,
839
- },
840
- {
841
- nativeSessionId: ownerExecution.nativeSessionId,
842
- attemptSession: ownerExecution.attemptSession,
843
- serverPid: ownerExecution.serverPid,
844
- },
845
- );
846
- if (
847
- !(await reproveLifecycleAttempt({
848
- tmux: input.tmux,
849
- env: input.env,
850
- serverKey: ownerExecution.serverKey,
851
- nativeSessionId: ownerExecution.nativeSessionId,
852
- attemptSession: ownerExecution.attemptSession,
853
- expectedServerPid: ownerExecution.serverPid,
854
- expectedServerStartTime: ownerExecution.serverStartTime,
855
- ownerIsolationProbe: input.ownerIsolationProbe,
856
- }))
857
- )
858
- throw new Error("gjc_lifecycle_owner_server_unverifiable");
859
- if (!(await isLifecycleGenerationUnchanged(input.stateDir, input.sessionId, previousBaseline)))
860
- throw new Error("gjc_lifecycle_owner_generation_changed");
861
- resolveManagedOwnerPredecessorSync(input.stateDir, input.sessionId, previousBaseline);
862
- await replaceOwnerGeneration(input.stateDir, input.sessionId, input.generation, previousBaseline);
863
- } catch (error) {
864
- let cleanupFailure: unknown;
865
- if (ownerExecution?.attemptCreated) {
866
- try {
867
- await cleanupLifecycleAttempt({
868
- tmux: input.tmux,
869
- env: input.env,
870
- serverKey: ownerExecution.serverKey,
871
- nativeSessionId: ownerExecution.nativeSessionId,
872
- attemptSession: ownerExecution.attemptSession,
873
- expectedServerPid: ownerExecution.serverPid,
874
- expectedServerStartTime: ownerExecution.serverStartTime,
875
- ownerIsolationProbe: input.ownerIsolationProbe,
876
- });
877
- } catch (cleanupError) {
878
- cleanupFailure = cleanupError;
879
- }
880
- }
881
- if (cleanupFailure !== undefined)
882
- throw new AggregateError([error, cleanupFailure], "gjc_lifecycle_cleanup_uncertain");
883
- throw error;
884
- }
885
- }
886
- function directTmuxReceipt(
887
- stdout: string,
888
- sessionName: string,
889
- ): { nativeSessionId: string; sessionName: string } | undefined {
890
- const line = stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout;
891
- const [nativeSessionId, receivedName, ...rest] = line.split("\t");
892
- return rest.length === 0 && /^\$\d+$/.test(nativeSessionId) && receivedName === sessionName
893
- ? { nativeSessionId, sessionName: receivedName }
894
- : undefined;
895
- }
896
-
897
- type DirectTmuxBinding =
898
- | { state: "bound"; serverPid: number; panePid: number; paneDead: boolean; paneDeadStatus: string }
899
- | { state: "absent" }
900
- | { state: "unverifiable" };
901
-
902
- function isKnownNoSessionDiagnostic(stderr: string): boolean {
903
- const diagnostic = stderr.trim().toLowerCase();
904
- return (
905
- diagnostic.length > 0 &&
906
- diagnostic.length <= 512 &&
907
- /no server running|can't find session|no such session|unknown session/.test(diagnostic)
908
- );
909
- }
910
-
911
- function directTmuxBinding(
912
- tmux: string,
913
- env: NodeJS.ProcessEnv,
914
- cwd: string,
915
- nativeSessionId: string,
916
- sessionName: string,
917
- ): DirectTmuxBinding {
918
- const result = Bun.spawnSync(
919
- [
920
- tmux,
921
- "display-message",
922
- "-p",
923
- "-t",
924
- nativeSessionId,
925
- "#{pid}\t#{session_id}\t#{session_name}\t#{pane_pid}\t#{pane_dead}\t#{pane_dead_status}",
926
- ],
927
- { stdout: "pipe", stderr: "pipe", env, cwd },
928
- );
929
- if (result.exitCode !== 0)
930
- return isKnownNoSessionDiagnostic(result.stderr.toString()) ? { state: "absent" } : { state: "unverifiable" };
931
- const output = result.stdout.toString();
932
- const line = output.endsWith("\n") ? output.slice(0, -1) : output;
933
- const [pid, receivedId, receivedName, panePid, paneDead, paneDeadStatus, ...rest] = line.split("\t");
934
- return rest.length === 0 &&
935
- /^\d+$/.test(pid) &&
936
- Number(pid) > 0 &&
937
- receivedId === nativeSessionId &&
938
- receivedName === sessionName &&
939
- /^\d+$/.test(panePid) &&
940
- Number(panePid) > 0 &&
941
- (paneDead === "0" || paneDead === "1")
942
- ? {
943
- state: "bound",
944
- serverPid: Number(pid),
945
- panePid: Number(panePid),
946
- paneDead: paneDead === "1",
947
- paneDeadStatus,
948
- }
949
- : { state: "unverifiable" };
950
- }
951
-
952
- function directTmuxIdentityIsCurrent(input: {
953
- tmux: string;
954
- env: NodeJS.ProcessEnv;
955
- cwd: string;
956
- nativeSessionId: string;
957
- sessionName: string;
958
- serverPid: number;
959
- serverIncarnation: string;
960
- readProcessIncarnation: (pid: number) => string | undefined;
961
- }): boolean {
962
- const binding = directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName);
963
- return (
964
- binding.state === "bound" &&
965
- binding.serverPid === input.serverPid &&
966
- input.readProcessIncarnation(input.serverPid) === input.serverIncarnation
967
- );
968
- }
969
- function directTmuxPaneLivenessIsCurrent(input: {
970
- tmux: string;
971
- env: NodeJS.ProcessEnv;
972
- cwd: string;
973
- nativeSessionId: string;
974
- sessionName: string;
975
- serverPid: number;
976
- serverIncarnation: string;
977
- panePid: number;
978
- paneIncarnation: string;
979
- readProcessIncarnation: (pid: number) => string | undefined;
980
- }): boolean {
981
- const binding = directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName);
982
- return (
983
- binding.state === "bound" &&
984
- binding.serverPid === input.serverPid &&
985
- input.readProcessIncarnation(input.serverPid) === input.serverIncarnation &&
986
- !binding.paneDead &&
987
- binding.panePid === input.panePid &&
988
- input.readProcessIncarnation(input.panePid) === input.paneIncarnation
989
- );
990
- }
991
-
992
- function directTmuxGuardedMutation(input: {
993
- tmux: string;
994
- env: NodeJS.ProcessEnv;
995
- cwd: string;
996
- nativeSessionId: string;
997
- sessionName: string;
998
- serverPid: number;
999
- serverIncarnation: string;
1000
- readProcessIncarnation: (pid: number) => string | undefined;
1001
- commands: string;
1002
- success: string;
1003
- }): boolean {
1004
- if (!directTmuxIdentityIsCurrent(input)) return false;
1005
- const result = Bun.spawnSync(
1006
- [
1007
- input.tmux,
1008
- "if-shell",
1009
- "-t",
1010
- input.nativeSessionId,
1011
- "-F",
1012
- lifecycleMetadataPredicate(input.serverPid, input.nativeSessionId, input.sessionName),
1013
- `${input.commands} ; display-message -p ${input.success}`,
1014
- "display-message -p __gjc_lifecycle_metadata_refused__",
1015
- ],
1016
- { stdout: "pipe", stderr: "pipe", env: input.env, cwd: input.cwd },
1017
- );
1018
- return result.exitCode === 0 && result.stdout.toString().trim() === input.success;
1019
- }
1020
-
1021
- function cleanupDirectLifecycleAttempt(input: {
1022
- tmux: string;
1023
- env: NodeJS.ProcessEnv;
1024
- cwd: string;
1025
- nativeSessionId: string;
1026
- sessionName: string;
1027
- serverPid: number;
1028
- serverIncarnation: string;
1029
- readProcessIncarnation: (pid: number) => string | undefined;
1030
- }): void {
1031
- if (
1032
- !directTmuxGuardedMutation({
1033
- ...input,
1034
- commands: `kill-session -t '${input.nativeSessionId}'`,
1035
- success: "__gjc_lifecycle_cleanup_ok__",
1036
- })
1037
- )
1038
- throw cleanupUncertain();
1039
- if (directTmuxBinding(input.tmux, input.env, input.cwd, input.nativeSessionId, input.sessionName).state !== "absent")
1040
- throw cleanupUncertain();
1041
- }
1042
-
1043
- async function completeNonLinuxLifecycleSpawn(input: {
1044
- tmux: string;
1045
- env: NodeJS.ProcessEnv;
1046
- sessionId: string;
1047
- cwd: string;
1048
- sessionName: string;
1049
- sessionStateFile: string;
1050
- command: string;
1051
- readProcessIncarnation: (pid: number) => string | undefined;
1052
- }): Promise<void> {
1053
- let attempt:
1054
- | {
1055
- nativeSessionId: string;
1056
- sessionName: string;
1057
- serverPid: number;
1058
- serverIncarnation: string;
1059
- panePid?: number;
1060
- paneIncarnation?: string;
1061
- }
1062
- | undefined;
1063
- let primaryError: unknown;
1064
- try {
1065
- const created = Bun.spawnSync(
1066
- [
1067
- input.tmux,
1068
- "new-session",
1069
- "-d",
1070
- "-P",
1071
- "-F",
1072
- "#{session_id}\t#{session_name}",
1073
- "-s",
1074
- input.sessionName,
1075
- "sh",
1076
- "-c",
1077
- input.command,
1078
- ],
1079
- { stdout: "pipe", stderr: "pipe", env: input.env, cwd: input.cwd },
1080
- );
1081
- const receipt = directTmuxReceipt(created.stdout.toString(), input.sessionName);
1082
- if (!receipt) throw new Error("gjc_lifecycle_tmux_launch_failed");
1083
- const binding = directTmuxBinding(input.tmux, input.env, input.cwd, receipt.nativeSessionId, receipt.sessionName);
1084
- const serverIncarnation = binding.state === "bound" && input.readProcessIncarnation(binding.serverPid);
1085
- const paneIncarnation =
1086
- binding.state === "bound" && !binding.paneDead && input.readProcessIncarnation(binding.panePid);
1087
- if (binding.state !== "bound" || !serverIncarnation) throw new Error("gjc_lifecycle_tmux_launch_failed");
1088
- attempt = { ...receipt, serverPid: binding.serverPid, serverIncarnation };
1089
- if (!binding.paneDead && paneIncarnation) {
1090
- attempt.panePid = binding.panePid;
1091
- attempt.paneIncarnation = paneIncarnation;
1092
- }
1093
- if (created.exitCode !== 0 || !directTmuxIdentityIsCurrent({ ...input, ...attempt }))
1094
- throw new Error("gjc_lifecycle_tmux_launch_failed");
1095
- const commands = buildGjcTmuxProfileCommands(attempt.nativeSessionId, input.env, {
1096
- sessionId: input.sessionId,
1097
- sessionStateFile: input.sessionStateFile,
1098
- project: input.cwd,
1099
- })
1100
- .map(command => command.args.map(tmuxCommandArgument).join(" "))
1101
- .join(" ; ");
1102
- if (
1103
- !directTmuxGuardedMutation({ ...input, ...attempt, commands, success: "__gjc_lifecycle_metadata_ok__" }) ||
1104
- !directTmuxIdentityIsCurrent({ ...input, ...attempt })
1105
- )
1106
- throw new Error("gjc_lifecycle_metadata_write_failed");
1107
- await Bun.sleep(directLifecycleStabilizationMs);
1108
- if (
1109
- attempt.panePid === undefined ||
1110
- attempt.paneIncarnation === undefined ||
1111
- !directTmuxPaneLivenessIsCurrent({
1112
- ...input,
1113
- ...attempt,
1114
- panePid: attempt.panePid,
1115
- paneIncarnation: attempt.paneIncarnation,
1116
- })
1117
- )
1118
- throw new Error("gjc_lifecycle_tmux_launch_liveness_failed");
1119
- } catch (error) {
1120
- primaryError = error;
1121
- }
1122
- if (primaryError === undefined) return;
1123
- let cleanupFailure: unknown;
1124
- if (attempt) {
1125
- try {
1126
- cleanupDirectLifecycleAttempt({ ...input, ...attempt });
1127
- } catch (error) {
1128
- cleanupFailure = error;
1129
- }
1130
- } else cleanupFailure = cleanupUncertain();
1131
- if (cleanupFailure !== undefined)
1132
- throw new AggregateError([primaryError, cleanupFailure], "gjc_lifecycle_cleanup_uncertain");
1133
- throw primaryError;
1134
- }
1135
- /** Real daemon-safe tmux launcher: canonical owner-isolation plan + GJC tags. */
1136
- export function daemonSpawnCreate(
1137
- env: NodeJS.ProcessEnv = process.env,
1138
- opts: {
1139
- ownerIsolationProbe?: OwnerIsolationProbe;
1140
- platform?: NodeJS.Platform;
1141
- processIncarnation?: (pid: number) => string | undefined;
1142
- } = {},
1143
- ) {
1144
- return async (
1145
- frame: SessionCreateFrame,
1146
- ids: { lifecycleRequestId: string; intendedSessionId: string; startupPromptRef?: string },
1147
- ): Promise<CreateEffectResult> => {
1148
- const tmuxBinary = resolveGjcTmuxBinary({ env, platform: opts.platform });
1149
- if (tmuxBinary.isPsmux) throw new Error("gjc_lifecycle_psmux_unsupported");
1150
- const tmux = tmuxBinary.command;
1151
- const name = tmuxSessionNameFor(ids.intendedSessionId);
1152
- const { cwd, args } = buildCreateArgv(frame, ids);
1153
- const sessionStateFile = lifecycleRuntimeStateFile(cwd, ids.intendedSessionId, name);
1154
- const commonChildEnv: Record<string, string> = {
1155
- GJC_TMUX_LAUNCHED: "1",
1156
- GJC_NOTIFICATIONS: "1",
1157
- GJC_SESSION_ID: ids.intendedSessionId,
1158
- GJC_LIFECYCLE_REQUEST_ID: ids.lifecycleRequestId,
1159
- [GJC_COORDINATOR_SESSION_ID_ENV]: ids.intendedSessionId,
1160
- [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
1161
- };
1162
- if (ids.startupPromptRef) commonChildEnv.GJC_STARTUP_PROMPT_REF = ids.startupPromptRef;
1163
- if ((opts.platform ?? process.platform) === "linux") {
1164
- const stateDir = path.dirname(sessionStateFile);
1165
- const previousBaseline = await captureOwnerGenerationBaseline(stateDir, ids.intendedSessionId);
1166
- const predecessor = resolveManagedOwnerPredecessorSync(stateDir, ids.intendedSessionId, previousBaseline);
1167
- const generation = crypto.randomUUID();
1168
- const runId = crypto.randomUUID();
1169
- const incarnation = crypto.randomUUID();
1170
- const managedChildEnv: Record<string, string> = {
1171
- ...commonChildEnv,
1172
- [GJC_TMUX_OWNER_GENERATION_ENV]: generation,
1173
- [GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
1174
- [GJC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
1175
- [MANAGED_OWNER_COMMAND_ENV]: JSON.stringify(["gjc", ...args]),
1176
- [MANAGED_OWNER_RUN_ID_ENV]: runId,
1177
- [MANAGED_OWNER_INCARNATION_ENV]: incarnation,
1178
- ...(predecessor
1179
- ? {
1180
- [MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
1181
- [MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
1182
- [MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
1183
- [MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
1184
- [MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.GJC_SESSION_FILE ?? "",
1185
- }
1186
- : {}),
1187
- };
1188
- const managedEnvPairs = Object.entries(managedChildEnv)
1189
- .map(([key, value]) => `${key}=${shellQuote(value)}`)
1190
- .join(" ");
1191
- const command = `cd ${shellQuote(cwd)} && exec env ${managedEnvPairs} gjc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
1192
- await completeLifecycleSpawnTransaction({
1193
- tmux,
1194
- env,
1195
- sessionId: ids.intendedSessionId,
1196
- generation,
1197
- stateDir,
1198
- cwd,
1199
- sessionName: name,
1200
- sessionStateFile,
1201
- argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
1202
- ownerIsolationProbe: opts.ownerIsolationProbe,
1203
- prepareSpawn: () => {
1204
- if (frame.target.kind === "plain_dir") fs.mkdirSync(cwd, { recursive: true });
1205
- },
1206
- previousBaseline,
1207
- });
1208
- } else {
1209
- if (frame.target.kind === "plain_dir") fs.mkdirSync(cwd, { recursive: true });
1210
- const directEnvPairs = directLifecycleEnvArguments(commonChildEnv);
1211
- const directCommand = `cd ${shellQuote(cwd)} && exec env ${directEnvPairs} gjc ${args.map(shellQuote).join(" ")}`;
1212
- await completeNonLinuxLifecycleSpawn({
1213
- tmux,
1214
- env,
1215
- sessionId: ids.intendedSessionId,
1216
- cwd,
1217
- sessionName: name,
1218
- sessionStateFile,
1219
- command: directCommand,
1220
- readProcessIncarnation: opts.processIncarnation ?? processIncarnation,
1221
- });
1222
- }
1223
-
1224
- return {
1225
- sessionId: ids.intendedSessionId,
1226
- tmuxSession: name,
1227
- sessionStateFile,
1228
- endpointUrl: "",
1229
- topicThreadId: "",
1230
- };
1231
- };
1232
- }
1233
-
1234
- function tmuxCommandArgument(value: string): string {
1235
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
1236
- }
1237
-
1238
- function lifecycleMetadataPredicate(expectedPid: number, nativeSessionId: string, attemptSession: string): string {
1239
- return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${attemptSession}}}}`;
1240
- }
1241
-
1242
- async function applyRequiredLifecycleTmuxMetadata(
1243
- tmux: string,
1244
- target: string,
1245
- env: NodeJS.ProcessEnv,
1246
- metadata: {
1247
- sessionId: string;
1248
- sessionStateFile: string;
1249
- project: string;
1250
- ownerGeneration: string;
1251
- ownerServerKey: string;
1252
- },
1253
- attempt: { nativeSessionId: string; attemptSession: string; serverPid: number },
1254
- ): Promise<void> {
1255
- if (
1256
- !metadata.sessionId.trim() ||
1257
- !metadata.sessionStateFile.trim() ||
1258
- !metadata.project.trim() ||
1259
- !metadata.ownerGeneration.trim() ||
1260
- !metadata.ownerServerKey.trim()
1261
- )
1262
- throw new Error("gjc_lifecycle_metadata_required_missing");
1263
- const commands = buildGjcTmuxProfileCommands(target, env, metadata)
1264
- .map(command => command.args.map(tmuxCommandArgument).join(" "))
1265
- .join(" ; ");
1266
- const result = Bun.spawnSync(
1267
- [
1268
- tmux,
1269
- "-L",
1270
- metadata.ownerServerKey,
1271
- "if-shell",
1272
- "-t",
1273
- attempt.nativeSessionId,
1274
- "-F",
1275
- lifecycleMetadataPredicate(attempt.serverPid, attempt.nativeSessionId, attempt.attemptSession),
1276
- `${commands} ; display-message -p __gjc_lifecycle_metadata_ok__`,
1277
- "display-message -p __gjc_lifecycle_metadata_refused__",
1278
- ],
1279
- { stdout: "pipe", stderr: "pipe", env },
1280
- );
1281
- if (result.exitCode !== 0 || result.stdout.toString().trim() !== "__gjc_lifecycle_metadata_ok__")
1282
- throw new Error("gjc_lifecycle_metadata_write_failed");
1283
- }
1284
-
1285
- /** Real force-close effect (GJC-managed only, id-matched). */
1286
- export function daemonCloseSession(
1287
- env: NodeJS.ProcessEnv = process.env,
1288
- deps: {
1289
- forceClose?: (
1290
- name: string,
1291
- env: NodeJS.ProcessEnv,
1292
- expectedSessionId?: string,
1293
- sessionStateFile?: string,
1294
- ) => Promise<void>;
1295
- findSession?: (name: string, env: NodeJS.ProcessEnv) => GjcTmuxSessionStatus | undefined;
1296
- } = {},
1297
- ) {
1298
- return async (target: { sessionId: string; tmuxSession?: string; sessionStateFile?: string }) => {
1299
- const name = target.tmuxSession ?? tmuxSessionNameFor(target.sessionId);
1300
- await (deps.forceClose ?? forceCloseGjcTmuxSession)(name, env, target.sessionId, target.sessionStateFile);
1301
- return { processGone: (deps.findSession ?? findGjcTmuxSessionByName)(name, env) === undefined };
1302
- };
1303
- }
1304
-
1305
- /** Real resume effect: reattach if a live GJC session matches; else resolve the
1306
- * prefix against saved history and fail closed (`ambiguous`/`notFound`) before
1307
- * cold-restarting exactly one resolved session via the daemon-safe launcher. */
1308
- export function daemonResumeSession(
1309
- env: NodeJS.ProcessEnv = process.env,
1310
- opts: {
1311
- agentDir?: string;
1312
- /** Explicit managed root for isolated tests. */
1313
- sessionsRoot?: string;
1314
- listSessions?: (env: NodeJS.ProcessEnv) => GjcTmuxSessionStatus[];
1315
- ownerIsolationProbe?: OwnerIsolationProbe;
1316
- platform?: NodeJS.Platform;
1317
- processIncarnation?: (pid: number) => string | undefined;
1318
- } = {},
1319
- ) {
1320
- return async (target: {
1321
- sessionIdOrPrefix: string;
1322
- path?: string;
1323
- }): Promise<ResumeEffectResult | { ambiguous: ResumeCandidate[] } | { notFound: true }> => {
1324
- const tmuxBinary = resolveGjcTmuxBinary({ env, platform: opts.platform });
1325
- if (tmuxBinary.isPsmux) throw new Error("gjc_lifecycle_psmux_unsupported");
1326
- const live = (opts.listSessions?.(env) ?? listGjcTmuxSessions(env)).filter(
1327
- s => s.sessionId === target.sessionIdOrPrefix || s.sessionId?.startsWith(target.sessionIdOrPrefix),
1328
- );
1329
- if (live.length > 1) {
1330
- return {
1331
- ambiguous: live.map(s => ({ sessionId: s.sessionId ?? s.name, path: s.project })),
1332
- };
1333
- }
1334
- if (live.length === 1) {
1335
- const s = live[0]!;
1336
- await assertLifecycleTmuxServerSafe({
1337
- tmux: resolveGjcTmuxCommand(env),
1338
- env,
1339
- ownerIsolationProbe: opts.ownerIsolationProbe,
1340
- });
1341
- return {
1342
- sessionId: s.sessionId ?? s.name,
1343
- tmuxSession: s.name,
1344
- sessionStateFile: s.sessionStateFile,
1345
- endpointUrl: "",
1346
- topicThreadId: "",
1347
- mode: "reattached",
1348
- };
1349
- }
1350
- // Dead: resolve the id/prefix against saved session history BEFORE cold
1351
- // restart, so an unknown or ambiguous prefix fails closed instead of
1352
- // blindly spawning `gjc --resume <prefix>` against a non-authoritative id.
1353
- let resumeId = target.sessionIdOrPrefix;
1354
- let resumeCwd = target.path;
1355
- if (!target.path && !opts.agentDir && !opts.sessionsRoot) return { notFound: true };
1356
- const recent = await listRecentSessions({
1357
- cwd: target.path ?? opts.agentDir ?? opts.sessionsRoot ?? "",
1358
- agentDir: opts.agentDir,
1359
- sessionsRoot: opts.sessionsRoot,
1360
- allWorkspaces: target.path === undefined,
1361
- limit: 1000,
1362
- });
1363
- if (recent.kind === "error") throw new Error(`gjc_lifecycle_saved_sessions_unavailable: ${recent.message}`);
1364
- const saved = recent.entries;
1365
- const prefixed = saved.filter(
1366
- s => s.sessionId === target.sessionIdOrPrefix || s.sessionId.startsWith(target.sessionIdOrPrefix),
1367
- );
1368
- const exact = prefixed.filter(s => s.sessionId === target.sessionIdOrPrefix);
1369
- const resolved = exact.length > 0 ? exact : prefixed;
1370
- if (resolved.length === 0) return { notFound: true };
1371
- if (resolved.length > 1) {
1372
- return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
1373
- }
1374
- const selected = resolved[0]!;
1375
- resumeId = selected.sessionId;
1376
- resumeCwd = selected.path;
1377
- const resolvedResumeCwd = resumeCwd ? path.resolve(resumeCwd) : undefined;
1378
- const resumeCwdStat = resolvedResumeCwd ? fs.statSync(resolvedResumeCwd, { throwIfNoEntry: false }) : undefined;
1379
- if (typeof resolvedResumeCwd !== "string" || !resumeCwdStat?.isDirectory()) {
1380
- throw new Error(`gjc_lifecycle_resume_cwd_unavailable: ${resolvedResumeCwd ?? "(missing)"}`);
1381
- }
1382
- const tmux = tmuxBinary.command;
1383
- const name = tmuxSessionNameFor(resumeId);
1384
- const sessionStateFile = lifecycleRuntimeStateFile(resolvedResumeCwd, resumeId, name);
1385
- const commonChildEnv: Record<string, string> = {
1386
- GJC_TMUX_LAUNCHED: "1",
1387
- GJC_NOTIFICATIONS: "1",
1388
- [GJC_COORDINATOR_SESSION_ID_ENV]: resumeId,
1389
- [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
1390
- };
1391
- if ((opts.platform ?? process.platform) === "linux") {
1392
- const stateDir = path.dirname(sessionStateFile);
1393
- const previousBaseline = await captureOwnerGenerationBaseline(stateDir, resumeId);
1394
- const predecessor = resolveManagedOwnerPredecessorSync(stateDir, resumeId, previousBaseline);
1395
- const generation = crypto.randomUUID();
1396
- const runId = crypto.randomUUID();
1397
- const incarnation = crypto.randomUUID();
1398
- const managedChildEnv: Record<string, string> = {
1399
- ...commonChildEnv,
1400
- [GJC_TMUX_OWNER_GENERATION_ENV]: generation,
1401
- [GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
1402
- [GJC_TMUX_OWNER_SERVER_KEY_ENV]: "default",
1403
- [MANAGED_OWNER_COMMAND_ENV]: JSON.stringify(["gjc", "--resume", resumeId]),
1404
- [MANAGED_OWNER_RUN_ID_ENV]: runId,
1405
- [MANAGED_OWNER_INCARNATION_ENV]: incarnation,
1406
- ...(predecessor
1407
- ? {
1408
- [MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: predecessor.predecessorToken,
1409
- [MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: predecessor.generation,
1410
- [MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: predecessor.runId,
1411
- [MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: predecessor.incarnation,
1412
- [MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: env.GJC_SESSION_FILE ?? "",
1413
- }
1414
- : {}),
1415
- };
1416
- const managedEnvPairs = Object.entries(managedChildEnv)
1417
- .map(([key, value]) => `${key}=${shellQuote(value)}`)
1418
- .join(" ");
1419
- const command = `cd ${shellQuote(resolvedResumeCwd)} && exec env ${managedEnvPairs} gjc ${shellQuote(MANAGED_OWNER_SUPERVISOR_ARG)}`;
1420
- await completeLifecycleSpawnTransaction({
1421
- tmux,
1422
- env,
1423
- sessionId: resumeId,
1424
- generation,
1425
- stateDir,
1426
- cwd: resolvedResumeCwd,
1427
- sessionName: name,
1428
- sessionStateFile,
1429
- argv: [tmux, "new-session", "-d", "-P", "-F", "#{session_id}", "-s", name, "sh", "-c", command],
1430
- ownerIsolationProbe: opts.ownerIsolationProbe,
1431
- previousBaseline,
1432
- });
1433
- } else {
1434
- const directEnvPairs = directLifecycleEnvArguments(commonChildEnv);
1435
- const directCommand = `cd ${shellQuote(resolvedResumeCwd)} && exec env ${directEnvPairs} gjc ${shellQuote("--resume")} ${shellQuote(resumeId)}`;
1436
- await completeNonLinuxLifecycleSpawn({
1437
- tmux,
1438
- env,
1439
- sessionId: resumeId,
1440
- cwd: resolvedResumeCwd,
1441
- sessionName: name,
1442
- sessionStateFile,
1443
- command: directCommand,
1444
- readProcessIncarnation: opts.processIncarnation ?? processIncarnation,
1445
- });
1446
- }
1447
-
1448
- return {
1449
- sessionId: resumeId,
1450
- tmuxSession: name,
1451
- sessionStateFile,
1452
- endpointUrl: "",
1453
- topicThreadId: "",
1454
- mode: "cold_restarted",
1455
- };
1456
- };
1457
- }
1458
-
1459
- function shellQuote(value: string): string {
1460
- return `'${value.replaceAll("'", "'\\''")}'`;
1461
- }
1462
-
1463
- /** Translate an orchestrator outcome into a wire response frame. */
1464
- export function outcomeToResponse(frame: SessionLifecycleRequest, outcome: LifecycleOutcome): SessionLifecycleResponse {
1465
- if (outcome.status === "error" || outcome.status === "pending") {
1466
- const reason = outcome.status === "pending" ? "terminal_uncertain" : outcome.reason;
1467
- return {
1468
- type: "session_lifecycle_error",
1469
- requestId: frame.requestId,
1470
- status: "error",
1471
- reason,
1472
- message: outcome.status === "pending" ? "request already in progress" : outcome.message,
1473
- ...(outcome.status === "error" && outcome.candidates ? { candidates: outcome.candidates } : {}),
1474
- };
1475
- }
1476
- const e = outcome.entry;
1477
- if (e.state !== "success") throw new Error("invalid lifecycle success ledger entry");
1478
- if (frame.type === "session_create") {
1479
- if (typeof e.sessionId !== "string" || typeof e.endpointUrl !== "string")
1480
- throw new Error("invalid create success ledger entry");
1481
- return {
1482
- type: "session_create_response",
1483
- requestId: frame.requestId,
1484
- status: "ok",
1485
- lifecycleRequestId: frame.lifecycleRequestId,
1486
- sessionId: e.sessionId,
1487
- matchedBy: "spawn_marker",
1488
- endpoint: { url: e.endpointUrl, token: "" },
1489
- topic: { chatId: frame.chatId, threadId: "" },
1490
- target: frame.target,
1491
- };
1492
- }
1493
- if (frame.type === "session_close") {
1494
- if (typeof e.sessionId !== "string" || typeof e.processGone !== "boolean")
1495
- throw new Error("invalid close success ledger entry");
1496
- if (!e.processGone) {
1497
- return {
1498
- type: "session_lifecycle_error",
1499
- requestId: frame.requestId,
1500
- status: "error",
1501
- reason: "terminal_uncertain",
1502
- message: "close did not confirm process termination",
1503
- };
1504
- }
1505
- return {
1506
- type: "session_close_response",
1507
- requestId: frame.requestId,
1508
- status: "ok",
1509
- sessionId: e.sessionId,
1510
- processGone: true,
1511
- historyPreserved: true,
1512
- // The killed session's per-session endpoint record is reaped by the
1513
- // daemon's dead-PID scan (scanRoots), so it is effectively stale.
1514
- endpointStale: true,
1515
- };
1516
- }
1517
- if (
1518
- typeof e.sessionId !== "string" ||
1519
- typeof e.endpointUrl !== "string" ||
1520
- (e.resumeMode !== "reattached" && e.resumeMode !== "cold_restarted")
1521
- )
1522
- throw new Error("invalid resume success ledger entry");
1523
- return {
1524
- type: "session_resume_response",
1525
- requestId: frame.requestId,
1526
- status: "ok",
1527
- sessionId: e.sessionId,
1528
- mode: outcome.mode ?? e.resumeMode,
1529
- endpoint: { url: e.endpointUrl, token: "" },
1530
- topic: { chatId: frame.chatId, threadId: "" },
1531
- };
1532
- }
1533
-
1534
- const MAX_LIFECYCLE_REQUEST_ID_BYTES = 128;
1535
-
1536
- function hasLoneUtf16Surrogate(value: string): boolean {
1537
- for (let index = 0; index < value.length; index += 1) {
1538
- const codeUnit = value.charCodeAt(index);
1539
- if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
1540
- if (index + 1 >= value.length || value.charCodeAt(index + 1) < 0xdc00 || value.charCodeAt(index + 1) > 0xdfff)
1541
- return true;
1542
- index += 1;
1543
- } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
1544
- return true;
1545
- }
1546
- }
1547
- return false;
1548
- }
1549
-
1550
- function boundedLifecycleRequestId(requestId: unknown): string {
1551
- if (typeof requestId !== "string" || hasLoneUtf16Surrogate(requestId)) return "";
1552
- let bounded = "";
1553
- let byteLength = 0;
1554
- for (const codePoint of requestId) {
1555
- const codePointBytes = Buffer.byteLength(codePoint, "utf8");
1556
- if (byteLength + codePointBytes > MAX_LIFECYCLE_REQUEST_ID_BYTES) break;
1557
- bounded += codePoint;
1558
- byteLength += codePointBytes;
1559
- }
1560
- return bounded;
1561
- }
1562
-
1563
- function lifecycleFailureResponse(requestId: unknown): SessionLifecycleResponse {
1564
- return {
1565
- type: "session_lifecycle_error",
1566
- requestId: boundedLifecycleRequestId(requestId),
1567
- status: "error",
1568
- reason: "terminal_uncertain",
1569
- message: "request could not be processed",
1570
- };
1571
- }
1572
-
1573
- function recordLifecycleControlDiagnostic(): void {
1574
- recordBoundedLifecycleDiagnostic("GJC lifecycle control request failed");
1575
- }
1576
-
1577
- function isRecord(value: unknown): value is Record<string, unknown> {
1578
- return !!value && typeof value === "object" && !Array.isArray(value);
1579
- }
1580
-
1581
- function isSafeLifecycleId(value: unknown): value is string {
1582
- return typeof value === "string" && !hasLoneUtf16Surrogate(value);
1583
- }
1584
-
1585
- function isNonEmptySafeLifecycleId(value: unknown): value is string {
1586
- return isSafeLifecycleId(value) && value.length > 0;
1587
- }
1588
-
1589
- /** The native server authenticates callbacks before redacting the control token. */
1590
- type AuthenticatedNativeLifecycleRequest =
1591
- | Omit<SessionCreateFrame, "token">
1592
- | Omit<Extract<SessionLifecycleRequest, { type: "session_close" }>, "token">
1593
- | Omit<Extract<SessionLifecycleRequest, { type: "session_resume" }>, "token">;
1594
-
1595
- function isBoundedRequestFrame(frame: unknown): frame is AuthenticatedNativeLifecycleRequest {
1596
- if (
1597
- !isRecord(frame) ||
1598
- !isSafeLifecycleId(frame.requestId) ||
1599
- Buffer.byteLength(frame.requestId, "utf8") > MAX_LIFECYCLE_REQUEST_ID_BYTES
1600
- )
1601
- return false;
1602
- if (
1603
- !isSafeLifecycleId(frame.chatId) ||
1604
- (typeof frame.token !== "undefined" && typeof frame.token !== "string") ||
1605
- !Number.isSafeInteger(frame.updateId)
1606
- )
1607
- return false;
1608
- if (!isRecord(frame.target)) return false;
1609
- switch (frame.type) {
1610
- case "session_create":
1611
- return (
1612
- isNonEmptySafeLifecycleId(frame.lifecycleRequestId) &&
1613
- isNonEmptySafeLifecycleId(frame.intendedSessionId) &&
1614
- (typeof frame.startupPromptRef === "undefined" || typeof frame.startupPromptRef === "string") &&
1615
- (typeof frame.modelPreset === "undefined" || typeof frame.modelPreset === "string") &&
1616
- ((frame.target.kind === "worktree" &&
1617
- typeof frame.target.repo === "string" &&
1618
- typeof frame.target.branch === "string") ||
1619
- ((frame.target.kind === "existing_path" || frame.target.kind === "plain_dir") &&
1620
- typeof frame.target.path === "string"))
1621
- );
1622
- case "session_close":
1623
- return (
1624
- isSafeLifecycleId(frame.target.sessionId) &&
1625
- (typeof frame.target.tmuxSession === "undefined" || isSafeLifecycleId(frame.target.tmuxSession)) &&
1626
- (typeof frame.target.sessionStateFile === "undefined" ||
1627
- typeof frame.target.sessionStateFile === "string") &&
1628
- (typeof frame.force === "undefined" || typeof frame.force === "boolean")
1629
- );
1630
- case "session_resume":
1631
- return (
1632
- isSafeLifecycleId(frame.target.sessionIdOrPrefix) &&
1633
- (typeof frame.target.path === "undefined" || typeof frame.target.path === "string") &&
1634
- (typeof frame.startupPromptRef === "undefined" || typeof frame.startupPromptRef === "string")
1635
- );
1636
- default:
1637
- return false;
1638
- }
1639
- }
1640
-
1641
- /**
1642
- * Wire a control server's lifecycle requests through the orchestrator.
1643
- *
1644
- * Handlers run on a single serial queue (a promise chain): the daemon owns the
1645
- * one control endpoint, so serializing here makes each request's ledger
1646
- * read -> classify -> write atomic with respect to every other request. Two
1647
- * identical updates that arrive nearly simultaneously can no longer both
1648
- * classify as `new` and both spawn — the second sees the first's persisted
1649
- * `in_progress`/`success` entry and re-acks instead.
1650
- */
1651
- export function attachLifecycleControl(server: ControlServerLike, deps: OrchestratorDeps): void {
1652
- let queue: Promise<void> = Promise.resolve();
1653
- server.onLifecycleRequest((err, req) => {
1654
- if (err) {
1655
- recordLifecycleControlDiagnostic();
1656
- return;
1657
- }
1658
- const envelopeRequestId = boundedLifecycleRequestId(req.requestId);
1659
- const run = async (): Promise<void> => {
1660
- let frame: AuthenticatedNativeLifecycleRequest | undefined;
1661
-
1662
- try {
1663
- const parsed = JSON.parse(req.payloadJson) as unknown;
1664
- if (!isBoundedRequestFrame(parsed)) throw new Error("invalid lifecycle frame");
1665
- frame = parsed;
1666
- const outcome = await handleLifecycleRequest(frame as SessionLifecycleRequest, deps);
1667
-
1668
- server.respond(
1669
- JSON.stringify(
1670
- outcome.status === "error" && outcome.reason === "terminal_uncertain"
1671
- ? lifecycleFailureResponse(frame.requestId)
1672
- : outcomeToResponse(frame as SessionLifecycleRequest, outcome),
1673
- ),
1674
- );
1675
- } catch {
1676
- recordLifecycleControlDiagnostic();
1677
- // A valid frame preserves its immutable request id; malformed frames use
1678
- // only the bounded native envelope id.
1679
- server.respond(JSON.stringify(lifecycleFailureResponse(frame?.requestId ?? envelopeRequestId)));
1680
- }
1681
- };
1682
- // Recover queue liveness only after this request has attempted its own
1683
- // failure response. A response transport failure cannot poison later work.
1684
- queue = queue.then(run, run).catch(() => {
1685
- recordLifecycleControlDiagnostic();
1686
- });
1687
- });
1688
- }
1689
-
1690
- /** Assemble real orchestrator deps for the daemon (ledger/audit under agentDir). */
1691
- export function buildOrchestratorDeps(input: {
1692
- pairedChatId: string;
1693
- agentNotificationsDir: string;
1694
- /** Required in-memory, 32-byte audit-v2 redaction key. */
1695
- auditRedactionKey: Uint8Array;
1696
- /** Root of saved session histories (`<agentDir>/sessions`), for resume resolution. */
1697
- sessionsRoot?: string;
1698
- /** Agent directory used for readonly managed-session resume resolution. */
1699
- agentDir?: string;
1700
- env?: NodeJS.ProcessEnv;
1701
- }): OrchestratorDeps {
1702
- if (input.auditRedactionKey.byteLength !== 32) throw new Error("invalid_audit_redaction_key");
1703
- const env = input.env ?? process.env;
1704
- return {
1705
- pairedChatId: input.pairedChatId,
1706
- auditRedactionKey: input.auditRedactionKey,
1707
- now: () => Date.now(),
1708
- store: fileLedgerStore(path.join(input.agentNotificationsDir, "telegram-lifecycle-idempotency.json")),
1709
- audit: fileAudit(path.join(input.agentNotificationsDir, "telegram-lifecycle-audit.jsonl")),
1710
- isPsmuxProvider: () => resolveGjcTmuxBinary({ env }).isPsmux,
1711
- allowCreate: createRateLimiter(3, 10 * 60 * 1000),
1712
- writeStartupPrompt: async (_requestId, prompt, _persistRef) => {
1713
- if (prompt === undefined) return undefined;
1714
- throw new Error("startup_prompt_capability_transport_unavailable");
1715
- },
1716
- spawnCreate: daemonSpawnCreate(env),
1717
- closeSession: daemonCloseSession(env),
1718
- resumeSession: daemonResumeSession(env, {
1719
- agentDir: input.agentDir ?? path.dirname(input.agentNotificationsDir),
1720
- }),
1721
- };
1722
- }
1723
-
1724
- /**
1725
- * Default production factory: a real native NotificationControlServer bound to
1726
- * the daemon's control token, owner id, and agent dir.
1727
- */
1728
- export const createNativeControlServer: LifecycleControlServerFactory = ({ token, ownerId, agentDir }) => {
1729
- // Lazy require so loading this module (for the orchestrator / wiring / tests)
1730
- // never eagerly resolves the native addon — only a real production start does.
1731
- const { NotificationControlServer } = require("@gajae-code/natives") as NativeControlServerModule;
1732
- return new NotificationControlServer(token, ownerId, agentDir);
1733
- };