@gajae-code/coding-agent 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  /**
3
- * Per-session forum-topic registry for the threaded session surface.
3
+ * Forum-topic registry for eligible orchestration sessions on the threaded
4
+ * Telegram surface.
4
5
  *
5
- * Each GJC session owns one active Telegram forum topic. Remote archive closes
6
- * daemon-created topics without deleting their durable records; rotated
7
- * successors move inactive records into retained history before creating a new
8
- * active authority. The registry also tracks whether the one-time identity
9
- * header has already been pinned.
6
+ * Each eligible orchestration session owns one active Telegram forum topic.
7
+ * Remote archive closes daemon-created topics without deleting their durable
8
+ * records; rotated successors move inactive records into retained history before
9
+ * creating a new active authority. The registry also tracks whether the one-time
10
+ * identity header has already been pinned.
10
11
  *
11
12
  * State is a plain serialisable map persisted beside the daemon state files;
12
13
  * topic creation is injected so this module is pure and unit-testable without a
@@ -60,6 +61,10 @@ export interface TopicRecord {
60
61
  authorityState?: TopicLifecycleState;
61
62
  /** Telegram chat and endpoint authority last proven to use this topic. */
62
63
  chatId?: string;
64
+ /** Stable provider binding; never contains SDK endpoint or attachment identity. */
65
+ telegramBinding?: TelegramTopicBinding;
66
+ /** Why this record is archive-fenced; absence means no remote archive is authorized. */
67
+ archiveReason?: "session_closed" | "notification_subscription_removed" | "create_compensation";
63
68
  /** Canonical endpoint tuple (URL + token) that currently holds the lease. */
64
69
  endpointKey?: string;
65
70
  /** Authenticated endpoint authority digest, excluding transport presentation. */
@@ -87,6 +92,8 @@ export interface TopicCreateClaim {
87
92
  leaseOwner?: string;
88
93
  authorityEpoch: number;
89
94
  createdAt: number;
95
+ telegramBinding?: TelegramTopicBinding;
96
+ /** Legacy v2 input only; never emitted by serialize(). */
90
97
  binding?: TopicEndpointBinding;
91
98
  }
92
99
 
@@ -114,8 +121,8 @@ export interface TopicRegistryState {
114
121
  topics: Record<string, TopicRecord>;
115
122
  /** Durable lifecycle epochs retained after an archive starts. */
116
123
  fences?: Record<string, number>;
117
- /** Closed transport endpoint leases; unchanged endpoint discovery remains fenced across restart. */
118
- closedEndpoints?: Record<string, TopicEndpointBinding>;
124
+ /** Closed durable Telegram bindings; values intentionally contain no SDK endpoint identity. */
125
+ closedEndpoints?: Record<string, TelegramTopicBinding>;
119
126
  /** Persistent host identity used to distinguish concurrent installations. */
120
127
  installationHostId?: string;
121
128
  /** Bounded durable archive work; no topic record is physically deleted. */
@@ -134,11 +141,22 @@ export interface TopicRegistryCasAuthority {
134
141
  compareAndSet(expectedGeneration: number, next: TopicRegistryState): Promise<boolean>;
135
142
  }
136
143
 
144
+ /** Durable Telegram authority. This is the only binding shape writers persist. */
145
+ export interface TelegramTopicBinding {
146
+ chatId: string;
147
+ transport: "telegram";
148
+ }
149
+
150
+ export type TopicArchiveReason = NonNullable<TopicRecord["archiveReason"]>;
151
+
137
152
  /** Authenticated runtime binding for a durable topic lease. */
138
153
  export interface TopicEndpointBinding {
139
154
  chatId: string;
140
- endpointKey: string;
141
- endpointDigest: string;
155
+ transport?: "telegram";
156
+ logicalSessionId?: string;
157
+ /** Legacy input-only fields. They are never authority or persistence fields. */
158
+ endpointKey?: string;
159
+ endpointDigest?: string;
142
160
  endpointGeneration?: number;
143
161
  }
144
162
 
@@ -159,6 +177,12 @@ export interface TopicArchiveAuthoritySnapshot {
159
177
  record?: TopicRecord;
160
178
  }
161
179
 
180
+ function isTopicArchiveReason(value: unknown): value is TopicArchiveReason {
181
+ return (
182
+ value === "session_closed" || value === "notification_subscription_removed" || value === "create_compensation"
183
+ );
184
+ }
185
+
162
186
  function isValidBindingString(value: unknown): value is string {
163
187
  return typeof value === "string" && value.trim().length > 0;
164
188
  }
@@ -168,6 +192,7 @@ function isValidBindingGeneration(value: unknown): value is number {
168
192
  }
169
193
  function hasAnyBinding(record: TopicRecord): boolean {
170
194
  return (
195
+ record.telegramBinding !== undefined ||
171
196
  record.chatId !== undefined ||
172
197
  record.endpointKey !== undefined ||
173
198
  record.endpointDigest !== undefined ||
@@ -176,22 +201,23 @@ function hasAnyBinding(record: TopicRecord): boolean {
176
201
  );
177
202
  }
178
203
 
179
- function hasCompleteBinding(record: TopicRecord): boolean {
204
+ function hasStableTelegramBinding(record: TopicRecord): boolean {
180
205
  return (
181
- isValidBindingString(record.chatId) &&
182
- isValidBindingString(record.endpointKey) &&
183
- isValidBindingString(record.endpointDigest) &&
184
- (record.endpointGeneration === undefined || isValidBindingGeneration(record.endpointGeneration)) &&
185
- (record.endpointIncarnation === undefined || isValidBindingGeneration(record.endpointIncarnation))
206
+ record.telegramBinding !== undefined &&
207
+ isValidBindingString(record.telegramBinding.chatId) &&
208
+ record.telegramBinding.transport === "telegram"
186
209
  );
187
210
  }
188
211
 
212
+ function hasCompleteBinding(record: TopicRecord): boolean {
213
+ return hasStableTelegramBinding(record);
214
+ }
215
+
189
216
  function hasValidBinding(binding: TopicEndpointBinding): boolean {
190
217
  return (
191
218
  isValidBindingString(binding.chatId) &&
192
- isValidBindingString(binding.endpointKey) &&
193
- isValidBindingString(binding.endpointDigest) &&
194
- (binding.endpointGeneration === undefined || isValidBindingGeneration(binding.endpointGeneration))
219
+ (binding.transport === undefined || binding.transport === "telegram") &&
220
+ (binding.logicalSessionId === undefined || isValidBindingString(binding.logicalSessionId))
195
221
  );
196
222
  }
197
223
 
@@ -207,15 +233,38 @@ function nextAuthorityEpoch(current: number): number {
207
233
  export function emptyTopicRegistryState(): TopicRegistryState {
208
234
  return { version: 2, topics: {} };
209
235
  }
236
+
237
+ /** Strip SDK endpoint authority from legacy v2 records without archiving them. */
238
+ function migrateLegacyTopicBinding(record: Record<string, unknown>): Record<string, unknown> {
239
+ const migrated = { ...record };
240
+ const chatId =
241
+ isObjectLike(migrated.telegramBinding) && isValidBindingString(migrated.telegramBinding.chatId)
242
+ ? migrated.telegramBinding.chatId
243
+ : migrated.chatId;
244
+ if (isValidBindingString(chatId)) {
245
+ migrated.telegramBinding = { chatId, transport: "telegram" };
246
+ delete migrated.endpointKey;
247
+ delete migrated.endpointDigest;
248
+ delete migrated.endpointGeneration;
249
+ delete migrated.endpointIncarnation;
250
+ }
251
+ return migrated;
252
+ }
253
+
254
+ function isObjectLike(value: unknown): value is Record<string, unknown> {
255
+ return !!value && typeof value === "object" && !Array.isArray(value);
256
+ }
210
257
  /**
211
258
  * Reject snapshots written by a newer daemon. Missing versions are preserved as
212
259
  * evidence but quarantined: legacy records must never route or mutate remotely.
213
260
  */
214
261
  export function parseTopicRegistryState(value: unknown): TopicRegistryState | undefined {
215
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
262
+ if (value === undefined) return undefined;
263
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("malformed Telegram topic state");
216
264
  let state = value as TopicRegistryState;
217
265
  if (state.version !== undefined && state.version !== 2) throw new Error("unsupported future Telegram topic state");
218
- if (!state.topics || typeof state.topics !== "object" || Array.isArray(state.topics)) return undefined;
266
+ if (!state.topics || typeof state.topics !== "object" || Array.isArray(state.topics))
267
+ throw new Error("malformed Telegram topic state");
219
268
  if (state.version === undefined) {
220
269
  if (state.registryGeneration !== undefined) throw new Error("malformed Telegram topic state");
221
270
  return parseTopicRegistryState({
@@ -228,6 +277,7 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
228
277
  record && typeof record === "object"
229
278
  ? {
230
279
  ...record,
280
+ ...migrateLegacyTopicBinding(record as unknown as Record<string, unknown>),
231
281
  topicOrigin: (record as TopicRecord).topicOrigin ?? "daemon_created",
232
282
  authorityState: "legacy_quarantined",
233
283
  }
@@ -258,7 +308,40 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
258
308
  : records,
259
309
  ]),
260
310
  ),
311
+ // Pre-#4401 writers persisted closed-endpoint bindings without the
312
+ // `transport` discriminator. Rejecting them would permanently brick an
313
+ // otherwise-healthy shared registry (the same class as the
314
+ // disconnectGraceExpiresAt artifact below): the CAS authority refuses
315
+ // every read and write forever. The transport can only be "telegram" in
316
+ // those snapshots, so heal the in-memory record; the serializer no
317
+ // longer persists closed endpoints at all. Only heal records that match
318
+ // the complete pre-#4401 shape (chatId + endpointKey + endpointDigest +
319
+ // endpointGeneration): partial records are corruption, not legacy data.
320
+ closedEndpoints:
321
+ state.closedEndpoints === undefined
322
+ ? undefined
323
+ : isObject(state.closedEndpoints)
324
+ ? Object.fromEntries(
325
+ Object.entries(state.closedEndpoints).map(([sessionId, binding]) => [
326
+ sessionId,
327
+ isObject(binding) &&
328
+ binding.transport === undefined &&
329
+ isValidBindingString(binding.chatId) &&
330
+ isValidBindingString(binding.endpointKey) &&
331
+ isValidBindingString(binding.endpointDigest) &&
332
+ isValidBindingGeneration(binding.endpointGeneration)
333
+ ? ({ chatId: binding.chatId, transport: "telegram" } as TelegramTopicBinding)
334
+ : binding,
335
+ ]),
336
+ )
337
+ : state.closedEndpoints,
261
338
  };
339
+ state.topics = Object.fromEntries(
340
+ Object.entries(state.topics).map(([sessionId, record]) => [
341
+ sessionId,
342
+ isObject(record) ? (migrateLegacyTopicBinding(record) as unknown as TopicRecord) : record,
343
+ ]),
344
+ );
262
345
  const validTimestamp = (candidate: unknown): candidate is number =>
263
346
  typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0;
264
347
  const validEpoch = (candidate: unknown): candidate is number =>
@@ -268,12 +351,13 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
268
351
  const validBinding = (candidate: unknown): candidate is TopicEndpointBinding =>
269
352
  isObject(candidate) &&
270
353
  isValidBindingString(candidate.chatId) &&
271
- isValidBindingString(candidate.endpointKey) &&
272
- isValidBindingString(candidate.endpointDigest) &&
273
- (candidate.endpointGeneration === undefined || isValidBindingGeneration(candidate.endpointGeneration));
354
+ (candidate.transport === undefined || candidate.transport === "telegram") &&
355
+ (candidate.logicalSessionId === undefined || isValidBindingString(candidate.logicalSessionId));
274
356
  const malformed = (): never => {
275
357
  throw new Error("malformed Telegram topic state");
276
358
  };
359
+ const validTelegramBinding = (candidate: unknown): candidate is TelegramTopicBinding =>
360
+ isObject(candidate) && isValidBindingString(candidate.chatId) && candidate.transport === "telegram";
277
361
 
278
362
  if (
279
363
  (state.registryGeneration !== undefined && !validEpoch(state.registryGeneration)) ||
@@ -286,6 +370,13 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
286
370
  for (const [sessionId, raw] of Object.entries(state.topics)) {
287
371
  if (!isValidBindingString(sessionId) || !isObject(raw) || !isValidTopicId(raw.topicId)) malformed();
288
372
  if (typeof raw.identitySent !== "boolean" || !validTimestamp(raw.createdAt)) malformed();
373
+ if (
374
+ raw.telegramBinding !== undefined &&
375
+ (!isObject(raw.telegramBinding) ||
376
+ !isValidBindingString(raw.telegramBinding.chatId) ||
377
+ raw.telegramBinding.transport !== "telegram")
378
+ )
379
+ malformed();
289
380
  if (
290
381
  !validOptionalString(raw.sessionUuid) ||
291
382
  (raw.orphanedAt !== undefined && !validTimestamp(raw.orphanedAt)) ||
@@ -332,8 +423,13 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
332
423
  raw.authorityState !== "inactive"
333
424
  )
334
425
  malformed();
335
- // Older archive transitions retained the expired disconnect-grace marker,
336
- // making the entire shared registry unreadable on the next daemon start.
426
+ // A grace deadline on a non-grace record is a known artifact of the
427
+ // v0.12.12–v0.12.17 archive writers, which moved disconnect_grace records
428
+ // to archive_pending without clearing disconnectGraceExpiresAt (fixed by
429
+ // #4054). Rejecting it here permanently bricked otherwise-healthy shared
430
+ // registries: the CAS authority refused every read and write forever.
431
+ // The field is inert outside disconnect_grace, so drop it here to heal
432
+ // the in-memory state; the next serialize no longer persists it.
337
433
  delete raw.disconnectGraceExpiresAt;
338
434
  }
339
435
  const hasBinding = hasAnyBinding(raw);
@@ -346,15 +442,12 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
346
442
  raw.endpointDigest === undefined &&
347
443
  raw.endpointGeneration === undefined &&
348
444
  raw.endpointIncarnation === undefined;
349
- if (
350
- hasBinding &&
351
- !hasArchiveOnlyChatIdentity &&
352
- (!isValidBindingString(raw.chatId) ||
353
- !isValidBindingString(raw.endpointKey) ||
354
- !isValidBindingString(raw.endpointDigest) ||
355
- (raw.endpointGeneration !== undefined && !isValidBindingGeneration(raw.endpointGeneration)) ||
356
- (raw.endpointIncarnation !== undefined && !isValidBindingGeneration(raw.endpointIncarnation)))
357
- )
445
+ const hasStableTelegramBinding =
446
+ raw.telegramBinding !== undefined &&
447
+ isObjectLike(raw.telegramBinding) &&
448
+ isValidBindingString(raw.telegramBinding.chatId) &&
449
+ raw.telegramBinding.transport === "telegram";
450
+ if (hasBinding && !hasStableTelegramBinding && !hasArchiveOnlyChatIdentity && !isValidBindingString(raw.chatId))
358
451
  malformed();
359
452
  }
360
453
  for (const [sessionId, records] of Object.entries(state.retiredTopics ?? {})) {
@@ -380,7 +473,11 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
380
473
  !validTimestamp(claim.createdAt) ||
381
474
  !validOptionalString(claim.hostId) ||
382
475
  !validOptionalString(claim.leaseOwner) ||
383
- (claim.binding !== undefined && !validBinding(claim.binding))
476
+ (claim.binding !== undefined && !validBinding(claim.binding)) ||
477
+ (claim.telegramBinding !== undefined &&
478
+ (!isObject(claim.telegramBinding) ||
479
+ !isValidBindingString(claim.telegramBinding.chatId) ||
480
+ claim.telegramBinding.transport !== "telegram"))
384
481
  )
385
482
  malformed();
386
483
  }
@@ -400,7 +497,7 @@ export function parseTopicRegistryState(value: unknown): TopicRegistryState | un
400
497
  malformed();
401
498
  }
402
499
  for (const [sessionId, binding] of Object.entries(state.closedEndpoints ?? {}))
403
- if (!isValidBindingString(sessionId) || !validBinding(binding)) malformed();
500
+ if (!isValidBindingString(sessionId) || !validTelegramBinding(binding)) malformed();
404
501
  return state;
405
502
  }
406
503
 
@@ -457,7 +554,7 @@ export class TopicRegistry {
457
554
  if (Array.isArray(records))
458
555
  this.retiredTopics.set(
459
556
  sessionId,
460
- records.map(record => ({ ...record })),
557
+ records.map(record => this.#normalizeLoadedRecord(record)),
461
558
  );
462
559
  for (const [sessionId, job] of Object.entries(state.archiveJobs ?? {})) {
463
560
  const attempt =
@@ -513,6 +610,9 @@ export class TopicRegistry {
513
610
  ...(isValidBindingString(claim.hostId) ? { hostId: claim.hostId } : {}),
514
611
  ...(isValidBindingString(claim.leaseOwner) ? { leaseOwner: claim.leaseOwner } : {}),
515
612
  ...(claim.binding ? { binding: claim.binding } : {}),
613
+ ...(isObjectLike(claim.telegramBinding) && isValidBindingString(claim.telegramBinding.chatId)
614
+ ? { telegramBinding: { chatId: claim.telegramBinding.chatId, transport: "telegram" } }
615
+ : {}),
516
616
  });
517
617
  }
518
618
 
@@ -554,6 +654,9 @@ export class TopicRegistry {
554
654
  typeof raw.sessionUuid === "string" && raw.sessionUuid.length > 0 ? raw.sessionUuid : randomUUID(),
555
655
  identitySent: raw.identitySent === true,
556
656
  createdAt: typeof raw.createdAt === "number" ? raw.createdAt : 0,
657
+ ...(isObjectLike(raw.telegramBinding) && isValidBindingString(raw.telegramBinding.chatId)
658
+ ? { telegramBinding: { chatId: raw.telegramBinding.chatId, transport: "telegram" as const } }
659
+ : {}),
557
660
  ...(typeof raw.name === "string" ? { name: raw.name } : {}),
558
661
  ...(typeof raw.orphanedAt === "number" && Number.isFinite(raw.orphanedAt) && raw.orphanedAt >= 0
559
662
  ? { orphanedAt: raw.orphanedAt }
@@ -564,6 +667,11 @@ export class TopicRegistry {
564
667
  ? { userNameUpdateId: raw.userNameUpdateId }
565
668
  : {}),
566
669
  ...(typeof raw.identityKey === "string" ? { identityKey: raw.identityKey } : {}),
670
+ ...(raw.archiveReason === "session_closed" ||
671
+ raw.archiveReason === "notification_subscription_removed" ||
672
+ raw.archiveReason === "create_compensation"
673
+ ? { archiveReason: raw.archiveReason }
674
+ : {}),
567
675
  ...(typeof raw.creationLeaseEpoch === "number" &&
568
676
  Number.isSafeInteger(raw.creationLeaseEpoch) &&
569
677
  raw.creationLeaseEpoch >= 0
@@ -592,12 +700,6 @@ export class TopicRegistry {
592
700
  }
593
701
  : { authorityState: "active" as const }),
594
702
  ...(isValidBindingString(raw.chatId) ? { chatId: raw.chatId } : {}),
595
- ...(isValidBindingString(raw.endpointKey) ? { endpointKey: raw.endpointKey } : {}),
596
- ...(isValidBindingString(raw.endpointDigest) ? { endpointDigest: raw.endpointDigest } : {}),
597
- ...(isValidBindingGeneration(raw.endpointGeneration) ? { endpointGeneration: raw.endpointGeneration } : {}),
598
- ...(isValidBindingGeneration(raw.endpointIncarnation)
599
- ? { endpointIncarnation: raw.endpointIncarnation }
600
- : {}),
601
703
  ...(isValidBindingString(raw.leaseOwner) ? { leaseOwner: raw.leaseOwner } : {}),
602
704
  ...(typeof raw.leaseHeartbeatAt === "number" && Number.isFinite(raw.leaseHeartbeatAt)
603
705
  ? { leaseHeartbeatAt: raw.leaseHeartbeatAt }
@@ -615,16 +717,37 @@ export class TopicRegistry {
615
717
  : {}),
616
718
  ...(bindingMalformed ? { bindingMalformed: true as const } : {}),
617
719
  };
618
- this.epochs.set(sessionId, Math.max(fenceEpoch, record.authorityEpoch ?? 0));
720
+ const normalizedRecord = this.#normalizeLoadedRecord(record);
721
+ this.epochs.set(sessionId, Math.max(fenceEpoch, normalizedRecord.authorityEpoch ?? 0));
619
722
  // Pre-generation-17 records have no endpoint authority. Retire them locally:
620
723
  // their unknown remote topic must neither be rebound nor deleted cross-chat.
621
724
  if (legacyUnbound) continue;
622
725
 
623
- this.topics.set(sessionId, record);
726
+ this.topics.set(sessionId, normalizedRecord);
624
727
  }
625
728
  this.rebuildInboundRoutes();
626
729
  }
627
730
 
731
+ /** Keep durable topic authority at stable logical session + Telegram binding. */
732
+ #normalizeLoadedRecord(raw: TopicRecord): TopicRecord {
733
+ const normalized = { ...raw };
734
+ const chatId =
735
+ isObjectLike(raw.telegramBinding) && isValidBindingString(raw.telegramBinding.chatId)
736
+ ? raw.telegramBinding.chatId
737
+ : isValidBindingString(raw.chatId)
738
+ ? raw.chatId
739
+ : undefined;
740
+ if (chatId !== undefined) {
741
+ normalized.chatId = chatId;
742
+ normalized.telegramBinding = { chatId, transport: "telegram" };
743
+ }
744
+ delete normalized.endpointKey;
745
+ delete normalized.endpointDigest;
746
+ delete normalized.endpointGeneration;
747
+ delete normalized.endpointIncarnation;
748
+ return normalized;
749
+ }
750
+
628
751
  private rebuildInboundRoutes(): void {
629
752
  this.byTopic.clear();
630
753
  this.#ambiguousTopicIds.clear();
@@ -680,12 +803,7 @@ export class TopicRegistry {
680
803
  if (topic.authorityEpoch !== claim.authorityEpoch) return false;
681
804
  if (
682
805
  claim.binding &&
683
- (!hasCompleteBinding(topic) ||
684
- !hasValidBinding(claim.binding) ||
685
- topic.chatId !== claim.binding.chatId ||
686
- topic.endpointKey !== claim.binding.endpointKey ||
687
- topic.endpointDigest !== claim.binding.endpointDigest ||
688
- topic.endpointGeneration !== claim.binding.endpointGeneration)
806
+ (!hasCompleteBinding(topic) || !hasValidBinding(claim.binding) || topic.chatId !== claim.binding.chatId)
689
807
  )
690
808
  return false;
691
809
  this.topics.set(sessionId, { ...topic });
@@ -735,31 +853,29 @@ export class TopicRegistry {
735
853
  */
736
854
  endpointAuthority(binding: TopicEndpointBinding, excludedTransientClaimant?: object): TopicEndpointAuthority {
737
855
  if (!hasValidBinding(binding)) return { state: "ambiguous" };
738
- const canClaim = (record: Pick<TopicRecord, "chatId" | "endpointKey" | "endpointDigest">): boolean =>
856
+ const canClaim = (sessionId: string, record: Pick<TopicRecord, "chatId">): boolean =>
739
857
  (record.chatId === undefined || record.chatId === binding.chatId) &&
740
- (record.endpointKey === undefined || record.endpointKey === binding.endpointKey) &&
741
- (record.endpointDigest === undefined || record.endpointDigest === binding.endpointDigest);
742
- const committed = [...this.topics].filter(([, record]) => canClaim(record));
858
+ (binding.logicalSessionId === undefined || sessionId === binding.logicalSessionId);
859
+ const committed = [...this.topics].filter(([sessionId, record]) => canClaim(sessionId, record));
743
860
  const excludesClaimant = (sessionId: string): boolean =>
744
861
  excludedTransientClaimant !== undefined &&
745
862
  this.transientClaimants.get(sessionId) === excludedTransientClaimant;
746
- const staged = [...this.staged].filter(([sessionId, record]) => canClaim(record) && !excludesClaimant(sessionId));
863
+ const staged = [...this.staged].filter(
864
+ ([sessionId, record]) => canClaim(sessionId, record) && !excludesClaimant(sessionId),
865
+ );
747
866
  const creating = [...this.creatingBindings].filter(
748
- ([sessionId, claim]) => canClaim(claim) && !excludesClaimant(sessionId),
867
+ ([sessionId, claim]) => canClaim(sessionId, claim) && !excludesClaimant(sessionId),
749
868
  );
750
869
  const durableClaims = [...this.createClaims].filter(
751
- ([sessionId, claim]) => claim.binding !== undefined && canClaim(claim.binding) && !excludesClaimant(sessionId),
870
+ ([sessionId, claim]) =>
871
+ claim.binding !== undefined && canClaim(sessionId, claim.binding) && !excludesClaimant(sessionId),
752
872
  );
753
873
  if (committed.length === 0 && staged.length === 0 && creating.length === 0 && durableClaims.length === 0)
754
874
  return { state: "none" };
755
875
  if (committed.length !== 1 || staged.length !== 0 || creating.length !== 0 || durableClaims.length !== 0)
756
876
  return { state: "ambiguous" };
757
877
  const [sessionId, record] = committed[0]!;
758
- return record.chatId === binding.chatId &&
759
- record.endpointKey === binding.endpointKey &&
760
- record.endpointDigest === binding.endpointDigest &&
761
- this.isActiveUnambiguous(sessionId) &&
762
- !record.bindingMalformed
878
+ return record.chatId === binding.chatId && this.isActiveUnambiguous(sessionId) && !record.bindingMalformed
763
879
  ? { state: "unique", sessionId }
764
880
  : { state: "ambiguous" };
765
881
  }
@@ -773,13 +889,7 @@ export class TopicRegistry {
773
889
  /** Whether this exact session owns the complete durable endpoint binding. */
774
890
  matchesEndpoint(sessionId: string, binding: TopicEndpointBinding): boolean {
775
891
  const record = this.topics.get(sessionId);
776
- return (
777
- this.isActiveUnambiguous(sessionId) &&
778
- !record?.bindingMalformed &&
779
- record?.chatId === binding.chatId &&
780
- record.endpointKey === binding.endpointKey &&
781
- record.endpointDigest === binding.endpointDigest
782
- );
892
+ return this.isActiveUnambiguous(sessionId) && !record?.bindingMalformed && record?.chatId === binding.chatId;
783
893
  }
784
894
 
785
895
  /**
@@ -794,7 +904,7 @@ export class TopicRegistry {
794
904
  record.bindingMalformed ||
795
905
  !hasCompleteBinding(record) ||
796
906
  !hasValidBinding(binding) ||
797
- (record.endpointKey === binding.endpointKey && record.endpointDigest === binding.endpointDigest)
907
+ false
798
908
  )
799
909
  return false;
800
910
  const history = this.retiredTopics.get(sessionId) ?? [];
@@ -816,8 +926,8 @@ export class TopicRegistry {
816
926
  bindEndpoint(
817
927
  sessionId: string,
818
928
  binding: TopicEndpointBinding,
819
- activeEndpointKeys: ReadonlySet<string> = new Set(),
820
- allowEndpointRotation = false,
929
+ _activeEndpointKeys: ReadonlySet<string> = new Set(),
930
+ _allowEndpointRotation = false,
821
931
  ): "bound" | "unchanged" | "rejected" {
822
932
  const record = this.topics.get(sessionId);
823
933
  if (
@@ -833,33 +943,10 @@ export class TopicRegistry {
833
943
  // authenticated its logical session id.
834
944
  if (record.chatId === undefined || record.chatId !== binding.chatId) return "rejected";
835
945
 
836
- const sameEndpoint =
837
- record.endpointKey === binding.endpointKey && record.endpointDigest === binding.endpointDigest;
838
- if (
839
- sameEndpoint &&
840
- record.endpointGeneration !== undefined &&
841
- binding.endpointGeneration !== undefined &&
842
- binding.endpointGeneration < record.endpointGeneration
843
- )
844
- return "rejected";
845
- if (
846
- !sameEndpoint &&
847
- hasAnyBinding(record) &&
848
- (!allowEndpointRotation || (record.endpointKey !== undefined && activeEndpointKeys.has(record.endpointKey)))
849
- )
850
- return "rejected";
851
-
852
- const changed =
853
- record.chatId !== binding.chatId ||
854
- record.endpointKey !== binding.endpointKey ||
855
- record.endpointDigest !== binding.endpointDigest ||
856
- record.endpointGeneration !== binding.endpointGeneration;
946
+ const changed = false;
857
947
  if (!changed && record.authorityState === "active") return "unchanged";
858
948
  record.chatId = binding.chatId;
859
- record.endpointKey = binding.endpointKey;
860
- record.endpointDigest = binding.endpointDigest;
861
- record.endpointGeneration = binding.endpointGeneration;
862
- if (!sameEndpoint) record.endpointIncarnation = (record.endpointIncarnation ?? 0) + 1;
949
+ record.telegramBinding = { chatId: binding.chatId, transport: "telegram" };
863
950
  if (record.authorityState === "disconnect_grace") {
864
951
  record.authorityState = "active";
865
952
  delete record.orphanedAt;
@@ -873,27 +960,13 @@ export class TopicRegistry {
873
960
  restoreEndpointBinding(
874
961
  sessionId: string,
875
962
  expected: TopicEndpointBinding,
876
- previous: Pick<
877
- TopicRecord,
878
- "chatId" | "endpointKey" | "endpointDigest" | "endpointGeneration" | "endpointIncarnation"
879
- >,
963
+ previous: Pick<TopicRecord, "chatId">,
880
964
  ): boolean {
881
965
  const record = this.topics.get(sessionId);
882
- if (
883
- !record ||
884
- record.chatId !== expected.chatId ||
885
- record.endpointKey !== expected.endpointKey ||
886
- record.endpointDigest !== expected.endpointDigest ||
887
- record.endpointGeneration !== expected.endpointGeneration
888
- )
889
- return false;
966
+ if (!record || record.chatId !== expected.chatId) return false;
890
967
  record.chatId = previous.chatId;
891
- record.endpointKey = previous.endpointKey;
892
- record.endpointDigest = previous.endpointDigest;
893
- if (previous.endpointGeneration === undefined) delete record.endpointGeneration;
894
- else record.endpointGeneration = previous.endpointGeneration;
895
- if (previous.endpointIncarnation === undefined) delete record.endpointIncarnation;
896
- else record.endpointIncarnation = previous.endpointIncarnation;
968
+ if (previous.chatId === undefined) delete record.telegramBinding;
969
+ else record.telegramBinding = { chatId: previous.chatId, transport: "telegram" };
897
970
  return true;
898
971
  }
899
972
 
@@ -950,15 +1023,11 @@ export class TopicRegistry {
950
1023
  authorityEpoch: revoked ? (this.epochs.get(sessionId) ?? 0) : epoch,
951
1024
  creationLeaseEpoch: epoch,
952
1025
  authorityState: revoked ? "archive_pending" : "active",
1026
+ ...(revoked ? { archiveReason: "create_compensation" as const } : {}),
953
1027
  ...(binding
954
1028
  ? {
955
1029
  chatId: binding.chatId,
956
- endpointKey: binding.endpointKey,
957
- endpointDigest: binding.endpointDigest,
958
- endpointIncarnation: 0,
959
- ...(binding.endpointGeneration === undefined
960
- ? {}
961
- : { endpointGeneration: binding.endpointGeneration }),
1030
+ telegramBinding: { chatId: binding.chatId, transport: "telegram" },
962
1031
  }
963
1032
  : {}),
964
1033
  };
@@ -978,6 +1047,7 @@ export class TopicRegistry {
978
1047
  if ((this.epochs.get(sessionId) ?? 0) !== epoch) {
979
1048
  record.authorityEpoch = this.epochs.get(sessionId) ?? 0;
980
1049
  record.authorityState = "archive_pending";
1050
+ record.archiveReason = "create_compensation";
981
1051
  this.topics.set(sessionId, record);
982
1052
  throw new Error("topic authority was revoked during creation");
983
1053
  }
@@ -1236,6 +1306,7 @@ export class TopicRegistry {
1236
1306
  ...snapshot.record,
1237
1307
  authorityEpoch: deleteEpoch,
1238
1308
  authorityState: "archive_pending",
1309
+ archiveReason: snapshot.record.archiveReason ?? "create_compensation",
1239
1310
  });
1240
1311
  const restored = this.topics.get(snapshot.sessionId);
1241
1312
  if (restored) delete restored.disconnectGraceExpiresAt;
@@ -1244,6 +1315,7 @@ export class TopicRegistry {
1244
1315
  } else {
1245
1316
  record.authorityEpoch = deleteEpoch;
1246
1317
  record.authorityState = "archive_pending";
1318
+ record.archiveReason = snapshot.record?.archiveReason ?? record.archiveReason ?? "create_compensation";
1247
1319
  delete record.disconnectGraceExpiresAt;
1248
1320
  if (this.byTopic.get(record.topicId) === snapshot.sessionId) this.byTopic.delete(record.topicId);
1249
1321
  }
@@ -1252,7 +1324,13 @@ export class TopicRegistry {
1252
1324
  }
1253
1325
 
1254
1326
  /** Fence new work before the remote archive starts, including an absent in-flight create. */
1255
- beginArchive(sessionId: string, hostId?: string, now = Date.now()): TopicRecord | undefined {
1327
+ beginArchive(
1328
+ sessionId: string,
1329
+ hostId: string | undefined,
1330
+ now: number,
1331
+ reason: TopicArchiveReason,
1332
+ ): TopicRecord | undefined {
1333
+ if (!isTopicArchiveReason(reason)) return undefined;
1256
1334
  const record = this.topics.get(sessionId);
1257
1335
  if (
1258
1336
  record?.topicOrigin === "user_created" ||
@@ -1278,6 +1356,7 @@ export class TopicRegistry {
1278
1356
  if (!record) return undefined;
1279
1357
  record.authorityEpoch = epoch;
1280
1358
  record.authorityState = "archive_pending";
1359
+ record.archiveReason = reason;
1281
1360
  delete record.disconnectGraceExpiresAt;
1282
1361
  if (hostId) record.archiveHostId = hostId;
1283
1362
  record.archiveLeaseEpoch = epoch;
@@ -1318,7 +1397,9 @@ export class TopicRegistry {
1318
1397
  binding?: TopicEndpointBinding,
1319
1398
  topicOrigin?: TopicRecord["topicOrigin"],
1320
1399
  archiveChatId?: string,
1400
+ reason: TopicArchiveReason = "create_compensation",
1321
1401
  ): TopicRecord {
1402
+ if (!isTopicArchiveReason(reason)) throw new Error("archive reason is required");
1322
1403
  const epoch = Math.max(this.epochs.get(sessionId) ?? 0, this.topics.get(sessionId)?.authorityEpoch ?? 0);
1323
1404
  const record: TopicRecord = {
1324
1405
  topicId,
@@ -1329,15 +1410,11 @@ export class TopicRegistry {
1329
1410
  createdAt: now(),
1330
1411
  authorityEpoch: epoch,
1331
1412
  authorityState: "archive_pending",
1413
+ archiveReason: reason,
1332
1414
  ...(binding
1333
1415
  ? {
1334
1416
  chatId: binding.chatId,
1335
- endpointKey: binding.endpointKey,
1336
- endpointDigest: binding.endpointDigest,
1337
- endpointIncarnation: 0,
1338
- ...(binding.endpointGeneration === undefined
1339
- ? {}
1340
- : { endpointGeneration: binding.endpointGeneration }),
1417
+ telegramBinding: { chatId: binding.chatId, transport: "telegram" },
1341
1418
  }
1342
1419
  : archiveChatId
1343
1420
  ? { chatId: archiveChatId }
@@ -1360,22 +1437,28 @@ export class TopicRegistry {
1360
1437
  binding?: TopicEndpointBinding,
1361
1438
  topicOrigin?: TopicRecord["topicOrigin"],
1362
1439
  archiveChatId?: string,
1440
+ reason: TopicArchiveReason = "create_compensation",
1363
1441
  ): TopicRecord | undefined {
1364
1442
  const record = this.topics.get(sessionId);
1365
- const matchesBinding =
1366
- record?.chatId === binding?.chatId &&
1367
- record?.endpointKey === binding?.endpointKey &&
1368
- record?.endpointDigest === binding?.endpointDigest &&
1369
- record?.endpointGeneration === binding?.endpointGeneration;
1443
+ const matchesBinding = record?.chatId === binding?.chatId;
1370
1444
  if (
1371
1445
  record
1372
1446
  ? record.topicId !== topicId || record.creationLeaseEpoch !== creationLeaseEpoch || !matchesBinding
1373
1447
  : (this.epochs.get(sessionId) ?? 0) !== creationLeaseEpoch
1374
1448
  )
1375
1449
  return undefined;
1376
- const archiveFence = this.beginArchive(sessionId, hostId, now());
1450
+ const archiveFence = this.beginArchive(sessionId, hostId, now(), reason);
1377
1451
  if (record && !archiveFence) return undefined;
1378
- const fenced = this.fenceAcceptedCreate(sessionId, topicId, now, name, binding, topicOrigin, archiveChatId);
1452
+ const fenced = this.fenceAcceptedCreate(
1453
+ sessionId,
1454
+ topicId,
1455
+ now,
1456
+ name,
1457
+ binding,
1458
+ topicOrigin,
1459
+ archiveChatId,
1460
+ reason,
1461
+ );
1379
1462
  fenced.creationLeaseEpoch = creationLeaseEpoch;
1380
1463
  fenced.archiveHostId = hostId;
1381
1464
  fenced.archiveLeaseEpoch = fenced.authorityEpoch;
@@ -1391,7 +1474,12 @@ export class TopicRegistry {
1391
1474
  * Retain a topic record after a definite remote archive only while the exact
1392
1475
  * dispatched authority epoch is still current.
1393
1476
  */
1394
- settleArchive(sessionId: string, topicId: string, dispatchedAuthorityEpoch: number): boolean {
1477
+ settleArchive(
1478
+ sessionId: string,
1479
+ topicId: string,
1480
+ dispatchedAuthorityEpoch: number,
1481
+ reason: TopicArchiveReason,
1482
+ ): boolean {
1395
1483
  if (
1396
1484
  !Number.isSafeInteger(dispatchedAuthorityEpoch) ||
1397
1485
  dispatchedAuthorityEpoch < 0 ||
@@ -1401,6 +1489,8 @@ export class TopicRegistry {
1401
1489
  const record = this.topics.get(sessionId);
1402
1490
  if (
1403
1491
  !record ||
1492
+ !isTopicArchiveReason(reason) ||
1493
+ record.archiveReason !== reason ||
1404
1494
  record.topicId !== topicId ||
1405
1495
  record.authorityState !== "archive_pending" ||
1406
1496
  record.authorityEpoch !== dispatchedAuthorityEpoch ||
@@ -1417,7 +1507,7 @@ export class TopicRegistry {
1417
1507
  /** Durable archive jobs that are eligible for a retry at `now`. */
1418
1508
  archivePendingSessionIds(now = Date.now()): string[] {
1419
1509
  return [...this.topics].flatMap(([sessionId, record]) => {
1420
- if (record.authorityState === "archive_exhausted") {
1510
+ if (record.authorityState === "archive_exhausted" && record.topicOrigin === "daemon_created") {
1421
1511
  const currentEpoch = Math.max(this.epochs.get(sessionId) ?? 0, record.authorityEpoch ?? 0);
1422
1512
  if (currentEpoch < Number.MAX_SAFE_INTEGER) {
1423
1513
  const epoch = Math.min(currentEpoch + 1, Number.MAX_SAFE_INTEGER - 1);
@@ -1427,7 +1517,8 @@ export class TopicRegistry {
1427
1517
  delete record.disconnectGraceExpiresAt;
1428
1518
  }
1429
1519
  }
1430
- return (record.authorityState === "archive_pending" || record.authorityState === "archive_exhausted") &&
1520
+ return isTopicArchiveReason(record.archiveReason) &&
1521
+ (record.authorityState === "archive_pending" || record.authorityState === "archive_exhausted") &&
1431
1522
  (this.archiveJobs.get(sessionId)?.nextAttemptAt ?? 0) <= now
1432
1523
  ? [sessionId]
1433
1524
  : [];
@@ -1441,16 +1532,29 @@ export class TopicRegistry {
1441
1532
  }
1442
1533
 
1443
1534
  /** Persist an indefinitely discoverable retry after an ambiguous archive result. */
1444
- scheduleArchiveRetry(sessionId: string, now: number, diagnostic?: string): ArchiveJob | undefined {
1535
+ scheduleArchiveRetry(
1536
+ sessionId: string,
1537
+ now: number,
1538
+ reason: TopicArchiveReason,
1539
+ diagnostic?: string,
1540
+ ): ArchiveJob | undefined {
1445
1541
  const record = this.topics.get(sessionId);
1446
- if (record?.authorityState !== "archive_pending" && record?.authorityState !== "archive_exhausted")
1542
+ if (
1543
+ !record ||
1544
+ !isTopicArchiveReason(reason) ||
1545
+ record.archiveReason !== reason ||
1546
+ (record.authorityState !== "archive_pending" && record.authorityState !== "archive_exhausted")
1547
+ )
1447
1548
  return undefined;
1448
1549
  const previous = this.archiveJobs.get(sessionId);
1449
1550
  const firstAttemptAt = previous?.firstAttemptAt ?? now;
1450
1551
  const attempt = (previous?.attempt ?? 0) + 1;
1451
1552
  const exhausted = attempt > 8 || now - firstAttemptAt > 24 * 60 * 60 * 1000;
1452
1553
  const effectiveAttempt = Math.min(attempt, 8);
1453
- const backoffMs = exhausted ? 1_000 : Math.min(60_000, 250 * 2 ** effectiveAttempt);
1554
+ // Post-budget retries stay discoverable but are paced: a permanently
1555
+ // ambiguous archive drained by a periodic sweep must not redispatch (and
1556
+ // bump the authority epoch) every sweep tick forever.
1557
+ const backoffMs = exhausted ? 15 * 60_000 : Math.min(60_000, 250 * 2 ** effectiveAttempt);
1454
1558
  const job = {
1455
1559
  sessionId,
1456
1560
  topicId: record.topicId,
@@ -1469,14 +1573,43 @@ export class TopicRegistry {
1469
1573
 
1470
1574
  /** Serialise active records plus unpublished staged creates for atomic commit. */
1471
1575
  serialize(): TopicRegistryState {
1576
+ const stripSdkAuthority = (record: TopicRecord): TopicRecord => {
1577
+ const next = { ...record };
1578
+ if (next.chatId) next.telegramBinding = { chatId: next.chatId, transport: "telegram" };
1579
+ if (!next.chatId && next.telegramBinding) next.chatId = next.telegramBinding.chatId;
1580
+ delete next.endpointKey;
1581
+ delete next.endpointDigest;
1582
+ delete next.endpointGeneration;
1583
+ delete next.endpointIncarnation;
1584
+ return next;
1585
+ };
1586
+ const topics = Object.fromEntries(
1587
+ [...this.topics, ...this.staged].map(([id, record]) => [id, stripSdkAuthority(record)]),
1588
+ );
1589
+ const claims = Object.fromEntries(
1590
+ [...this.createClaims].map(([id, claim]) => [
1591
+ id,
1592
+ {
1593
+ ...claim,
1594
+ ...(claim.binding
1595
+ ? { telegramBinding: { chatId: claim.binding.chatId, transport: "telegram" as const } }
1596
+ : {}),
1597
+ binding: undefined,
1598
+ },
1599
+ ]),
1600
+ );
1601
+ const closedEndpoints: Record<string, TelegramTopicBinding> = {};
1472
1602
  return {
1473
1603
  version: 2,
1474
1604
  registryGeneration: this.registryGeneration,
1475
- topics: Object.fromEntries([...this.topics, ...this.staged]),
1605
+ topics,
1476
1606
  fences: Object.fromEntries(this.epochs),
1607
+ closedEndpoints,
1477
1608
  archiveJobs: Object.fromEntries(this.archiveJobs),
1478
- createClaims: Object.fromEntries(this.createClaims),
1479
- retiredTopics: Object.fromEntries(this.retiredTopics),
1609
+ createClaims: claims,
1610
+ retiredTopics: Object.fromEntries(
1611
+ [...this.retiredTopics].map(([id, records]) => [id, records.map(stripSdkAuthority)]),
1612
+ ),
1480
1613
  };
1481
1614
  }
1482
1615
  }