@gajae-code/coding-agent 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (599) hide show
  1. package/CHANGELOG.md +184 -2
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +1 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -0,0 +1,1658 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { logger } from "@gajae-code/utils";
5
+ import {
6
+ SessionIndex as DefaultSessionIndex,
7
+ type IndexedSession,
8
+ isSessionAuthorityEligible,
9
+ type SessionIndex,
10
+ } from "../broker/session-index";
11
+ import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
12
+ import { SdkClient } from "../client/client";
13
+ import { readSdkBrokerDiscovery, readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
14
+ import {
15
+ type ActivatedPreparedSession,
16
+ type PreparedSessionActivationClient,
17
+ requestPreparedSessionActivation,
18
+ SessionActivationError,
19
+ } from "../session-activation";
20
+ import { ACP_SESSION_RECONNECT, SESSION_REQUEST_TIMEOUT_MS } from "../session-reconnect";
21
+
22
+ /**
23
+ * Exact identity of one attached SDK session endpoint. Providers persist it next to
24
+ * their conversation state and re-prove it before every resume, so it must be derived
25
+ * in exactly one place: a caller that recomputes the digest by hand silently stops
26
+ * matching the moment the bound fields change.
27
+ */
28
+ export function sessionAttachmentAuthorityId(input: {
29
+ sessionId: string;
30
+ generation: number;
31
+ pid: number;
32
+ endpointMtimeMs: number | undefined;
33
+ url: string;
34
+ token: string;
35
+ }): string {
36
+ const endpointAuthorityDigest = crypto
37
+ .createHash("sha256")
38
+ .update(JSON.stringify({ url: input.url, token: input.token }))
39
+ .digest("hex");
40
+ return crypto
41
+ .createHash("sha256")
42
+ .update(
43
+ JSON.stringify({
44
+ sessionId: input.sessionId,
45
+ generation: input.generation,
46
+ pid: input.pid,
47
+ endpointMtimeMs: input.endpointMtimeMs,
48
+ endpointAuthorityDigest,
49
+ }),
50
+ )
51
+ .digest("hex");
52
+ }
53
+
54
+ /** The only capability a provider may retain for an attached SDK session. */
55
+ export interface SessionAttachment {
56
+ readonly sessionId: string;
57
+ readonly authorityId?: string;
58
+ /** Current Router-owned transport identity for this exact attachment's reverse leases. */
59
+ readonly connectionId?: string;
60
+ readonly generation: number;
61
+ isCurrent(): boolean;
62
+ send(frame: Record<string, unknown>): unknown;
63
+ /** Revoke this exact capability after provider admission or replay fails closed. */
64
+ retire?(): Promise<void>;
65
+ }
66
+
67
+ /**
68
+ * Provider-local notification capability. This is deliberately not an
69
+ * attachment lease: it carries no endpoint, connection, generation, or
70
+ * authority identity and its cancellation can only stop this subscription.
71
+ */
72
+ export interface NotificationSubscription {
73
+ readonly sessionId: string;
74
+ readonly subscriptionId: string;
75
+ readonly cursor: { readonly generation: number; readonly seq: number };
76
+ readonly isActive: () => boolean;
77
+ readonly send: (frame: Record<string, unknown>) => unknown;
78
+ readonly advanceCursor: (generation: number, seq: number) => void;
79
+ readonly cancel: (reason?: string) => void;
80
+ }
81
+
82
+ export type NotificationCleanupState = "pending" | "failed" | "completed";
83
+
84
+ export interface NotificationCleanupReceipt {
85
+ readonly subscriptionId: string;
86
+ readonly sessionId: string;
87
+ readonly state: NotificationCleanupState;
88
+ readonly reason?: string;
89
+ }
90
+
91
+ /** The transport surface Router keeps private behind its attachment capabilities. */
92
+ export interface SessionRouterClient {
93
+ onFrame(handler: (frame: Record<string, unknown>) => void): () => void;
94
+ onReconnect?(handler: () => void): () => void;
95
+ connect?(): Promise<void>;
96
+ request(frame: Record<string, unknown>, options?: { timeoutMs?: number }): Promise<Record<string, unknown>>;
97
+ /** Current private transport connection identity, surfaced only through its exact attachment. */
98
+ readonly connectionId?: string;
99
+
100
+ close(): Promise<void>;
101
+ send(frame: Record<string, unknown>): void;
102
+ }
103
+
104
+ /** One frame after the caller's envelope/payload identity correlation. */
105
+ export interface SessionRouterFrame {
106
+ readonly body: Record<string, unknown>;
107
+ readonly name: string | undefined;
108
+ readonly sessionId: string | undefined;
109
+ readonly generation: number | undefined;
110
+ readonly commandId?: string;
111
+ readonly turnId?: string;
112
+ readonly publicationId?: string;
113
+ readonly seq?: number;
114
+ }
115
+
116
+ export type SessionRouterFrameCorrelator = (frame: Record<string, unknown>) => SessionRouterFrame | undefined;
117
+
118
+ export interface SessionRouterDeps {
119
+ createClient?: (authority: {
120
+ readonly sessionId: string;
121
+ readonly generation: number;
122
+ readonly pid: number;
123
+ readonly endpointMtimeMs: number;
124
+ }) => Promise<SessionRouterClient>;
125
+ createIndex?: (agentDir: string) => SessionIndex;
126
+ createBrokerClient?: () => Promise<SessionRouterClient>;
127
+ /** Receives only an opaque capability and correlated provider-neutral frames. */
128
+ onFrame?: (attachment: SessionAttachment, frame: SessionRouterFrame) => Promise<void> | void;
129
+ onAttachment?: (attachment: SessionAttachment) => Promise<void> | void;
130
+ /** Called only after the opaque capability becomes externally current. */
131
+ onAttachmentReady?: (attachment: SessionAttachment) => Promise<void> | void;
132
+ /** Called when the Broker index no longer reports an attached session as live. */
133
+ onSessionRemoved?: (
134
+ attachment: SessionAttachment,
135
+ reason?: "removed" | "replaced" | "replaced_same_generation",
136
+ ) => Promise<void> | void;
137
+ /** Narrow provider surface for notification consumers such as Telegram. */
138
+ onNotificationSubscription?: (subscription: NotificationSubscription) => Promise<void> | void;
139
+ onNotificationSubscriptionReady?: (subscription: NotificationSubscription) => Promise<void> | void;
140
+ onNotificationFrame?: (subscription: NotificationSubscription, frame: SessionRouterFrame) => Promise<void> | void;
141
+ onNotificationSubscriptionRemoved?: (
142
+ subscription: NotificationSubscription,
143
+ reason?: "removed" | "replaced" | "replaced_same_generation" | "cancelled",
144
+ ) => Promise<void> | void;
145
+ onReconciled?: () => void;
146
+ setInterval?: typeof setInterval;
147
+ clearInterval?: typeof clearInterval;
148
+ setTimeout?: typeof setTimeout;
149
+ clearTimeout?: typeof clearTimeout;
150
+ }
151
+
152
+ export type SessionRouterProviderDeps = Pick<
153
+ SessionRouterDeps,
154
+ | "createClient"
155
+ | "createIndex"
156
+ | "createBrokerClient"
157
+ | "setInterval"
158
+ | "clearInterval"
159
+ | "setTimeout"
160
+ | "clearTimeout"
161
+ | "onReconciled"
162
+ >;
163
+
164
+ export interface SessionRouterOptions {
165
+ agentDir: string;
166
+ deps?: SessionRouterDeps;
167
+ /** Runtime-specific identity validation; Router supplies a conservative fallback. */
168
+ correlateFrame?: SessionRouterFrameCorrelator;
169
+ }
170
+
171
+ export type SessionRouterErrorPhase = "pre_send" | "ambiguous";
172
+
173
+ export class SessionRouterError extends Error {
174
+ constructor(
175
+ readonly phase: SessionRouterErrorPhase,
176
+ message = "SDK session attachment is unavailable.",
177
+ ) {
178
+ super(message);
179
+ this.name = "SessionRouterError";
180
+ }
181
+ }
182
+
183
+ type HeldFrame = Readonly<{ seq: number; frame: Record<string, unknown> }>;
184
+ type FrameOrigin = "live" | "ordered";
185
+ type ReplayBarrier = {
186
+ held: HeldFrame[] | undefined;
187
+ detached: boolean;
188
+ failed: boolean;
189
+ };
190
+
191
+ type AttachedSession = {
192
+ readonly id: string;
193
+ readonly sessionId: string;
194
+ readonly endpoint: SdkSessionEndpoint;
195
+ readonly generation: number;
196
+ readonly pid: number;
197
+ readonly endpointMtimeMs: number;
198
+ readonly runEpoch: number;
199
+ readonly client: SessionRouterClient;
200
+ readonly indexed: IndexedSession;
201
+ readonly cursor: { seq: number };
202
+ readonly barrier: ReplayBarrier;
203
+ readonly capability: SessionAttachment;
204
+ readonly notificationSubscription: NotificationSubscription;
205
+ notificationCancelled: boolean;
206
+ readonly notificationCursor: { generation: number; seq: number };
207
+ published: boolean;
208
+ initializingPublication: boolean;
209
+ readyTail: Promise<void>;
210
+ readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
211
+ dispose: () => void;
212
+ };
213
+
214
+ const REPLAY_BARRIER_LIMIT = 1_024;
215
+ const REPLAY_RETRY_ATTEMPTS = 3;
216
+ const REPLAY_RETRY_BACKOFF_MS = 100;
217
+ const DELIVERY_ATTEMPT_LIMIT = 3;
218
+ const ATTACH_CONCURRENCY = 4;
219
+ const ATTACH_CONNECT_TIMEOUT_MS = 10_000;
220
+ const NOTIFICATION_WORK_TIMEOUT_MS = 5_000;
221
+ const NOTIFICATION_WORK_TIMEOUT = Symbol("notification_work_timeout");
222
+ /**
223
+ * Client-message types the native session server authorizes with the
224
+ * per-session endpoint token (`tokens_match` in crates/gjc-sdk server.rs).
225
+ * Frames of these types without a matching `token` are dropped silently.
226
+ */
227
+ const TOKEN_AUTHORIZED_FRAME_TYPES = new Set([
228
+ "user_message",
229
+ "reply",
230
+ "ephemeral_turn",
231
+ "ephemeral_turn_cancel",
232
+ "config_command",
233
+ "control_command",
234
+ ]);
235
+
236
+ function readGeneration(value: unknown): number | undefined {
237
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
238
+ }
239
+
240
+ function readPositiveInteger(value: unknown): number | undefined {
241
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
242
+ }
243
+
244
+ function readEndpointMtime(value: unknown): number | undefined {
245
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
246
+ }
247
+
248
+ function readSequence(value: unknown): number | undefined {
249
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 ? value : undefined;
250
+ }
251
+
252
+ function fallbackCorrelation(frame: Record<string, unknown>): SessionRouterFrame | undefined {
253
+ const payload =
254
+ frame.type === "event" && frame.payload && typeof frame.payload === "object" && !Array.isArray(frame.payload)
255
+ ? (frame.payload as Record<string, unknown>)
256
+ : undefined;
257
+ const readSession = (value: unknown): string | undefined =>
258
+ typeof value === "string" && value.length > 0 ? value : undefined;
259
+ const readName = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
260
+ const readCorrelation = (value: unknown): string | undefined =>
261
+ typeof value === "string" && value.length > 0 ? value : undefined;
262
+ const outerSession = frame.sessionId;
263
+ const innerSession = payload?.sessionId;
264
+ const outerGeneration = frame.generation;
265
+ const innerGeneration = payload?.generation;
266
+ if (outerSession !== undefined && innerSession !== undefined && outerSession !== innerSession) return undefined;
267
+ if (outerGeneration !== undefined && innerGeneration !== undefined && outerGeneration !== innerGeneration)
268
+ return undefined;
269
+ const sessionClaim = outerSession !== undefined ? outerSession : innerSession;
270
+ const generationClaim = outerGeneration !== undefined ? outerGeneration : innerGeneration;
271
+ const sessionId = readSession(sessionClaim);
272
+ const generation = readGeneration(generationClaim);
273
+ if (sessionClaim !== undefined && sessionId === undefined) return undefined;
274
+ if (generationClaim !== undefined && generation === undefined) return undefined;
275
+ const body = payload ?? frame;
276
+ const nestedEvent = payload
277
+ ? payload.event && typeof payload.event === "object" && !Array.isArray(payload.event)
278
+ ? (payload.event as Record<string, unknown>)
279
+ : undefined
280
+ : undefined;
281
+ const commandId =
282
+ readCorrelation(frame.commandId) ??
283
+ readCorrelation(payload?.commandId) ??
284
+ readCorrelation(nestedEvent?.commandId);
285
+ const turnId =
286
+ readCorrelation(frame.turnId) ?? readCorrelation(payload?.turnId) ?? readCorrelation(nestedEvent?.turnId);
287
+ return {
288
+ body,
289
+ name: readName(frame.name) ?? readName(frame.kind) ?? readName(body.type),
290
+ sessionId,
291
+ generation,
292
+ commandId,
293
+ turnId,
294
+ seq: readSequence(frame.seq) ?? readSequence(payload?.seq),
295
+ };
296
+ }
297
+
298
+ function readReplayGap(
299
+ value: unknown,
300
+ ):
301
+ | Readonly<{ kind: "generation_reset"; toGeneration: number }>
302
+ | Readonly<{ kind: "sequence_gap"; fromSeq: number; toSeq: number }>
303
+ | undefined {
304
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
305
+ const gap = value as Record<string, unknown>;
306
+ if (gap.kind === "generation_reset") {
307
+ const toGeneration = readGeneration(gap.toGeneration);
308
+ return toGeneration === undefined ? undefined : { kind: "generation_reset", toGeneration };
309
+ }
310
+ if (gap.kind !== "sequence_gap") return undefined;
311
+ const fromSeq = readSequence(gap.fromSeq);
312
+ const toSeq = readSequence(gap.toSeq);
313
+ if (fromSeq === undefined || toSeq === undefined || toSeq < fromSeq) return undefined;
314
+ return { kind: "sequence_gap", fromSeq, toSeq };
315
+ }
316
+
317
+ function sameIndexedAuthority(expected: IndexedSession, current: IndexedSession): boolean {
318
+ return (
319
+ current.sessionId === expected.sessionId &&
320
+ current.live &&
321
+ isSessionAuthorityEligible(current) &&
322
+ !current.terminalUncertain &&
323
+ current.endpointGeneration === expected.endpointGeneration &&
324
+ current.pid === expected.pid &&
325
+ current.endpointMtimeMs === expected.endpointMtimeMs
326
+ );
327
+ }
328
+
329
+ type AdoptedSession = {
330
+ readonly generation: number;
331
+ readonly pid: number;
332
+ readonly endpointMtimeMs: number;
333
+ readonly attachment: SessionAttachment;
334
+ };
335
+
336
+ /**
337
+ * Broker-index-backed SDK attachment authority. Providers receive only opaque
338
+ * attachment capabilities; endpoint records and SDK clients remain here.
339
+ */
340
+ export class SessionRouter {
341
+ readonly #agentDir: string;
342
+ readonly #deps: SessionRouterDeps;
343
+ readonly #correlateFrame: SessionRouterFrameCorrelator;
344
+ readonly #index: SessionIndex;
345
+ readonly #sessions = new Map<string, AttachedSession>();
346
+ readonly #adopted = new Map<string, AdoptedSession>();
347
+ readonly #retirements = new Map<string, Promise<void>>();
348
+ readonly #retirementVersions = new Map<string, number>();
349
+ readonly #pending = new Set<Promise<void>>();
350
+ readonly #frameTails = new Map<string, Promise<void>>();
351
+ readonly #undelivered = new Map<string, { generation: number; seq: number; attempts: number }>();
352
+ readonly #recoveredFrames = new Map<
353
+ string,
354
+ { generation: number; frames: Array<{ seq: number; frame: Record<string, unknown> }> }
355
+ >();
356
+ readonly #reviving = new Set<string>();
357
+ readonly #notificationReceipts = new Map<string, NotificationCleanupReceipt>();
358
+ #stopTimer: (() => void) | undefined;
359
+ #reconcileTail: Promise<void> = Promise.resolve();
360
+ #reconcilePending: { readonly runEpoch: number } | undefined;
361
+ #ready = false;
362
+ #started = false;
363
+ #stopController = new AbortController();
364
+ #runEpoch = 0;
365
+
366
+ constructor(options: SessionRouterOptions) {
367
+ this.#agentDir = options.agentDir;
368
+ this.#deps = options.deps ?? {};
369
+ this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
370
+ this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
371
+ }
372
+
373
+ /** Provider-local cleanup outcomes; core authority never depends on these. */
374
+ notificationCleanupReceipts(): NotificationCleanupReceipt[] {
375
+ return [...this.#notificationReceipts.values()].map(receipt => ({ ...receipt }));
376
+ }
377
+
378
+ isReady(): boolean {
379
+ return this.#ready;
380
+ }
381
+
382
+ /** Starts reconciliation and the index watcher. */
383
+ async start(): Promise<void> {
384
+ if (this.#started) return;
385
+ this.#started = true;
386
+ const runEpoch = ++this.#runEpoch;
387
+ if (this.#stopController.signal.aborted) {
388
+ this.#stopController = new AbortController();
389
+ this.#reconcileTail = Promise.resolve();
390
+ this.#reconcilePending = undefined;
391
+ this.#frameTails.clear();
392
+ }
393
+ try {
394
+ await this.#serialReconcile(runEpoch);
395
+ if (!this.#running(runEpoch)) return;
396
+ const timer = (this.#deps.setInterval ?? setInterval)(
397
+ () => this.#schedule(this.#serialReconcile(runEpoch, true)),
398
+ 2_000,
399
+ );
400
+ this.#stopTimer = () => (this.#deps.clearInterval ?? clearInterval)(timer);
401
+ } catch (error) {
402
+ if (this.#running(runEpoch)) await this.stop();
403
+ throw error;
404
+ }
405
+ }
406
+
407
+ /** Exposed for deterministic callers and reconciliation tests. */
408
+ async reconcile(): Promise<void> {
409
+ await this.#serialReconcile(this.#runEpoch, true);
410
+ // Periodic reconciliation may have published an attachment while its
411
+ // initial replay continues on that attachment's isolated ready tail.
412
+ // Explicit callers retain the historical synchronous contract without
413
+ // putting any ready tail back onto the fleet-wide reconcile tail.
414
+ await Promise.all([...this.#sessions.values()].map(attached => attached.readyTail));
415
+ }
416
+ /** Ingests a credential-bearing Broker lifecycle result directly into Router custody. */
417
+ async adoptLifecycleResult(
418
+ value: unknown,
419
+ fallback: { sessionId: string; cwd: string },
420
+ ): Promise<SessionAttachment> {
421
+ const outer =
422
+ value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
423
+ const result =
424
+ outer.result !== null && typeof outer.result === "object" && !Array.isArray(outer.result)
425
+ ? (outer.result as Record<string, unknown>)
426
+ : outer;
427
+ const endpointValue = result.endpoint;
428
+ const endpointRecord =
429
+ endpointValue !== null && typeof endpointValue === "object" && !Array.isArray(endpointValue)
430
+ ? (endpointValue as Record<string, unknown>)
431
+ : result;
432
+ const sessionId = typeof result.sessionId === "string" ? result.sessionId : undefined;
433
+ const endpointGeneration = readPositiveInteger(result.endpointGeneration);
434
+ const pid = readPositiveInteger(result.pid);
435
+ const endpointMtimeMs = readEndpointMtime(result.endpointMtimeMs);
436
+ if (
437
+ sessionId !== fallback.sessionId ||
438
+ endpointGeneration === undefined ||
439
+ pid === undefined ||
440
+ endpointMtimeMs === undefined ||
441
+ endpointRecord.sessionId !== sessionId ||
442
+ endpointRecord.pid !== pid ||
443
+ typeof endpointRecord.url !== "string" ||
444
+ typeof endpointRecord.token !== "string"
445
+ )
446
+ throw new SessionRouterError(
447
+ "pre_send",
448
+ "Broker lifecycle result omitted an exact session endpoint authority.",
449
+ );
450
+ const repo = path.resolve(fallback.cwd);
451
+ const stateRoot = path.join(repo, ".gjc", "state");
452
+ const indexed: IndexedSession = {
453
+ sessionId,
454
+ locator: { repo, stateRoot },
455
+ endpointGeneration,
456
+ pid,
457
+ endpointMtimeMs,
458
+ live: true,
459
+ indexSeq: 0,
460
+ identityProvenance: "legacy",
461
+ ambiguous: false,
462
+ terminal: false,
463
+ };
464
+ const endpoint: SdkSessionEndpoint = {
465
+ sessionId,
466
+ url: endpointRecord.url,
467
+ token: endpointRecord.token,
468
+ pid,
469
+ path: path.join(stateRoot, "sdk", `${sessionId}.json`),
470
+ };
471
+ const attached = await this.#attach(indexed, this.#runEpoch, endpoint, true, true);
472
+ const current = this.#sessions.get(sessionId);
473
+ const capability = current?.capability;
474
+ if (!attached || !current || !capability)
475
+ throw new SessionRouterError("pre_send", "Broker session endpoint could not be attached.");
476
+ const listing = this.#index.listSessions();
477
+ const indexedCurrent =
478
+ listing.warnings.length === 0 ? listing.sessions.find(item => item.sessionId === sessionId) : undefined;
479
+ if (indexedCurrent && sameIndexedAuthority(indexed, indexedCurrent))
480
+ await this.#serialReconcile(this.#runEpoch, true);
481
+ return capability;
482
+ }
483
+
484
+ async stop(): Promise<void> {
485
+ if (this.#stopTimer) this.#stopTimer();
486
+ this.#stopTimer = undefined;
487
+ this.#started = false;
488
+ this.#runEpoch += 1;
489
+ this.#stopController.abort();
490
+ this.#ready = false;
491
+ const shutdownTasks: Promise<void>[] = [];
492
+ for (const [sessionId, attached] of this.#sessions) {
493
+ this.#sessions.delete(sessionId);
494
+ attached.dispose();
495
+ this.#detachNotification(attached, "removed");
496
+ shutdownTasks.push((async () => await attached.client.close())());
497
+ void Promise.resolve(this.#deps.onSessionRemoved?.(attached.capability)).catch(error =>
498
+ logger.warn(`SDK provider cleanup failed during router stop: ${String(error)}`),
499
+ );
500
+ }
501
+ this.#adopted.clear();
502
+ // Provider notification work is detached and bounded independently; core
503
+ // shutdown waits only for Router reconciliation and client close.
504
+ const pending = Promise.allSettled([this.#reconcileTail, ...shutdownTasks]);
505
+ const outcome = await Promise.race([
506
+ pending.then(results => ({ kind: "settled" as const, results })),
507
+ Bun.sleep(5_000).then(() => ({ kind: "timeout" as const })),
508
+ ]);
509
+ if (outcome.kind === "timeout") {
510
+ logger.warn(
511
+ "SessionRouter shutdown exceeded 5000ms; authority is revoked and cleanup continues in background.",
512
+ );
513
+ return;
514
+ }
515
+ const errors = outcome.results
516
+ .filter((result): result is PromiseRejectedResult => result.status === "rejected")
517
+ .map(result => result.reason);
518
+ if (errors.length > 0) throw new AggregateError(errors, "SessionRouter shutdown failed.");
519
+ }
520
+
521
+ /** Returns an opaque lease only while the exact attachment generation is live. */
522
+ attachment(sessionId: string, expectedGeneration?: number): SessionAttachment | null {
523
+ const attached = this.#sessions.get(sessionId);
524
+ if (!attached || !this.#attachmentPublished(attached)) return null;
525
+ if (expectedGeneration !== undefined && expectedGeneration !== attached.generation) return null;
526
+ return attached.capability;
527
+ }
528
+ #prepareFrame(attached: AttachedSession, frame: Record<string, unknown>): Record<string, unknown> {
529
+ // The native session server authorizes these client-message types with
530
+ // the per-session endpoint token and silently drops frames whose token
531
+ // is missing or wrong. Providers only hold opaque capabilities (the
532
+ // endpoint record lives here), so the router must stamp the token —
533
+ // omitting it made every daemon-origin injection (Telegram → session)
534
+ // vanish after the daemon had already ACKed the user's message.
535
+ const withToken =
536
+ typeof frame.type === "string" && TOKEN_AUTHORIZED_FRAME_TYPES.has(frame.type) && frame.token === undefined
537
+ ? { ...frame, token: attached.endpoint.token }
538
+ : frame;
539
+ const connectionId = attached.client.connectionId;
540
+ if (connectionId === undefined) return withToken;
541
+ if (withToken.connectionId !== undefined && withToken.connectionId !== connectionId)
542
+ throw new SessionRouterError("pre_send", "SDK session transport identity changed before command dispatch.");
543
+ return { ...withToken, connectionId };
544
+ }
545
+
546
+ /** Sends an SDK command through the current attachment without exposing its client. */
547
+ async request(
548
+ sessionId: string,
549
+ frame: Record<string, unknown>,
550
+ expectedGeneration?: number,
551
+ expectedAttachment?: SessionAttachment,
552
+ options?: { timeoutMs?: number },
553
+ ): Promise<Record<string, unknown>> {
554
+ const publishing = this.#sessions.get(sessionId);
555
+ if (!expectedAttachment || publishing?.capability !== expectedAttachment || !publishing.initializingPublication)
556
+ await this.#serialReconcile(this.#runEpoch, true);
557
+ const attached = this.#sessions.get(sessionId);
558
+ if (!attached || !this.#attachmentPublished(attached))
559
+ throw new SessionRouterError("pre_send", "SDK session attachment is unavailable: session not published.");
560
+ if (expectedGeneration !== undefined && expectedGeneration !== attached.generation)
561
+ throw new SessionRouterError("pre_send", "SDK session endpoint changed before command dispatch.");
562
+ if (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
563
+ throw new SessionRouterError("pre_send", "SDK session attachment changed before command dispatch.");
564
+ if (attached.initializingPublication) {
565
+ const endpoint = await this.#readEndpoint(attached.indexed);
566
+ if (
567
+ !endpoint ||
568
+ endpoint.url !== attached.endpoint.url ||
569
+ endpoint.token !== attached.endpoint.token ||
570
+ endpoint.pid !== attached.pid
571
+ ) {
572
+ await this.#retireAttachment(attached, endpoint ? "replaced_same_generation" : undefined);
573
+ throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
574
+ }
575
+ }
576
+ // A caller that sized its own budget keeps it; everything else gets the
577
+ // long-lived session budget instead of the transport's one-shot default,
578
+ // which a cold host's first credential-collecting query outruns (#4258).
579
+ const response = await attached.client.request(this.#prepareFrame(attached, frame), {
580
+ ...options,
581
+ timeoutMs: options?.timeoutMs ?? SESSION_REQUEST_TIMEOUT_MS,
582
+ });
583
+ if (
584
+ !this.#attachmentPublished(attached) ||
585
+ (expectedGeneration !== undefined && attached.generation !== expectedGeneration) ||
586
+ (expectedAttachment !== undefined && attached.capability !== expectedAttachment)
587
+ )
588
+ throw new SessionRouterError("ambiguous", "SDK session attachment changed while awaiting command response.");
589
+ return response;
590
+ }
591
+
592
+ /** Resolves the exact provider-neutral binding authority for operator adoption. */
593
+ async bindingAuthority(sessionId: string): Promise<{ sessionId: string; endpointGeneration: number } | undefined> {
594
+ const attached = this.#sessions.get(sessionId);
595
+ if (!attached || !this.#attachmentPublished(attached)) return undefined;
596
+ let indexed: IndexedSession | undefined;
597
+ try {
598
+ await this.#index.refresh();
599
+ const listing = this.#index.listSessions();
600
+ if (listing.warnings.length > 0) return undefined;
601
+ indexed = listing.sessions.find(candidate => candidate.sessionId === sessionId);
602
+ } catch {
603
+ return undefined;
604
+ }
605
+ if (!indexed?.live || !isSessionAuthorityEligible(indexed) || indexed.terminalUncertain) return undefined;
606
+ if (
607
+ !Number.isSafeInteger(indexed.endpointGeneration) ||
608
+ indexed.endpointGeneration <= 0 ||
609
+ indexed.endpointGeneration !== attached.generation ||
610
+ indexed.endpointMtimeMs === undefined
611
+ )
612
+ return undefined;
613
+ if (!Number.isSafeInteger(indexed.pid) || indexed.pid <= 0) return undefined;
614
+ const endpoint = await this.#readEndpoint(indexed).catch(() => null);
615
+ if (!endpoint || endpoint.stale === true || endpoint.pid !== indexed.pid || !endpoint.token) return undefined;
616
+ if (this.#sessions.get(sessionId) !== attached || !this.#attachmentPublished(attached)) return undefined;
617
+ return { sessionId, endpointGeneration: attached.generation };
618
+ }
619
+
620
+ /** Activates a prepared session through one Router-owned, one-shot SDK client. */
621
+ async activatePreparedSession(sessionId: string): Promise<ActivatedPreparedSession> {
622
+ let indexed: IndexedSession | undefined;
623
+ try {
624
+ await this.#index.open();
625
+ await this.#index.refresh();
626
+ const listing = this.#index.listSessions();
627
+ if (listing.warnings.length > 0)
628
+ throw new SessionActivationError(
629
+ "session_not_live",
630
+ "Session activation requires an intact session index.",
631
+ );
632
+ indexed = listing.sessions.find(candidate => candidate.sessionId === sessionId);
633
+ } catch (error) {
634
+ if (error instanceof SessionActivationError) throw error;
635
+ throw new SessionActivationError(
636
+ "session_not_live",
637
+ "Session activation requires an exact live session endpoint.",
638
+ );
639
+ }
640
+ if (
641
+ !indexed?.live ||
642
+ !isSessionAuthorityEligible(indexed) ||
643
+ indexed.terminalUncertain ||
644
+ !Number.isSafeInteger(indexed.endpointGeneration) ||
645
+ indexed.endpointGeneration <= 0 ||
646
+ !Number.isSafeInteger(indexed.pid) ||
647
+ indexed.pid <= 0 ||
648
+ typeof indexed.endpointMtimeMs !== "number" ||
649
+ !Number.isFinite(indexed.endpointMtimeMs) ||
650
+ indexed.endpointMtimeMs <= 0
651
+ )
652
+ throw new SessionActivationError(
653
+ "session_not_live",
654
+ "Session activation requires an exact live session endpoint.",
655
+ );
656
+ const endpoint = await this.#readEndpoint(indexed).catch(() => null);
657
+ if (!endpoint || endpoint.stale === true || !endpoint.url || !endpoint.token || endpoint.pid !== indexed.pid)
658
+ throw new SessionActivationError(
659
+ "session_not_live",
660
+ "Session activation requires a readable session discovery endpoint.",
661
+ );
662
+
663
+ let client: PreparedSessionActivationClient;
664
+ try {
665
+ client = await (this.#deps.createClient
666
+ ? this.#deps.createClient({
667
+ sessionId: indexed.sessionId,
668
+ generation: indexed.endpointGeneration,
669
+ pid: indexed.pid,
670
+ endpointMtimeMs: indexed.endpointMtimeMs,
671
+ })
672
+ : connectPreparedSession(endpoint));
673
+ } catch {
674
+ throw new SessionActivationError("activation_unavailable", "The session endpoint could not be reached.");
675
+ }
676
+ try {
677
+ const currentEndpoint = await this.#readEndpoint(indexed).catch(() => null);
678
+ if (
679
+ !currentEndpoint ||
680
+ currentEndpoint.url !== endpoint.url ||
681
+ currentEndpoint.token !== endpoint.token ||
682
+ currentEndpoint.pid !== endpoint.pid
683
+ )
684
+ throw new SessionActivationError(
685
+ "session_not_live",
686
+ "The session endpoint changed before activation could be dispatched.",
687
+ );
688
+ return await requestPreparedSessionActivation(client, sessionId, indexed.endpointGeneration);
689
+ } finally {
690
+ await client.close().catch(() => undefined);
691
+ }
692
+ }
693
+
694
+ /** Lists saved sessions through Router-owned Broker discovery without exposing credentials or mutation authority. */
695
+ async listBrokerSessions(input: Record<string, unknown>, idempotencyKey: string): Promise<Record<string, unknown>> {
696
+ const operation = "session.list";
697
+ const discovery = await readSdkBrokerDiscovery(this.#agentDir);
698
+ if (!discovery) throw new SessionRouterError("pre_send", "SDK broker discovery is unavailable.");
699
+ let client: SessionRouterClient;
700
+ try {
701
+ client = this.#deps.createBrokerClient
702
+ ? await this.#deps.createBrokerClient()
703
+ : await SdkClient.connect(discovery.url, discovery.token);
704
+ } catch {
705
+ throw new SessionRouterError("pre_send", "SDK broker connection failed.");
706
+ }
707
+ try {
708
+ const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
709
+ return await client.request(
710
+ { type: "broker_request", operation, input, idempotencyKey },
711
+ timeoutMs === undefined ? undefined : { timeoutMs },
712
+ );
713
+ } finally {
714
+ await client.close().catch(error => {
715
+ logger.warn(`SDK Broker session.list transport cleanup failed (${String(error)}).`);
716
+ });
717
+ }
718
+ }
719
+
720
+ #serialReconcile(runEpoch: number, deferReplay = false): Promise<void> {
721
+ if (!this.#running(runEpoch)) return Promise.resolve();
722
+ const pending = this.#reconcilePending;
723
+ if (pending?.runEpoch === runEpoch) return this.#reconcileTail;
724
+ const queued = { runEpoch };
725
+ this.#reconcilePending = queued;
726
+ const task = this.#reconcileTail
727
+ .catch(() => undefined)
728
+ .then(async () => {
729
+ if (this.#reconcilePending === queued) this.#reconcilePending = undefined;
730
+ try {
731
+ await this.#reconcile(runEpoch, deferReplay);
732
+ if (!this.#running(runEpoch)) return;
733
+ this.#ready = true;
734
+ this.#deps.onReconciled?.();
735
+ } catch (error) {
736
+ if (this.#running(runEpoch)) this.#ready = false;
737
+ throw error;
738
+ }
739
+ });
740
+ this.#reconcileTail = task;
741
+ return task;
742
+ }
743
+
744
+ async #reconcile(runEpoch: number, deferReplay = false): Promise<void> {
745
+ if (!this.#running(runEpoch)) return;
746
+ await this.#index.open();
747
+ if (!this.#running(runEpoch)) return;
748
+ await this.#index.refresh();
749
+ if (!this.#running(runEpoch)) return;
750
+ const indexed = this.#index.listSessions();
751
+ const live =
752
+ indexed.warnings.length === 0
753
+ ? indexed.sessions.filter(
754
+ session => session.live && isSessionAuthorityEligible(session) && !session.terminalUncertain,
755
+ )
756
+ : [];
757
+ const liveIds = new Set(live.map(session => session.sessionId));
758
+ const attachedIds = new Set<string>();
759
+ if (indexed.warnings.length === 0) {
760
+ for (const [sessionId, adopted] of [...this.#adopted]) {
761
+ const attached = this.#sessions.get(sessionId);
762
+ const indexedSession = indexed.sessions.find(session => session.sessionId === sessionId);
763
+ if (!attached || attached.capability !== adopted.attachment) {
764
+ this.#adopted.delete(sessionId);
765
+ continue;
766
+ }
767
+ const exactIndex =
768
+ indexedSession?.live === true &&
769
+ isSessionAuthorityEligible(indexedSession) &&
770
+ !indexedSession.terminalUncertain &&
771
+ indexedSession.endpointGeneration === adopted.generation &&
772
+ indexedSession.pid === adopted.pid &&
773
+ indexedSession.endpointMtimeMs === adopted.endpointMtimeMs;
774
+ const endpoint = exactIndex ? await this.#readEndpoint(indexedSession).catch(() => null) : null;
775
+ if (
776
+ !exactIndex ||
777
+ !endpoint ||
778
+ endpoint.pid !== adopted.pid ||
779
+ endpoint.url !== attached.endpoint.url ||
780
+ endpoint.token !== attached.endpoint.token
781
+ ) {
782
+ this.#adopted.delete(sessionId);
783
+ await this.#retireAttachment(attached);
784
+ continue;
785
+ }
786
+ try {
787
+ if (await this.#publishAttachment(attached, true)) {
788
+ this.#adopted.delete(sessionId);
789
+ attachedIds.add(sessionId);
790
+ }
791
+ } catch {
792
+ this.#adopted.delete(sessionId);
793
+ await this.#retireAttachment(attached);
794
+ }
795
+ }
796
+ }
797
+ let nextAttachment = 0;
798
+ const attachWorkers = Array.from({ length: Math.min(ATTACH_CONCURRENCY, live.length) }, async () => {
799
+ for (;;) {
800
+ if (!this.#running(runEpoch)) return;
801
+ const session = live[nextAttachment++];
802
+ if (!session) return;
803
+ try {
804
+ if (await this.#attach(session, runEpoch, undefined, false, false, deferReplay))
805
+ attachedIds.add(session.sessionId);
806
+ } catch {
807
+ const failed = this.#sessions.get(session.sessionId);
808
+ if (failed?.runEpoch === runEpoch)
809
+ await this.#retireAttachment(failed, liveIds.has(session.sessionId) ? "replaced" : "removed");
810
+ if (this.#running(runEpoch))
811
+ logger.warn(
812
+ `SDK session attachment failed for indexed session ${session.sessionId} at generation ${session.endpointGeneration}; the endpoint remains unauthorized.`,
813
+ );
814
+ }
815
+ }
816
+ });
817
+ await Promise.all(attachWorkers);
818
+ if (!this.#running(runEpoch)) return;
819
+ const cleanupErrors: unknown[] = [];
820
+ for (const [sessionId, attached] of [...this.#sessions]) {
821
+ if (attachedIds.has(sessionId)) continue;
822
+ if (!this.#running(runEpoch) || this.#sessions.get(sessionId) !== attached) continue;
823
+ if (!liveIds.has(sessionId)) {
824
+ this.#undelivered.delete(sessionId);
825
+ this.#recoveredFrames.delete(sessionId);
826
+ }
827
+ try {
828
+ await this.#retireAttachment(attached, liveIds.has(sessionId) ? "replaced" : "removed");
829
+ } catch (error) {
830
+ cleanupErrors.push(error);
831
+ }
832
+ }
833
+ if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "SessionRouter stale cleanup failed.");
834
+ }
835
+
836
+ async #readEndpoint(indexed: IndexedSession): Promise<SdkSessionEndpoint | null> {
837
+ if (!isSessionAuthorityEligible(indexed)) return null;
838
+ const repo = path.resolve(indexed.locator.repo);
839
+ const defaultStateRoot = path.join(repo, ".gjc", "state");
840
+ const indexedStateRoot = path.resolve(indexed.locator.stateRoot);
841
+ const scope =
842
+ indexedStateRoot === defaultStateRoot
843
+ ? "default"
844
+ : indexedStateRoot === path.join(defaultStateRoot, "chat")
845
+ ? "chat"
846
+ : undefined;
847
+ if (!scope || indexed.endpointMtimeMs === undefined || !Number.isFinite(indexed.endpointMtimeMs)) return null;
848
+ const endpoint = await readSdkSessionEndpoint(repo, indexed.sessionId, scope);
849
+ if (!endpoint || endpoint.stale || endpoint.pid !== indexed.pid) return null;
850
+ const endpointStat = await fs.stat(endpoint.path).catch(() => undefined);
851
+ if (!endpointStat || endpointStat.mtimeMs !== indexed.endpointMtimeMs) return null;
852
+ let raw: Record<string, unknown>;
853
+ try {
854
+ const parsed = JSON.parse(await Bun.file(endpoint.path).text());
855
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
856
+ raw = parsed as Record<string, unknown>;
857
+ } catch {
858
+ return null;
859
+ }
860
+ if (
861
+ raw.sessionId !== indexed.sessionId ||
862
+ raw.pid !== indexed.pid ||
863
+ raw.stale === true ||
864
+ raw.url !== endpoint.url ||
865
+ raw.token !== endpoint.token
866
+ )
867
+ return null;
868
+ const endpointStatAfterRead = await fs.stat(endpoint.path).catch(() => undefined);
869
+ if (!endpointStatAfterRead || endpointStatAfterRead.mtimeMs !== indexed.endpointMtimeMs) return null;
870
+ await this.#index.refresh();
871
+ const listing = this.#index.listSessions();
872
+ if (listing.warnings.length > 0) return null;
873
+ const current = listing.sessions.find(session => session.sessionId === indexed.sessionId);
874
+ if (!current || !sameIndexedAuthority(indexed, current)) return null;
875
+ return endpoint;
876
+ }
877
+
878
+ async #createAttachedClient(
879
+ indexed: IndexedSession,
880
+ endpoint: SdkSessionEndpoint,
881
+ runEpoch: number,
882
+ ): Promise<SessionRouterClient | undefined> {
883
+ const endpointMtimeMs = indexed.endpointMtimeMs;
884
+ if (endpointMtimeMs === undefined) return undefined;
885
+ const createClient = this.#deps.createClient;
886
+ let transport: SdkClient | undefined;
887
+ let connection: Promise<SessionRouterClient>;
888
+ if (createClient) {
889
+ connection = createClient({
890
+ sessionId: indexed.sessionId,
891
+ generation: indexed.endpointGeneration,
892
+ pid: indexed.pid,
893
+ endpointMtimeMs,
894
+ });
895
+ } else {
896
+ const defaultClient = new SdkClient(endpoint.url, endpoint.token, { ...ACP_SESSION_RECONNECT });
897
+ transport = defaultClient;
898
+ connection = defaultClient.connect().then(() => defaultClient);
899
+ }
900
+ const stopped = Promise.withResolvers<void>();
901
+ const timeout = Promise.withResolvers<void>();
902
+ const signal = this.#stopController.signal;
903
+ const onStop = (): void => stopped.resolve();
904
+ if (!this.#running(runEpoch) || signal.aborted) onStop();
905
+ else signal.addEventListener("abort", onStop, { once: true });
906
+ const timer = (this.#deps.setTimeout ?? setTimeout)(() => timeout.resolve(), ATTACH_CONNECT_TIMEOUT_MS);
907
+ timer.unref?.();
908
+ try {
909
+ const outcome = await Promise.race([
910
+ connection.then(client => ({ kind: "client" as const, client })),
911
+ stopped.promise.then(() => ({ kind: "stopped" as const })),
912
+ timeout.promise.then(() => ({ kind: "timeout" as const })),
913
+ ]);
914
+ if (outcome.kind === "client") return outcome.client;
915
+ if (transport) void transport.close().catch(() => undefined);
916
+ void connection.then(client => client.close().catch(() => undefined)).catch(() => undefined);
917
+ if (outcome.kind === "stopped") return undefined;
918
+ throw new SessionRouterError("pre_send", "SDK session attachment connection timed out.");
919
+ } finally {
920
+ (this.#deps.clearTimeout ?? clearTimeout)(timer);
921
+ signal.removeEventListener("abort", onStop);
922
+ }
923
+ }
924
+
925
+ async #attach(
926
+ indexed: IndexedSession,
927
+ runEpoch: number,
928
+ resolvedEndpoint?: SdkSessionEndpoint,
929
+ skipReplay = false,
930
+ deferPublication = false,
931
+ deferReplay = false,
932
+ ): Promise<boolean> {
933
+ const retirementVersion = this.#retirementVersions.get(indexed.sessionId) ?? 0;
934
+ const retirement = this.#retirements.get(indexed.sessionId);
935
+ if (retirement) await retirement;
936
+ if (!this.#running(runEpoch)) return false;
937
+ if (indexed.endpointMtimeMs === undefined) return false;
938
+ const endpoint = resolvedEndpoint ?? (await this.#readEndpoint(indexed));
939
+ const retirementAfterValidation = this.#retirements.get(indexed.sessionId);
940
+ if (retirementAfterValidation) await retirementAfterValidation;
941
+ if ((this.#retirementVersions.get(indexed.sessionId) ?? 0) !== retirementVersion)
942
+ return await this.#attach(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
943
+ if (!this.#running(runEpoch)) return false;
944
+ if (!endpoint) return false;
945
+ const existing = this.#sessions.get(indexed.sessionId);
946
+ const resumable =
947
+ existing !== undefined &&
948
+ existing.endpoint.url === endpoint.url &&
949
+ existing.endpoint.token === endpoint.token &&
950
+ existing.generation === indexed.endpointGeneration &&
951
+ existing.pid === indexed.pid &&
952
+ existing.endpointMtimeMs === indexed.endpointMtimeMs;
953
+ if (existing && resumable && !existing.barrier.failed) {
954
+ this.#reviveTransport(existing);
955
+ return true;
956
+ }
957
+ const resumeSeq = existing && resumable ? existing.cursor.seq : 0;
958
+ if (existing) {
959
+ this.#adopted.delete(indexed.sessionId);
960
+
961
+ this.#sessions.delete(indexed.sessionId);
962
+ existing.dispose();
963
+ this.#detachNotification(
964
+ existing,
965
+ existing.generation === indexed.endpointGeneration ? "replaced_same_generation" : "replaced",
966
+ );
967
+ try {
968
+ await existing.client.close();
969
+ } catch (error) {
970
+ logger.warn(
971
+ `SDK session replacement transport cleanup failed for ${indexed.sessionId}; authority remains revoked (${String(error)}).`,
972
+ );
973
+ }
974
+ void Promise.resolve(
975
+ this.#deps.onSessionRemoved?.(
976
+ existing.capability,
977
+ existing.generation === indexed.endpointGeneration &&
978
+ (existing.endpoint.url !== endpoint.url ||
979
+ existing.endpoint.token !== endpoint.token ||
980
+ existing.pid !== indexed.pid ||
981
+ existing.endpointMtimeMs !== indexed.endpointMtimeMs)
982
+ ? "replaced_same_generation"
983
+ : "replaced",
984
+ ),
985
+ ).catch(error => logger.warn(`SDK provider cleanup failed after replacement: ${String(error)}`));
986
+ if (!resumable) {
987
+ this.#undelivered.delete(indexed.sessionId);
988
+ this.#recoveredFrames.delete(indexed.sessionId);
989
+ }
990
+ }
991
+ const client = await this.#createAttachedClient(indexed, endpoint, runEpoch);
992
+ if (!client) return false;
993
+
994
+ if (!this.#running(runEpoch)) {
995
+ await client.close().catch(() => undefined);
996
+ return false;
997
+ }
998
+ if ((this.#retirementVersions.get(indexed.sessionId) ?? 0) !== retirementVersion) {
999
+ await client.close().catch(() => undefined);
1000
+ const currentRetirement = this.#retirements.get(indexed.sessionId);
1001
+ if (currentRetirement) await currentRetirement;
1002
+ return await this.#attach(indexed, runEpoch, undefined, skipReplay, deferPublication, deferReplay);
1003
+ }
1004
+ let attached: AttachedSession | undefined;
1005
+ const barrier: ReplayBarrier = { held: undefined, detached: false, failed: false };
1006
+ const publication = Promise.withResolvers<void>();
1007
+ void publication.promise.catch(() => undefined);
1008
+ const capability: SessionAttachment = Object.freeze({
1009
+ authorityId: sessionAttachmentAuthorityId({
1010
+ sessionId: indexed.sessionId,
1011
+ generation: indexed.endpointGeneration,
1012
+ pid: indexed.pid,
1013
+ endpointMtimeMs: indexed.endpointMtimeMs,
1014
+ url: endpoint.url,
1015
+ token: endpoint.token,
1016
+ }),
1017
+ sessionId: indexed.sessionId,
1018
+ generation: indexed.endpointGeneration,
1019
+ get connectionId(): string | undefined {
1020
+ return attached?.client.connectionId;
1021
+ },
1022
+ isCurrent: () => attached !== undefined && this.#attachmentPublished(attached),
1023
+ send: async (frame: Record<string, unknown>) => {
1024
+ if (!attached || !this.#attachmentPublished(attached))
1025
+ throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
1026
+ if (attached.initializingPublication) {
1027
+ const endpoint = await this.#readEndpoint(attached.indexed);
1028
+ if (
1029
+ !endpoint ||
1030
+ endpoint.url !== attached.endpoint.url ||
1031
+ endpoint.token !== attached.endpoint.token ||
1032
+ endpoint.pid !== attached.pid
1033
+ ) {
1034
+ await this.#retireAttachment(attached, endpoint ? "replaced_same_generation" : undefined);
1035
+ throw new SessionRouterError("pre_send", "SDK session attachment changed during publication.");
1036
+ }
1037
+ } else await this.#serialReconcile(runEpoch, true);
1038
+ if (!attached || !this.#attachmentPublished(attached))
1039
+ throw new SessionRouterError("pre_send", "SDK session attachment is stale.");
1040
+ attached.client.send(this.#prepareFrame(attached, frame));
1041
+ },
1042
+ retire: async () => {
1043
+ if (attached) await this.#retireAttachment(attached);
1044
+ },
1045
+ });
1046
+ const notificationCursor = { generation: indexed.endpointGeneration, seq: resumeSeq };
1047
+ const notificationSubscription: NotificationSubscription = Object.freeze({
1048
+ sessionId: indexed.sessionId,
1049
+ subscriptionId: `notification:${indexed.sessionId}:${crypto.randomUUID()}`,
1050
+ cursor: notificationCursor,
1051
+ isActive: () => attached !== undefined && !attached.notificationCancelled && this.#attachmentLive(attached),
1052
+ send: (frame: Record<string, unknown>) => {
1053
+ if (!attached || attached.notificationCancelled || !this.#attachmentLive(attached))
1054
+ throw new SessionRouterError("pre_send", "Notification subscription is cancelled.");
1055
+ attached.client.send(this.#prepareFrame(attached, frame));
1056
+ },
1057
+ advanceCursor: (generation: number, seq: number) => {
1058
+ if (!Number.isSafeInteger(generation) || generation < 0 || !Number.isSafeInteger(seq) || seq < 0) return;
1059
+ if (
1060
+ generation > notificationCursor.generation ||
1061
+ (generation === notificationCursor.generation && seq > notificationCursor.seq)
1062
+ ) {
1063
+ notificationCursor.generation = generation;
1064
+ notificationCursor.seq = seq;
1065
+ }
1066
+ },
1067
+ cancel: (reason?: string) => {
1068
+ if (attached) this.#detachNotification(attached, "cancelled");
1069
+ if (reason) this.#recordNotificationReceipt(notificationSubscription, "pending", reason);
1070
+ },
1071
+ });
1072
+ const disposeFrames = client.onFrame(frame => {
1073
+ if (!attached) return;
1074
+ const task =
1075
+ frame.type === "event_replay_result" && frame.seq === undefined
1076
+ ? this.#deliverOutOfBandFrame(attached, frame)
1077
+ : this.#enqueueFrame(attached, frame, "live");
1078
+ this.#schedule(task);
1079
+ });
1080
+ const disposeReconnect = client.onReconnect?.(() => {
1081
+ if (attached) {
1082
+ attached.barrier.held ??= [];
1083
+ this.#schedule(this.#reinitializeAttachment(attached));
1084
+ }
1085
+ });
1086
+ attached = {
1087
+ initializingPublication: false,
1088
+ id: crypto.randomUUID(),
1089
+ sessionId: indexed.sessionId,
1090
+ endpoint,
1091
+ pid: indexed.pid,
1092
+ endpointMtimeMs: indexed.endpointMtimeMs,
1093
+ generation: indexed.endpointGeneration,
1094
+ runEpoch,
1095
+ client,
1096
+ indexed,
1097
+ readyTail: Promise.resolve(),
1098
+ cursor: { seq: resumeSeq },
1099
+ barrier,
1100
+ capability,
1101
+ notificationSubscription,
1102
+ notificationCancelled: false,
1103
+ notificationCursor,
1104
+ published: false,
1105
+ publication,
1106
+ dispose: () => {
1107
+ disposeFrames();
1108
+ disposeReconnect?.();
1109
+ barrier.detached = true;
1110
+ if (!attached?.published)
1111
+ publication.reject(
1112
+ new SessionRouterError("pre_send", "SDK session publication was detached before completion."),
1113
+ );
1114
+ barrier.held = undefined;
1115
+ },
1116
+ };
1117
+ this.#sessions.set(indexed.sessionId, attached);
1118
+ if (deferPublication)
1119
+ this.#adopted.set(indexed.sessionId, {
1120
+ generation: indexed.endpointGeneration,
1121
+ pid: indexed.pid,
1122
+ endpointMtimeMs: indexed.endpointMtimeMs,
1123
+ attachment: capability,
1124
+ });
1125
+ try {
1126
+ await this.#deps.onAttachment?.(capability);
1127
+ this.#recordNotificationReceipt(notificationSubscription, "pending");
1128
+ void Promise.resolve()
1129
+ .then(() => this.#deps.onNotificationSubscription?.(notificationSubscription))
1130
+ .catch(error => {
1131
+ this.#detachNotification(attached!, "cancelled");
1132
+ logger.warn(`SDK notification subscription admission failed: ${String(error)}`);
1133
+ });
1134
+ } catch (error) {
1135
+ const failedStillCurrent = this.#sessions.get(indexed.sessionId) === attached;
1136
+ this.#adopted.delete(indexed.sessionId);
1137
+ if (failedStillCurrent) this.#sessions.delete(indexed.sessionId);
1138
+ attached.dispose();
1139
+ await attached.client.close().catch(() => undefined);
1140
+ if (failedStillCurrent)
1141
+ try {
1142
+ await this.#deps.onSessionRemoved?.(capability);
1143
+ } catch {
1144
+ // Attachment publication failed closed; provider cleanup remains best effort.
1145
+ }
1146
+ throw error;
1147
+ }
1148
+ if (deferPublication) return true;
1149
+ return await this.#publishAttachment(attached, skipReplay, deferReplay);
1150
+ }
1151
+
1152
+ async #publishAttachment(attached: AttachedSession, skipReplay: boolean, deferReplay = false): Promise<boolean> {
1153
+ if (attached.published) return this.#attachmentPublished(attached);
1154
+ if (!this.#attachmentLive(attached)) return false;
1155
+ const endpoint = await this.#readEndpoint(attached.indexed).catch(() => null);
1156
+ if (!this.#attachmentLive(attached)) return false;
1157
+ if (
1158
+ !endpoint ||
1159
+ endpoint.url !== attached.endpoint.url ||
1160
+ endpoint.token !== attached.endpoint.token ||
1161
+ endpoint.pid !== attached.pid
1162
+ ) {
1163
+ await this.#retireAttachment(attached, endpoint ? "replaced_same_generation" : undefined);
1164
+ return false;
1165
+ }
1166
+ if (!skipReplay) attached.barrier.held ??= [];
1167
+ attached.published = true;
1168
+ attached.publication.resolve();
1169
+ attached.initializingPublication = true;
1170
+ try {
1171
+ void Promise.resolve()
1172
+ .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1173
+ .catch(error => {
1174
+ this.#detachNotification(attached, "cancelled");
1175
+ logger.warn(`SDK notification subscription ready hook failed locally: ${String(error)}`);
1176
+ });
1177
+ await this.#deps.onAttachmentReady?.(attached.capability);
1178
+ } catch (error) {
1179
+ const stillCurrent = this.#sessions.get(attached.sessionId) === attached;
1180
+ attached.published = false;
1181
+ this.#adopted.delete(attached.sessionId);
1182
+ if (stillCurrent) this.#sessions.delete(attached.sessionId);
1183
+ attached.dispose();
1184
+ await attached.client.close().catch(() => undefined);
1185
+ if (stillCurrent)
1186
+ try {
1187
+ await this.#deps.onSessionRemoved?.(attached.capability);
1188
+ } catch {
1189
+ // Ready publication failed closed; provider cleanup remains best effort.
1190
+ }
1191
+ throw error;
1192
+ } finally {
1193
+ attached.initializingPublication = false;
1194
+ }
1195
+ if (skipReplay) return true;
1196
+ // When the caller drives the serialized reconcile tail (periodic
1197
+ // re-attachment after a rehost), initial replay must not hold it: each
1198
+ // replay owns its own retry budget, so awaiting it here wedges all later
1199
+ // reconciles (and the sends that funnel through them) until the budget
1200
+ // expires. The barrier still holds live frames, so ordering and
1201
+ // generation fences are unchanged; replay just runs on the attachment's
1202
+ // ready tail like the reconnect path (#4527).
1203
+ if (deferReplay) {
1204
+ attached.readyTail = attached.readyTail
1205
+ .catch(() => undefined)
1206
+ .then(async () => {
1207
+ if (!this.#attachmentLive(attached)) return;
1208
+ if (!(await this.#deliverRecoveredFrames(attached))) return;
1209
+ await this.#replayAttachment(attached, attached.cursor.seq);
1210
+ });
1211
+ return true;
1212
+ }
1213
+ if (!(await this.#deliverRecoveredFrames(attached))) return false;
1214
+ await this.#replayAttachment(attached, attached.cursor.seq);
1215
+ return true;
1216
+ }
1217
+
1218
+ async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
1219
+ const previous = attached.readyTail;
1220
+ const current = previous
1221
+ .catch(() => undefined)
1222
+ .then(async () => {
1223
+ if (!this.#attachmentLive(attached)) return;
1224
+ const endpoint = await this.#readEndpoint(attached.indexed);
1225
+ if (
1226
+ !endpoint ||
1227
+ endpoint.url !== attached.endpoint.url ||
1228
+ endpoint.token !== attached.endpoint.token ||
1229
+ endpoint.pid !== attached.pid
1230
+ ) {
1231
+ await this.#retireAttachment(attached, endpoint ? "replaced_same_generation" : undefined);
1232
+ return;
1233
+ }
1234
+ attached.initializingPublication = true;
1235
+ try {
1236
+ void Promise.resolve()
1237
+ .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1238
+ .catch(error => {
1239
+ this.#detachNotification(attached, "cancelled");
1240
+ logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
1241
+ });
1242
+ await this.#deps.onAttachmentReady?.(attached.capability);
1243
+ } catch {
1244
+ await this.#retireAttachment(attached);
1245
+ return;
1246
+ } finally {
1247
+ attached.initializingPublication = false;
1248
+ }
1249
+ if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
1250
+ });
1251
+ attached.readyTail = current;
1252
+ await current;
1253
+ }
1254
+ #reviveTransport(attached: AttachedSession): void {
1255
+ const connect = attached.client.connect?.bind(attached.client);
1256
+ if (!connect || this.#reviving.has(attached.id)) return;
1257
+ this.#reviving.add(attached.id);
1258
+ void connect()
1259
+ .catch(() => undefined)
1260
+ .finally(() => this.#reviving.delete(attached.id));
1261
+ }
1262
+
1263
+ #running(runEpoch: number): boolean {
1264
+ return this.#started && runEpoch === this.#runEpoch;
1265
+ }
1266
+
1267
+ #attachmentLive(attached: AttachedSession): boolean {
1268
+ return (
1269
+ this.#running(attached.runEpoch) &&
1270
+ !attached.barrier.detached &&
1271
+ !attached.barrier.failed &&
1272
+ this.#sessions.get(attached.sessionId) === attached
1273
+ );
1274
+ }
1275
+
1276
+ #attachmentPublished(attached: AttachedSession): boolean {
1277
+ return attached.published && this.#attachmentLive(attached);
1278
+ }
1279
+
1280
+ #recordNotificationReceipt(
1281
+ subscription: NotificationSubscription,
1282
+ state: NotificationCleanupState,
1283
+ reason?: string,
1284
+ ): void {
1285
+ this.#notificationReceipts.set(subscription.subscriptionId, {
1286
+ subscriptionId: subscription.subscriptionId,
1287
+ sessionId: subscription.sessionId,
1288
+ state,
1289
+ ...(reason ? { reason: reason.slice(0, 256) } : {}),
1290
+ });
1291
+ }
1292
+
1293
+ async #boundedNotificationWork<T>(work: () => Promise<T> | T): Promise<T | typeof NOTIFICATION_WORK_TIMEOUT> {
1294
+ const timeout: Promise<typeof NOTIFICATION_WORK_TIMEOUT> = Bun.sleep(NOTIFICATION_WORK_TIMEOUT_MS).then(
1295
+ () => NOTIFICATION_WORK_TIMEOUT,
1296
+ );
1297
+ return await Promise.race([Promise.resolve().then(work), timeout]);
1298
+ }
1299
+
1300
+ #detachNotification(
1301
+ attached: AttachedSession,
1302
+ reason: "removed" | "replaced" | "replaced_same_generation" | "cancelled",
1303
+ ): void {
1304
+ if (attached.notificationCancelled) return;
1305
+ attached.notificationCancelled = true;
1306
+ this.#recordNotificationReceipt(attached.notificationSubscription, "pending", reason);
1307
+ const work = this.#boundedNotificationWork(() =>
1308
+ this.#deps.onNotificationSubscriptionRemoved?.(attached.notificationSubscription, reason),
1309
+ )
1310
+ .then(
1311
+ result =>
1312
+ this.#recordNotificationReceipt(
1313
+ attached.notificationSubscription,
1314
+ result === NOTIFICATION_WORK_TIMEOUT ? "failed" : "completed",
1315
+ reason,
1316
+ ),
1317
+ (error: unknown) =>
1318
+ this.#recordNotificationReceipt(
1319
+ attached.notificationSubscription,
1320
+ "failed",
1321
+ error instanceof Error ? error.message : String(error),
1322
+ ),
1323
+ )
1324
+ .catch(() => undefined);
1325
+ void work;
1326
+ }
1327
+
1328
+ #dispatchNotificationFrame(attached: AttachedSession, frame: SessionRouterFrame): void {
1329
+ if (attached.notificationCancelled || !this.#attachmentLive(attached)) return;
1330
+ const callback = this.#deps.onNotificationFrame;
1331
+ if (!callback) return;
1332
+ const work = this.#boundedNotificationWork(async () => {
1333
+ await callback(attached.notificationSubscription, frame);
1334
+ return true;
1335
+ }).then(
1336
+ result => {
1337
+ if (result === NOTIFICATION_WORK_TIMEOUT) {
1338
+ this.#detachNotification(attached, "cancelled");
1339
+ return;
1340
+ }
1341
+ if (frame.seq !== undefined)
1342
+ attached.notificationSubscription.advanceCursor(frame.generation ?? attached.generation, frame.seq);
1343
+ },
1344
+ (error: unknown) => {
1345
+ this.#detachNotification(attached, "cancelled");
1346
+ logger.warn(
1347
+ `SDK notification subscription ${attached.notificationSubscription.subscriptionId} failed locally: ${
1348
+ error instanceof Error ? error.message : String(error)
1349
+ }`,
1350
+ );
1351
+ },
1352
+ );
1353
+ void work;
1354
+ }
1355
+
1356
+ async #retireAttachment(
1357
+ attached: AttachedSession,
1358
+ explicitReason?: "removed" | "replaced" | "replaced_same_generation",
1359
+ ): Promise<void> {
1360
+ this.#adopted.delete(attached.sessionId);
1361
+ if (this.#sessions.get(attached.sessionId) !== attached) return;
1362
+ this.#retirementVersions.set(attached.sessionId, (this.#retirementVersions.get(attached.sessionId) ?? 0) + 1);
1363
+ this.#sessions.delete(attached.sessionId);
1364
+ attached.dispose();
1365
+ const gate = Promise.withResolvers<void>();
1366
+ this.#retirements.set(attached.sessionId, gate.promise);
1367
+ try {
1368
+ let reason = explicitReason;
1369
+ if (reason === undefined) {
1370
+ try {
1371
+ await this.#index.refresh();
1372
+ const current = this.#index
1373
+ .listSessions()
1374
+ .sessions.find(session => session.sessionId === attached.sessionId);
1375
+ if (
1376
+ current?.live &&
1377
+ (current.endpointGeneration !== attached.generation ||
1378
+ current.pid !== attached.pid ||
1379
+ current.endpointMtimeMs !== attached.endpointMtimeMs)
1380
+ )
1381
+ reason = current.endpointGeneration === attached.generation ? "replaced_same_generation" : "replaced";
1382
+ } catch {
1383
+ // Revocation remains terminal when current Broker authority cannot be proven.
1384
+ }
1385
+ }
1386
+ reason ??= "removed";
1387
+ void Promise.resolve(this.#deps.onSessionRemoved?.(attached.capability, reason)).catch(error =>
1388
+ logger.warn(`SDK provider cleanup failed after authority revocation: ${String(error)}`),
1389
+ );
1390
+ this.#detachNotification(attached, reason);
1391
+ await attached.client.close().catch(() => undefined);
1392
+ } finally {
1393
+ gate.resolve();
1394
+ if (this.#retirements.get(attached.sessionId) === gate.promise) this.#retirements.delete(attached.sessionId);
1395
+ }
1396
+ }
1397
+
1398
+ #failBarrier(attached: AttachedSession, reason: string): void {
1399
+ if (attached.barrier.detached || attached.barrier.failed) return;
1400
+ attached.barrier.failed = true;
1401
+ attached.barrier.held = undefined;
1402
+ logger.warn(
1403
+ `chat daemon replay barrier failed (${reason}); rebuilding session ${attached.sessionId} at generation ${attached.generation} from seq ${attached.cursor.seq}.`,
1404
+ );
1405
+ }
1406
+ #failDelivery(attached: AttachedSession, seq: number, error: unknown): void {
1407
+ const previous = this.#undelivered.get(attached.sessionId);
1408
+ const attempts = previous?.generation === attached.generation && previous.seq === seq ? previous.attempts + 1 : 1;
1409
+ const reason = error instanceof Error ? error.message : String(error);
1410
+ if (attempts >= DELIVERY_ATTEMPT_LIMIT) {
1411
+ this.#undelivered.delete(attached.sessionId);
1412
+ this.#removeRecoveredFrame(attached.sessionId, attached.generation, seq);
1413
+ attached.cursor.seq = seq;
1414
+ logger.warn(
1415
+ `chat daemon conceded seq ${seq} of session ${attached.sessionId} at generation ${attached.generation} after ${attempts} refused publications (${reason}); delivery resumes above it.`,
1416
+ );
1417
+ return;
1418
+ }
1419
+ this.#undelivered.set(attached.sessionId, { generation: attached.generation, seq, attempts });
1420
+ this.#failBarrier(attached, `publication failed at seq ${seq} (${reason})`);
1421
+ }
1422
+
1423
+ #rememberRecoveredFrame(attached: AttachedSession, seq: number, frame: Record<string, unknown>): void {
1424
+ let pending = this.#recoveredFrames.get(attached.sessionId);
1425
+ if (!pending || pending.generation !== attached.generation) {
1426
+ pending = { generation: attached.generation, frames: [] };
1427
+ this.#recoveredFrames.set(attached.sessionId, pending);
1428
+ }
1429
+ const existing = pending.frames.find(item => item.seq === seq);
1430
+ if (existing) existing.frame = frame;
1431
+ else {
1432
+ pending.frames.push({ seq, frame });
1433
+ pending.frames.sort((left, right) => left.seq - right.seq);
1434
+ }
1435
+ }
1436
+
1437
+ #removeRecoveredFrame(sessionId: string, generation: number, seq: number): void {
1438
+ const pending = this.#recoveredFrames.get(sessionId);
1439
+ if (!pending || pending.generation !== generation) return;
1440
+ pending.frames = pending.frames.filter(item => item.seq !== seq);
1441
+ if (pending.frames.length === 0) this.#recoveredFrames.delete(sessionId);
1442
+ }
1443
+
1444
+ async #deliverRecoveredFrames(attached: AttachedSession): Promise<boolean> {
1445
+ const pending = this.#recoveredFrames.get(attached.sessionId);
1446
+ if (!pending || pending.generation !== attached.generation) return true;
1447
+ for (const item of [...pending.frames]) {
1448
+ if (item.seq <= attached.cursor.seq) {
1449
+ this.#removeRecoveredFrame(attached.sessionId, attached.generation, item.seq);
1450
+ continue;
1451
+ }
1452
+ await this.#enqueueFrame(attached, item.frame, "ordered");
1453
+ if (attached.barrier.detached || attached.barrier.failed) return false;
1454
+ }
1455
+ return true;
1456
+ }
1457
+
1458
+ #schedule(task: Promise<void>): void {
1459
+ this.#pending.add(task);
1460
+ void task.then(
1461
+ () => this.#pending.delete(task),
1462
+ () => this.#pending.delete(task),
1463
+ );
1464
+ }
1465
+
1466
+ async #deliverOutOfBandFrame(attached: AttachedSession, frame: Record<string, unknown>): Promise<void> {
1467
+ if (!this.#attachmentLive(attached)) return;
1468
+ if (!attached.published) {
1469
+ await attached.publication.promise.catch(() => undefined);
1470
+ if (!this.#attachmentPublished(attached)) return;
1471
+ }
1472
+ const correlated = this.#correlateFrame(frame);
1473
+ if (!correlated) return;
1474
+ if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
1475
+ if (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
1476
+ this.#dispatchNotificationFrame(attached, correlated);
1477
+ void Promise.resolve(this.#deps.onFrame?.(attached.capability, correlated)).catch(error =>
1478
+ logger.warn(`SDK provider frame hook failed: ${String(error)}`),
1479
+ );
1480
+ }
1481
+ #enqueueFrame(attached: AttachedSession, frame: Record<string, unknown>, origin: FrameOrigin): Promise<void> {
1482
+ const previous = this.#frameTails.get(attached.id) ?? Promise.resolve();
1483
+ const current = previous
1484
+ .catch(() => undefined)
1485
+ .then(async () => {
1486
+ if (!this.#attachmentLive(attached)) return;
1487
+ if (!attached.published) {
1488
+ await attached.publication.promise.catch(() => undefined);
1489
+ if (!this.#attachmentPublished(attached)) return;
1490
+ }
1491
+ const correlated = this.#correlateFrame(frame);
1492
+ if (!correlated) return;
1493
+ const seq = typeof frame.seq === "number" && Number.isSafeInteger(frame.seq) ? frame.seq : undefined;
1494
+ if (correlated.sessionId !== undefined && correlated.sessionId !== attached.sessionId) return;
1495
+ if (correlated.generation !== undefined && correlated.generation !== attached.generation) return;
1496
+ if (seq !== undefined && correlated.generation === undefined) return;
1497
+ const ownsSequence =
1498
+ correlated.generation === attached.generation &&
1499
+ (correlated.sessionId === undefined || correlated.sessionId === attached.sessionId);
1500
+ if (seq !== undefined && ownsSequence) {
1501
+ if (seq <= attached.cursor.seq) return;
1502
+ const held = attached.barrier.held;
1503
+ if (held && origin === "live") {
1504
+ if (held.length >= REPLAY_BARRIER_LIMIT) {
1505
+ this.#failBarrier(attached, `hold buffer overflowed at ${REPLAY_BARRIER_LIMIT} frames`);
1506
+ return;
1507
+ }
1508
+ held.push({ seq, frame });
1509
+ return;
1510
+ }
1511
+ }
1512
+ const publicationId =
1513
+ seq !== undefined && ownsSequence ? `${attached.sessionId}:${attached.generation}:${seq}` : undefined;
1514
+ try {
1515
+ const notificationFrame = publicationId === undefined ? correlated : { ...correlated, publicationId };
1516
+ this.#dispatchNotificationFrame(attached, notificationFrame);
1517
+ await this.#deps.onFrame?.(attached.capability, notificationFrame);
1518
+ } catch (error) {
1519
+ if (!this.#attachmentLive(attached)) return;
1520
+ if (seq === undefined || !ownsSequence) throw error;
1521
+ this.#failDelivery(attached, seq, error);
1522
+ return;
1523
+ }
1524
+ if (!this.#attachmentLive(attached)) return;
1525
+ if (seq !== undefined && ownsSequence) {
1526
+ this.#undelivered.delete(attached.sessionId);
1527
+ this.#removeRecoveredFrame(attached.sessionId, attached.generation, seq);
1528
+ if (seq > attached.cursor.seq) attached.cursor.seq = seq;
1529
+ }
1530
+ });
1531
+ this.#frameTails.set(attached.id, current);
1532
+ void current.then(
1533
+ () => {
1534
+ if (this.#frameTails.get(attached.id) === current) this.#frameTails.delete(attached.id);
1535
+ },
1536
+ () => {
1537
+ if (this.#frameTails.get(attached.id) === current) this.#frameTails.delete(attached.id);
1538
+ },
1539
+ );
1540
+ return current;
1541
+ }
1542
+
1543
+ async #drainHeldFrames(attached: AttachedSession, held: HeldFrame[]): Promise<void> {
1544
+ for (;;) {
1545
+ if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
1546
+ if (held.length === 0) {
1547
+ attached.barrier.held = undefined;
1548
+ return;
1549
+ }
1550
+ const batch = held.splice(0, held.length).sort((left, right) => left.seq - right.seq);
1551
+ for (const entry of batch) await this.#enqueueFrame(attached, entry.frame, "ordered");
1552
+ }
1553
+ }
1554
+
1555
+ async #replayAttachment(attached: AttachedSession, sinceSeq: number): Promise<void> {
1556
+ if (!this.#attachmentLive(attached)) return;
1557
+ const held: HeldFrame[] = attached.barrier.held ?? [];
1558
+ attached.barrier.held = held;
1559
+ try {
1560
+ let replay: Record<string, unknown>;
1561
+ for (let attempt = 0; ; attempt++) {
1562
+ try {
1563
+ const stopped = Promise.withResolvers<void>();
1564
+ const onStop = (): void => stopped.resolve();
1565
+ if (this.#stopController.signal.aborted) stopped.resolve();
1566
+ else this.#stopController.signal.addEventListener("abort", onStop, { once: true });
1567
+ const replayRequest = attached.client.request({
1568
+ type: "event_replay",
1569
+ sinceGeneration: attached.generation,
1570
+ sinceSeq,
1571
+ });
1572
+ let outcome: { kind: "response"; value: Record<string, unknown> } | { kind: "stopped" };
1573
+ try {
1574
+ outcome = await Promise.race([
1575
+ replayRequest.then(value => ({ kind: "response" as const, value })),
1576
+ stopped.promise.then(() => ({ kind: "stopped" as const })),
1577
+ ]);
1578
+ } finally {
1579
+ this.#stopController.signal.removeEventListener("abort", onStop);
1580
+ }
1581
+ if (outcome.kind === "stopped") return;
1582
+ replay = outcome.value;
1583
+ break;
1584
+ } catch {
1585
+ if (attempt >= REPLAY_RETRY_ATTEMPTS) {
1586
+ this.#failBarrier(attached, "replay went unanswered");
1587
+ return;
1588
+ }
1589
+ await Bun.sleep(REPLAY_RETRY_BACKOFF_MS * 2 ** attempt);
1590
+ if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
1591
+ }
1592
+ }
1593
+ if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
1594
+ await this.#frameTails.get(attached.id)?.catch(() => undefined);
1595
+ if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
1596
+ const events = Array.isArray(replay.events)
1597
+ ? replay.events.filter(
1598
+ (event): event is Record<string, unknown> =>
1599
+ !!event && typeof event === "object" && !Array.isArray(event),
1600
+ )
1601
+ : [];
1602
+ if (replay.gap !== undefined) {
1603
+ const gap = readReplayGap(replay.gap);
1604
+ if (!gap) {
1605
+ this.#failBarrier(attached, "replay reported a gap it did not state");
1606
+ return;
1607
+ }
1608
+ if (gap.kind === "generation_reset") {
1609
+ this.#failBarrier(attached, `replay reported a generation reset to ${gap.toGeneration}`);
1610
+ return;
1611
+ }
1612
+ if (gap.fromSeq !== sinceSeq + 1) {
1613
+ this.#failBarrier(
1614
+ attached,
1615
+ `replay conceded sequences ${gap.fromSeq}-${gap.toSeq} for a request that resumed from seq ${sinceSeq}`,
1616
+ );
1617
+ return;
1618
+ }
1619
+ const retained = events
1620
+ .map(event => readSequence(event.seq))
1621
+ .find(seq => seq !== undefined && seq <= gap.toSeq);
1622
+ if (retained !== undefined) {
1623
+ this.#failBarrier(
1624
+ attached,
1625
+ `replay conceded sequences ${gap.fromSeq}-${gap.toSeq} while returning seq ${retained}`,
1626
+ );
1627
+ return;
1628
+ }
1629
+ const recovered = held.filter(entry => entry.seq <= gap.toSeq).sort((left, right) => left.seq - right.seq);
1630
+ const carried = held.filter(entry => entry.seq > gap.toSeq);
1631
+ held.splice(0, held.length, ...carried);
1632
+ const recoveredNote =
1633
+ recovered.length > 0 ? `, ${recovered.length} of them recovered from live delivery` : "";
1634
+ logger.warn(
1635
+ `chat daemon replay conceded a retention gap (sequences ${gap.fromSeq}-${gap.toSeq} are gone from the host${recoveredNote}); session ${attached.sessionId} generation ${attached.generation} resumes at seq ${gap.toSeq + 1}.`,
1636
+ );
1637
+ for (const entry of recovered) this.#rememberRecoveredFrame(attached, entry.seq, entry.frame);
1638
+ if (!(await this.#deliverRecoveredFrames(attached))) return;
1639
+ if (gap.toSeq > attached.cursor.seq) attached.cursor.seq = gap.toSeq;
1640
+ }
1641
+ for (const event of events) await this.#enqueueFrame(attached, event, "ordered");
1642
+ await this.#drainHeldFrames(attached, held);
1643
+ } finally {
1644
+ if (attached.barrier.held === held) attached.barrier.held = undefined;
1645
+ }
1646
+ }
1647
+ }
1648
+
1649
+ async function connectPreparedSession(endpoint: {
1650
+ url: string;
1651
+ token: string;
1652
+ }): Promise<PreparedSessionActivationClient> {
1653
+ const client = await SdkClient.connect(endpoint.url, endpoint.token, { reconnectAttempts: 0 });
1654
+ return {
1655
+ request: async frame => (await client.request(frame)) as Record<string, unknown>,
1656
+ close: async () => await client.close(),
1657
+ };
1658
+ }