@gajae-code/coding-agent 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (599) hide show
  1. package/CHANGELOG.md +184 -2
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +1 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,49 +1,27 @@
1
1
  /**
2
- * Durable, non-routable sidecar store for "adopt a user-created Telegram forum
3
- * topic into a preallocated session" intents.
2
+ * Durable provider-local reservations for Telegram topic adoption.
4
3
  *
5
- * When a user creates a new forum topic, the daemon first records durable
6
- * pending-topic authorization. After folder selection it preallocates an
7
- * `intendedSessionId` and records an adoption intent here *before* spawning.
8
- * Later, the authenticated `ensureTopic` create callback reads that intent and
9
- * returns the user's existing topicId to `TopicRegistry.getOrCreateTopic`, which
10
- * is the only authority that commits a topic binding. This keeps the intent out
11
- * of routing/endpoint authority until commit, so a failed spawn or a late socket
12
- * never deletes the user's topic and never strands a duplicate.
13
- *
14
- * Invariants (see `pending-approval.md`):
15
- * - One file per intent, named `<intendedSessionId>.adoption-intent.json` under
16
- * the daemon notifications dir. Sweep touches files only; never the Telegram API.
17
- * - Only `{ intendedSessionId, topicId, chatId, target, createdAt, expiresAt }`
18
- * is persisted. Bot/control tokens and endpoint digests are never stored.
19
- * - Owner-only dir (0o700) and 0o600 files; atomic temp→fsync→chmod→rename→parent sync.
20
- * - `tryClaim` is a synchronous, in-memory claim keyed by topicId; it excludes a
21
- * second `intendedSessionId` from winning the same topic across awaits. It
22
- * does not delete the sidecar; `releaseClaim` re-enables retry on commit failure.
4
+ * A reservation is keyed by the stable provider request identity, never by a
5
+ * preallocated Broker SessionId. Broker allocates the SessionId; the daemon
6
+ * CAS-binds that opaque value only after the lifecycle service returns it.
7
+ * This store owns only presentation mappings and pending-topic authorization.
23
8
  */
24
9
 
25
10
  import { randomUUID } from "node:crypto";
26
11
  import * as fs from "node:fs";
27
12
  import * as path from "node:path";
13
+ import { logger } from "@gajae-code/utils";
28
14
  import { daemonPaths } from "./daemon-paths";
29
15
  import type { SessionCreateTarget } from "./index";
30
16
 
31
- /** Persisted document version. */
32
- export const TELEGRAM_ADOPTION_INTENT_VERSION = 1;
17
+ export const TELEGRAM_ADOPTION_INTENT_VERSION = 2;
18
+ const LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION = 1 as const;
33
19
  export type TelegramAdoptionTarget = Extract<SessionCreateTarget, { kind: "existing_path" }>;
34
- /** Default intent TTL (configurable at write time). Plan starts at 10 minutes. */
35
20
  export const DEFAULT_ADOPTION_INTENT_TTL_MS = 10 * 60 * 1000;
36
- /** Per-intent filename prefix/suffix under the notifications dir. */
37
21
  const INTENT_FILE_SUFFIX = ".adoption-intent.json";
38
22
  const PENDING_TOPIC_FILE_SUFFIX = ".pending-topic.json";
23
+ const MIGRATION_DIAGNOSTIC_MAX_LENGTH = 256;
39
24
 
40
- /**
41
- * Minimal async filesystem surface the store needs. Structurally a subset of the
42
- * daemon's `TelegramDaemonFs` / `ConversationStoreFs`, so the daemon can pass its
43
- * own `fs` straight through and tests can inject a fake. `open`/`sync`/`close`
44
- * mirror `ConversationStoreFileHandle` so the atomic fsync path reuses the same
45
- * shape as the conversation store.
46
- */
47
25
  export interface AdoptionIntentFs {
48
26
  mkdir(directory: string, options: { recursive: true; mode: number }): Promise<unknown>;
49
27
  chmod(target: string, mode: number): Promise<void>;
@@ -71,14 +49,15 @@ const nodeFs: AdoptionIntentFs = {
71
49
  open: async (file, flags) => fs.promises.open(file, flags),
72
50
  };
73
51
 
74
- /** What gets persisted per intent. No tokens, no digests. */
52
+ /** Persisted before create; `sessionId` is added only by bindSession. */
75
53
  export interface TelegramAdoptionIntent {
76
- intendedSessionId: string;
77
- topicId: number;
78
- chatId: string;
79
- target: TelegramAdoptionTarget;
80
- createdAt: number;
81
- expiresAt: number;
54
+ readonly providerRequestKey: string;
55
+ readonly topicId: number;
56
+ readonly chatId: string;
57
+ readonly target: TelegramAdoptionTarget;
58
+ readonly createdAt: number;
59
+ readonly expiresAt: number;
60
+ readonly sessionId?: string;
82
61
  }
83
62
 
84
63
  interface PersistedIntent {
@@ -86,12 +65,27 @@ interface PersistedIntent {
86
65
  intent: TelegramAdoptionIntent;
87
66
  }
88
67
 
89
- /** Durable authorization that a topic was observed through forum_topic_created. */
68
+ interface LegacyTelegramAdoptionIntent {
69
+ readonly intendedSessionId: string;
70
+ readonly topicId: number;
71
+ readonly chatId: string;
72
+ readonly target: TelegramAdoptionTarget;
73
+ readonly createdAt: number;
74
+ readonly expiresAt: number;
75
+ }
76
+
77
+ interface LegacyPersistedIntent {
78
+ version: typeof LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION;
79
+ intent: LegacyTelegramAdoptionIntent;
80
+ }
81
+
82
+ type PersistedIntentRecord = PersistedIntent | LegacyPersistedIntent;
83
+
90
84
  export interface TelegramPendingTopic {
91
- topicId: number;
92
- chatId: string;
93
- createdAt: number;
94
- expiresAt: number;
85
+ readonly topicId: number;
86
+ readonly chatId: string;
87
+ readonly createdAt: number;
88
+ readonly expiresAt: number;
95
89
  }
96
90
 
97
91
  interface PersistedPendingTopic {
@@ -99,6 +93,13 @@ interface PersistedPendingTopic {
99
93
  pendingTopic: TelegramPendingTopic;
100
94
  }
101
95
 
96
+ interface LegacyPersistedPendingTopic {
97
+ version: typeof LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION;
98
+ pendingTopic: TelegramPendingTopic;
99
+ }
100
+
101
+ type PersistedPendingTopicRecord = PersistedPendingTopic | LegacyPersistedPendingTopic;
102
+
102
103
  function isRecord(value: unknown): value is Record<string, unknown> {
103
104
  return typeof value === "object" && value !== null && !Array.isArray(value);
104
105
  }
@@ -107,11 +108,28 @@ function isSessionCreateTarget(value: unknown): value is TelegramAdoptionTarget
107
108
  return isRecord(value) && value.kind === "existing_path" && typeof value.path === "string";
108
109
  }
109
110
 
110
- function isPersistedIntent(value: unknown): value is PersistedIntent {
111
- if (!isRecord(value)) return false;
112
- if (value.version !== TELEGRAM_ADOPTION_INTENT_VERSION) return false;
111
+ function isPersistedIntent(value: unknown): value is PersistedIntentRecord {
112
+ if (!isRecord(value) || !isRecord(value.intent)) return false;
113
+ if (value.version === TELEGRAM_ADOPTION_INTENT_VERSION) {
114
+ const intent = value.intent;
115
+ return (
116
+ typeof intent.providerRequestKey === "string" &&
117
+ intent.providerRequestKey.length > 0 &&
118
+ typeof intent.topicId === "number" &&
119
+ Number.isSafeInteger(intent.topicId) &&
120
+ intent.topicId > 0 &&
121
+ typeof intent.chatId === "string" &&
122
+ typeof intent.createdAt === "number" &&
123
+ Number.isFinite(intent.createdAt) &&
124
+ typeof intent.expiresAt === "number" &&
125
+ Number.isFinite(intent.expiresAt) &&
126
+ intent.expiresAt > intent.createdAt &&
127
+ (intent.sessionId === undefined || (typeof intent.sessionId === "string" && intent.sessionId.length > 0)) &&
128
+ isSessionCreateTarget(intent.target)
129
+ );
130
+ }
131
+ if (value.version !== LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION) return false;
113
132
  const intent = value.intent;
114
- if (!isRecord(intent)) return false;
115
133
  return (
116
134
  typeof intent.intendedSessionId === "string" &&
117
135
  intent.intendedSessionId.length > 0 &&
@@ -128,11 +146,28 @@ function isPersistedIntent(value: unknown): value is PersistedIntent {
128
146
  );
129
147
  }
130
148
 
131
- function isPersistedPendingTopic(value: unknown): value is PersistedPendingTopic {
132
- if (!isRecord(value) || value.version !== TELEGRAM_ADOPTION_INTENT_VERSION) return false;
149
+ function normalizePersistedIntent(value: unknown): { intent: TelegramAdoptionIntent; legacy: boolean } | undefined {
150
+ if (!isPersistedIntent(value)) return undefined;
151
+ if (value.version === TELEGRAM_ADOPTION_INTENT_VERSION) return { intent: value.intent, legacy: false };
152
+ return {
153
+ legacy: true,
154
+ intent: {
155
+ providerRequestKey: `legacy:v1:${value.intent.intendedSessionId}`,
156
+ topicId: value.intent.topicId,
157
+ chatId: value.intent.chatId,
158
+ target: value.intent.target,
159
+ createdAt: value.intent.createdAt,
160
+ expiresAt: value.intent.expiresAt,
161
+ },
162
+ };
163
+ }
164
+
165
+ function isPersistedPendingTopic(value: unknown): value is PersistedPendingTopicRecord {
166
+ if (!isRecord(value) || !isRecord(value.pendingTopic)) return false;
167
+ if (value.version !== TELEGRAM_ADOPTION_INTENT_VERSION && value.version !== LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION)
168
+ return false;
133
169
  const pending = value.pendingTopic;
134
170
  return (
135
- isRecord(pending) &&
136
171
  typeof pending.topicId === "number" &&
137
172
  Number.isSafeInteger(pending.topicId) &&
138
173
  pending.topicId > 0 &&
@@ -145,19 +180,29 @@ function isPersistedPendingTopic(value: unknown): value is PersistedPendingTopic
145
180
  );
146
181
  }
147
182
 
148
- /** Compose the per-intent sidecar path. Public so callers/tests can inspect it. */
149
- export function adoptionIntentFilePath(agentDir: string, intendedSessionId: string): string {
150
- return path.join(daemonPaths(agentDir).dir, `${intendedSessionId}${INTENT_FILE_SUFFIX}`);
183
+ /** Sidecar names are opaque, deterministic encodings of provider request keys. */
184
+ function encodeProviderRequestKey(providerRequestKey: string): string {
185
+ return Buffer.from(providerRequestKey, "utf8").toString("base64url");
186
+ }
187
+
188
+ function decodeProviderRequestKey(value: string): string | undefined {
189
+ try {
190
+ return Buffer.from(value, "base64url").toString("utf8");
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ }
195
+
196
+ export function adoptionIntentFilePath(agentDir: string, providerRequestKey: string): string {
197
+ return path.join(daemonPaths(agentDir).dir, `${encodeProviderRequestKey(providerRequestKey)}${INTENT_FILE_SUFFIX}`);
151
198
  }
152
199
 
153
- /** Compose the sidecar path for a user-created topic awaiting a folder choice. */
154
200
  export function pendingTopicFilePath(agentDir: string, topicId: number): string {
155
201
  return path.join(daemonPaths(agentDir).dir, `${topicId}${PENDING_TOPIC_FILE_SUFFIX}`);
156
202
  }
157
203
 
158
- /** Build a fresh intent with `createdAt`/`expiresAt` derived from `now`/`ttlMs`. */
159
204
  export function buildAdoptionIntent(input: {
160
- intendedSessionId: string;
205
+ providerRequestKey: string;
161
206
  topicId: number;
162
207
  chatId: string;
163
208
  target: TelegramAdoptionTarget;
@@ -165,45 +210,31 @@ export function buildAdoptionIntent(input: {
165
210
  ttlMs?: number;
166
211
  }): TelegramAdoptionIntent {
167
212
  const createdAt = input.now ?? Date.now();
168
- const ttl = input.ttlMs ?? DEFAULT_ADOPTION_INTENT_TTL_MS;
169
213
  return {
170
- intendedSessionId: input.intendedSessionId,
214
+ providerRequestKey: input.providerRequestKey,
171
215
  topicId: input.topicId,
172
216
  chatId: input.chatId,
173
217
  target: input.target,
174
218
  createdAt,
175
- expiresAt: createdAt + ttl,
219
+ expiresAt: createdAt + (input.ttlMs ?? DEFAULT_ADOPTION_INTENT_TTL_MS),
176
220
  };
177
221
  }
178
222
 
179
- /** Test-only: filename suffix used to recognize intent sidecars in a directory listing. */
180
223
  export const ADOPTION_INTENT_FILENAME_SUFFIX = INTENT_FILE_SUFFIX;
181
224
  export const PENDING_TOPIC_FILENAME_SUFFIX = PENDING_TOPIC_FILE_SUFFIX;
182
225
 
183
- /**
184
- * Durable, non-routable adoption-intent store. Owns durable sidecar files and
185
- * the synchronous in-memory topicId claim table. The daemon owns Telegram and
186
- * lifecycle wiring; this store never calls the Telegram API.
187
- */
188
226
  export class TelegramAdoptionIntentStore {
189
227
  readonly #agentDir: string;
190
228
  readonly #dir: string;
191
229
  readonly #fsImpl: AdoptionIntentFs;
192
230
  readonly #now: () => number;
193
231
  readonly #platform: NodeJS.Platform;
194
- /** intendedSessionId -> intent, rehydrated from disk. */
195
232
  readonly #intents = new Map<string, TelegramAdoptionIntent>();
196
- /** topicId -> intendedSessionId currently holding the synchronous claim. */
197
233
  readonly #claims = new Map<number, string>();
198
- /** topicId -> durable authorization observed from forum_topic_created. */
199
234
  readonly #pendingTopics = new Map<number, TelegramPendingTopic>();
235
+ readonly #bindingKeys = new Set<string>();
200
236
 
201
- constructor(input: {
202
- agentDir: string;
203
- fs?: AdoptionIntentFs;
204
- now?: () => number;
205
- platform?: NodeJS.Platform;
206
- }) {
237
+ constructor(input: { agentDir: string; fs?: AdoptionIntentFs; now?: () => number; platform?: NodeJS.Platform }) {
207
238
  this.#agentDir = input.agentDir;
208
239
  this.#dir = daemonPaths(input.agentDir).dir;
209
240
  this.#fsImpl = input.fs ?? nodeFs;
@@ -211,113 +242,117 @@ export class TelegramAdoptionIntentStore {
211
242
  this.#platform = input.platform ?? process.platform;
212
243
  }
213
244
 
214
- /** Directory holding the sidecar files. */
215
245
  get directory(): string {
216
246
  return this.#dir;
217
247
  }
218
248
 
219
- /** In-memory lookup by intendedSessionId (no disk read). undefined if absent/expired. */
220
- bySession(intendedSessionId: string): TelegramAdoptionIntent | undefined {
221
- const intent = this.#intents.get(intendedSessionId);
222
- if (intent === undefined) return undefined;
223
- return this.#now() < intent.expiresAt ? intent : undefined;
249
+ byProviderRequestKey(providerRequestKey: string): TelegramAdoptionIntent | undefined {
250
+ const intent = this.#intents.get(providerRequestKey);
251
+ return intent && this.#now() < intent.expiresAt ? intent : undefined;
252
+ }
253
+
254
+ /** Only a post-create CAS binding makes a reservation visible by SessionId. */
255
+ bySession(sessionId: string): TelegramAdoptionIntent | undefined {
256
+ for (const intent of this.#intents.values()) {
257
+ if (intent.sessionId === sessionId && this.#now() < intent.expiresAt) return intent;
258
+ }
259
+ return undefined;
224
260
  }
225
261
 
226
- /** In-memory lookup of the (single) non-expired intent for a topicId. */
227
262
  byTopic(topicId: number): TelegramAdoptionIntent | undefined {
228
263
  for (const intent of this.#intents.values()) {
229
- if (intent.topicId !== topicId) continue;
230
- if (this.#now() >= intent.expiresAt) continue;
231
- return intent;
264
+ if (intent.topicId === topicId && this.#now() < intent.expiresAt) return intent;
232
265
  }
233
266
  return undefined;
234
267
  }
235
268
 
236
- /** `true` if a non-expired intent already references `topicId`. */
237
269
  hasNonExpiredTopic(topicId: number): boolean {
238
270
  return this.byTopic(topicId) !== undefined;
239
271
  }
240
272
 
241
- /** Durable authorization for an observed user-created topic, if still live. */
242
273
  pendingTopic(topicId: number): TelegramPendingTopic | undefined {
243
274
  const pending = this.#pendingTopics.get(topicId);
244
- return pending !== undefined && this.#now() < pending.expiresAt ? pending : undefined;
275
+ return pending && this.#now() < pending.expiresAt ? pending : undefined;
245
276
  }
246
277
 
247
278
  hasPendingTopic(topicId: number, chatId: string): boolean {
248
279
  return this.pendingTopic(topicId)?.chatId === chatId;
249
280
  }
250
281
 
251
- /**
252
- * Synchronously claim `topicId` for `intendedSessionId`. Returns false if a
253
- * *different* non-expired intendedSessionId already holds the claim (or has a
254
- * non-expired intent for the topic). Same intendedSessionId re-claiming its
255
- * own topic is idempotent and succeeds. The claim is in-memory only; it never
256
- * deletes the sidecar, so a commit failure can `releaseClaim` and retry.
257
- */
258
- tryClaim(topicId: number, intendedSessionId: string): boolean {
282
+ /** Synchronous fail-closed topic claim keyed by provider request identity. */
283
+ tryClaim(topicId: number, providerRequestKey: string): boolean {
259
284
  const holder = this.#claims.get(topicId);
260
- if (holder !== undefined) return holder === intendedSessionId;
261
- // No live claim yet: refuse if a *different* non-expired intent already
262
- // targets this topic (race between two pre-spawn intents on one topic).
285
+ if (holder !== undefined) return holder === providerRequestKey;
263
286
  const existing = this.byTopic(topicId);
264
- if (existing !== undefined && existing.intendedSessionId !== intendedSessionId) return false;
265
- this.#claims.set(topicId, intendedSessionId);
287
+ if (existing !== undefined && existing.providerRequestKey !== providerRequestKey) return false;
288
+ this.#claims.set(topicId, providerRequestKey);
266
289
  return true;
267
290
  }
268
291
 
269
- /** Release a claim held by `intendedSessionId` on `topicId`. No-op otherwise. */
270
- releaseClaim(topicId: number, intendedSessionId: string): void {
271
- if (this.#claims.get(topicId) === intendedSessionId) this.#claims.delete(topicId);
292
+ releaseClaim(topicId: number, providerRequestKey: string): void {
293
+ if (this.#claims.get(topicId) === providerRequestKey) this.#claims.delete(topicId);
272
294
  }
273
295
 
274
- /**
275
- * Durable write of one intent: temp→write→fsync→chmod(0600)→atomic rename→
276
- * chmod(final, 0600)→parent-dir fsync, then track it in memory. The dir is
277
- * created owner-only (0700). chmod-ing the final path after the rename makes
278
- * the 0600 contract durable regardless of whether the platform's rename
279
- * preserves the source mode (POSIX does; some test/network filesystems and
280
- * the in-memory test double model rename as copy+delete and would otherwise
281
- * drop it). The rename is still the atomic commit point; the final chmod only
282
- * tightens permissions on the already-durable file. The in-memory map is only
283
- * mutated after the atomic persist succeeds. Rejects (does not swallow) so the
284
- * daemon can decide fail-closed behavior.
285
- */
286
296
  async put(intent: TelegramAdoptionIntent): Promise<void> {
297
+ const current =
298
+ this.#intents.get(intent.providerRequestKey) ?? (await this.readIntent(intent.providerRequestKey));
299
+ if (
300
+ current &&
301
+ (current.topicId !== intent.topicId ||
302
+ current.chatId !== intent.chatId ||
303
+ current.target.path !== intent.target.path)
304
+ )
305
+ throw new Error("provider request key is already reserved for a different topic or target");
287
306
  const stored: TelegramAdoptionIntent = {
288
- intendedSessionId: intent.intendedSessionId,
307
+ providerRequestKey: intent.providerRequestKey,
289
308
  topicId: intent.topicId,
290
309
  chatId: intent.chatId,
291
310
  target: { kind: "existing_path", path: intent.target.path },
292
311
  createdAt: intent.createdAt,
293
312
  expiresAt: intent.expiresAt,
313
+ ...((intent.sessionId ?? current?.sessionId) ? { sessionId: intent.sessionId ?? current?.sessionId } : {}),
294
314
  };
295
- const file = adoptionIntentFilePath(this.#agentDir, stored.intendedSessionId);
296
- const payload: PersistedIntent = { version: TELEGRAM_ADOPTION_INTENT_VERSION, intent: stored };
297
- if (!isPersistedIntent(payload)) throw new Error("invalid Telegram adoption intent");
298
- await this.#writeSidecar(file, payload);
299
- this.#intents.set(stored.intendedSessionId, stored);
315
+ const file = adoptionIntentFilePath(this.#agentDir, stored.providerRequestKey);
316
+ await this.#writeSidecar(file, { version: TELEGRAM_ADOPTION_INTENT_VERSION, intent: stored });
317
+ this.#intents.set(stored.providerRequestKey, stored);
318
+ }
319
+
320
+ /** CAS-bind the Broker-returned opaque SessionId exactly once. */
321
+ async bindSession(providerRequestKey: string, sessionId: string): Promise<boolean> {
322
+ if (!sessionId || this.#bindingKeys.has(providerRequestKey)) return false;
323
+ const current = this.byProviderRequestKey(providerRequestKey);
324
+ if (!current) return false;
325
+ if (current.sessionId !== undefined) return current.sessionId === sessionId;
326
+ this.#bindingKeys.add(providerRequestKey);
327
+ try {
328
+ const onDisk = await this.readIntent(providerRequestKey);
329
+ if (!onDisk || (onDisk.sessionId !== undefined && onDisk.sessionId !== sessionId)) return false;
330
+ const bound: TelegramAdoptionIntent = { ...current, sessionId };
331
+ await this.#writeSidecar(adoptionIntentFilePath(this.#agentDir, providerRequestKey), {
332
+ version: TELEGRAM_ADOPTION_INTENT_VERSION,
333
+ intent: bound,
334
+ });
335
+ this.#intents.set(providerRequestKey, bound);
336
+ return true;
337
+ } finally {
338
+ this.#bindingKeys.delete(providerRequestKey);
339
+ }
300
340
  }
301
341
 
302
- /** Persist authorization for a topic observed through forum_topic_created. */
303
342
  async putPendingTopic(pendingTopic: TelegramPendingTopic): Promise<void> {
304
- const stored: TelegramPendingTopic = {
305
- topicId: pendingTopic.topicId,
306
- chatId: pendingTopic.chatId,
307
- createdAt: pendingTopic.createdAt,
308
- expiresAt: pendingTopic.expiresAt,
309
- };
310
- const file = pendingTopicFilePath(this.#agentDir, stored.topicId);
311
- const payload: PersistedPendingTopic = {
343
+ const stored = { ...pendingTopic };
344
+ await this.#writeSidecar(pendingTopicFilePath(this.#agentDir, stored.topicId), {
312
345
  version: TELEGRAM_ADOPTION_INTENT_VERSION,
313
346
  pendingTopic: stored,
314
- };
315
- if (!isPersistedPendingTopic(payload)) throw new Error("invalid Telegram pending topic");
316
- await this.#writeSidecar(file, payload);
347
+ });
317
348
  this.#pendingTopics.set(stored.topicId, stored);
318
349
  }
319
350
 
320
- async #writeSidecar(file: string, payload: PersistedIntent | PersistedPendingTopic): Promise<void> {
351
+ async #writeSidecar(
352
+ file: string,
353
+ payload: PersistedIntent | PersistedPendingTopic,
354
+ retainRenamedFileOnFailure = false,
355
+ ): Promise<void> {
321
356
  await this.#fsImpl.mkdir(this.#dir, { recursive: true, mode: 0o700 });
322
357
  await this.#fsImpl.chmod(this.#dir, 0o700);
323
358
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
@@ -336,6 +371,7 @@ export class TelegramAdoptionIntentStore {
336
371
  await this.#fsImpl.chmod(file, 0o600);
337
372
  await this.#syncParentDirectory();
338
373
  } catch (error) {
374
+ if (renamed && retainRenamedFileOnFailure) throw new SidecarDurabilityUncertainError(error);
339
375
  const cleanupTarget = renamed ? file : temporary;
340
376
  let cleanupError: unknown;
341
377
  try {
@@ -349,45 +385,27 @@ export class TelegramAdoptionIntentStore {
349
385
  }
350
386
  }
351
387
 
352
- /**
353
- * Read a single intent sidecar from disk and track it if it is non-expired
354
- * and well-formed. Returns the parsed intent (or undefined). Used by
355
- * `rehydrate`; callers normally use `rehydrate` / `bySession`.
356
- */
357
- async readIntent(intendedSessionId: string): Promise<TelegramAdoptionIntent | undefined> {
358
- const file = adoptionIntentFilePath(this.#agentDir, intendedSessionId);
359
- let raw: string;
360
- try {
361
- raw = await this.#fsImpl.readFile(file, "utf8");
362
- } catch (error) {
363
- if (isMissing(error)) return undefined;
364
- throw error;
365
- }
366
- let parsed: unknown;
367
- try {
368
- parsed = JSON.parse(raw);
369
- } catch {
388
+ async readIntent(providerRequestKey: string): Promise<TelegramAdoptionIntent | undefined> {
389
+ const file = adoptionIntentFilePath(this.#agentDir, providerRequestKey);
390
+ const record = await this.#readSidecarRecord(file);
391
+ const parsed = record?.intent;
392
+ if (!record || !parsed || parsed.providerRequestKey !== providerRequestKey || this.#now() >= parsed.expiresAt)
370
393
  return undefined;
371
- }
372
- if (!isPersistedIntent(parsed) || parsed.intent.intendedSessionId !== intendedSessionId) return undefined;
373
- if (this.#now() >= parsed.intent.expiresAt) return undefined;
374
- this.#intents.set(intendedSessionId, parsed.intent);
375
- return parsed.intent;
394
+ this.#intents.set(providerRequestKey, parsed);
395
+ if (record.legacy) await this.#migrateIntentSidecar(file, parsed);
396
+ return parsed;
376
397
  }
377
398
 
378
399
  async readPendingTopic(topicId: number): Promise<TelegramPendingTopic | undefined> {
379
400
  const file = pendingTopicFilePath(this.#agentDir, topicId);
380
- const pending = await this.#readPendingSidecar(file);
381
- if (!pending || pending.topicId !== topicId || this.#now() >= pending.expiresAt) return undefined;
401
+ const record = await this.#readPendingSidecarRecord(file);
402
+ const pending = record?.pendingTopic;
403
+ if (!record || !pending || pending.topicId !== topicId || this.#now() >= pending.expiresAt) return undefined;
382
404
  this.#pendingTopics.set(topicId, pending);
405
+ if (record.legacy) await this.#migratePendingTopicSidecar(file, pending);
383
406
  return pending;
384
407
  }
385
408
 
386
- /**
387
- * Rehydrate all non-expired intents from the notifications dir. Files are the
388
- * only source: missing/corrupt/expired entries are skipped (never thrown).
389
- * Returns the count of live intents loaded.
390
- */
391
409
  async rehydrate(): Promise<number> {
392
410
  let names: readonly string[];
393
411
  try {
@@ -398,55 +416,52 @@ export class TelegramAdoptionIntentStore {
398
416
  }
399
417
  let loaded = 0;
400
418
  for (const name of names) {
419
+ const file = path.join(this.#dir, name);
401
420
  if (name.endsWith(INTENT_FILE_SUFFIX)) {
402
- const intendedSessionId = name.slice(0, -INTENT_FILE_SUFFIX.length);
403
- if (!intendedSessionId) continue;
404
- const intent = await this.readIntent(intendedSessionId);
405
- if (intent) loaded++;
421
+ const encoded = name.slice(0, -INTENT_FILE_SUFFIX.length);
422
+ const key = decodeProviderRequestKey(encoded);
423
+ const record = await this.#readSidecarRecord(file);
424
+ const intent = record?.intent;
425
+ if (!intent || this.#now() >= intent.expiresAt) continue;
426
+ if (!record.legacy && (!key || intent.providerRequestKey !== key)) continue;
427
+ this.#intents.set(intent.providerRequestKey, intent);
428
+ if (record.legacy) await this.#migrateIntentSidecar(file, intent);
429
+ loaded++;
406
430
  continue;
407
431
  }
408
432
  if (!name.endsWith(PENDING_TOPIC_FILE_SUFFIX)) continue;
409
- const rawTopicId = name.slice(0, -PENDING_TOPIC_FILE_SUFFIX.length);
410
- const topicId = Number(rawTopicId);
433
+ const topicId = Number(name.slice(0, -PENDING_TOPIC_FILE_SUFFIX.length));
411
434
  if (!Number.isSafeInteger(topicId) || topicId <= 0) continue;
412
- const pending = await this.readPendingTopic(topicId);
413
- if (pending) loaded++;
435
+ const record = await this.#readPendingSidecarRecord(file);
436
+ const pending = record?.pendingTopic;
437
+ if (!pending || pending.topicId !== topicId || this.#now() >= pending.expiresAt) continue;
438
+ this.#pendingTopics.set(topicId, pending);
439
+ if (record.legacy) await this.#migratePendingTopicSidecar(file, pending);
440
+ loaded++;
414
441
  }
415
442
  return loaded;
416
443
  }
417
444
 
418
- /**
419
- * Remove a single intent sidecar and drop its in-memory entry. Safe to call
420
- * after a successful registry commit or a definite spawn failure. Missing
421
- * files are not an error. Never calls the Telegram API.
422
- */
423
- async remove(intendedSessionId: string): Promise<void> {
424
- const file = adoptionIntentFilePath(this.#agentDir, intendedSessionId);
445
+ async remove(providerRequestKey: string): Promise<void> {
425
446
  try {
426
- await this.#fsImpl.unlink(file);
447
+ await this.#fsImpl.unlink(adoptionIntentFilePath(this.#agentDir, providerRequestKey));
427
448
  } catch (error) {
428
449
  if (!isMissing(error)) throw error;
429
450
  }
430
- const intent = this.#intents.get(intendedSessionId);
431
- this.#intents.delete(intendedSessionId);
432
- if (intent) this.releaseClaim(intent.topicId, intendedSessionId);
451
+ const intent = this.#intents.get(providerRequestKey);
452
+ this.#intents.delete(providerRequestKey);
453
+ if (intent) this.releaseClaim(intent.topicId, providerRequestKey);
433
454
  }
434
455
 
435
- /** Remove a pending-topic authorization after an adoption intent is durable. */
436
456
  async removePendingTopic(topicId: number): Promise<void> {
437
- const file = pendingTopicFilePath(this.#agentDir, topicId);
438
457
  try {
439
- await this.#fsImpl.unlink(file);
458
+ await this.#fsImpl.unlink(pendingTopicFilePath(this.#agentDir, topicId));
440
459
  } catch (error) {
441
460
  if (!isMissing(error)) throw error;
442
461
  }
443
462
  this.#pendingTopics.delete(topicId);
444
463
  }
445
464
 
446
- /**
447
- * Sweep expired intent sidecars from disk. Touches files only. Returns the
448
- * count of expired sidecars removed. Safe to run at startup and periodically.
449
- */
450
465
  async sweepExpired(): Promise<number> {
451
466
  let names: readonly string[];
452
467
  try {
@@ -460,10 +475,9 @@ export class TelegramAdoptionIntentStore {
460
475
  for (const name of names) {
461
476
  const file = path.join(this.#dir, name);
462
477
  if (name.endsWith(INTENT_FILE_SUFFIX)) {
463
- const intendedSessionId = name.slice(0, -INTENT_FILE_SUFFIX.length);
464
- if (!intendedSessionId) continue;
465
- const intent = await this.#readSidecar(file);
466
- if (intent && intent.intendedSessionId === intendedSessionId && now < intent.expiresAt) continue;
478
+ const record = await this.#readSidecarRecord(file);
479
+ const intent = record?.intent;
480
+ if (intent && now < intent.expiresAt) continue;
467
481
  try {
468
482
  await this.#fsImpl.unlink(file);
469
483
  removed++;
@@ -471,15 +485,14 @@ export class TelegramAdoptionIntentStore {
471
485
  if (!isMissing(error)) throw error;
472
486
  }
473
487
  if (intent) {
474
- this.#intents.delete(intent.intendedSessionId);
475
- this.releaseClaim(intent.topicId, intent.intendedSessionId);
488
+ this.#intents.delete(intent.providerRequestKey);
489
+ this.releaseClaim(intent.topicId, intent.providerRequestKey);
476
490
  }
477
491
  continue;
478
492
  }
479
493
  if (!name.endsWith(PENDING_TOPIC_FILE_SUFFIX)) continue;
480
- const rawTopicId = name.slice(0, -PENDING_TOPIC_FILE_SUFFIX.length);
481
- const topicId = Number(rawTopicId);
482
- const pending = await this.#readPendingSidecar(file);
494
+ const topicId = Number(name.slice(0, -PENDING_TOPIC_FILE_SUFFIX.length));
495
+ const pending = (await this.#readPendingSidecarRecord(file))?.pendingTopic;
483
496
  if (Number.isSafeInteger(topicId) && topicId > 0 && pending?.topicId === topicId && now < pending.expiresAt)
484
497
  continue;
485
498
  try {
@@ -493,8 +506,7 @@ export class TelegramAdoptionIntentStore {
493
506
  return removed;
494
507
  }
495
508
 
496
- /** Read and validate a sidecar at an arbitrary path (used by sweep). */
497
- async #readSidecar(file: string): Promise<TelegramAdoptionIntent | undefined> {
509
+ async #readSidecarRecord(file: string): Promise<{ intent: TelegramAdoptionIntent; legacy: boolean } | undefined> {
498
510
  let raw: string;
499
511
  try {
500
512
  raw = await this.#fsImpl.readFile(file, "utf8");
@@ -502,16 +514,16 @@ export class TelegramAdoptionIntentStore {
502
514
  if (isMissing(error)) return undefined;
503
515
  throw error;
504
516
  }
505
- let parsed: unknown;
506
517
  try {
507
- parsed = JSON.parse(raw);
518
+ return normalizePersistedIntent(JSON.parse(raw) as unknown);
508
519
  } catch {
509
520
  return undefined;
510
521
  }
511
- return isPersistedIntent(parsed) ? parsed.intent : undefined;
512
522
  }
513
523
 
514
- async #readPendingSidecar(file: string): Promise<TelegramPendingTopic | undefined> {
524
+ async #readPendingSidecarRecord(
525
+ file: string,
526
+ ): Promise<{ pendingTopic: TelegramPendingTopic; legacy: boolean } | undefined> {
515
527
  let raw: string;
516
528
  try {
517
529
  raw = await this.#fsImpl.readFile(file, "utf8");
@@ -519,16 +531,60 @@ export class TelegramAdoptionIntentStore {
519
531
  if (isMissing(error)) return undefined;
520
532
  throw error;
521
533
  }
522
- let parsed: unknown;
523
534
  try {
524
- parsed = JSON.parse(raw);
535
+ const parsed: unknown = JSON.parse(raw);
536
+ if (!isPersistedPendingTopic(parsed)) return undefined;
537
+ return {
538
+ pendingTopic: parsed.pendingTopic,
539
+ legacy: parsed.version === LEGACY_TELEGRAM_ADOPTION_INTENT_VERSION,
540
+ };
525
541
  } catch {
526
542
  return undefined;
527
543
  }
528
- return isPersistedPendingTopic(parsed) ? parsed.pendingTopic : undefined;
529
544
  }
530
545
 
531
- /** fsync the parent dir so the rename survives a crash; best-effort on win32. */
546
+ async #migrateIntentSidecar(file: string, intent: TelegramAdoptionIntent): Promise<void> {
547
+ const destination = adoptionIntentFilePath(this.#agentDir, intent.providerRequestKey);
548
+ try {
549
+ await this.#writeSidecar(
550
+ destination,
551
+ { version: TELEGRAM_ADOPTION_INTENT_VERSION, intent },
552
+ destination === file,
553
+ );
554
+ } catch (error) {
555
+ if (error instanceof SidecarDurabilityUncertainError) logMigrationDurabilityUncertain("intent", error);
556
+ else logMigrationFailure("intent", "write", error);
557
+ return;
558
+ }
559
+ if (destination === file) return;
560
+ try {
561
+ await this.#fsImpl.unlink(file);
562
+ } catch (error) {
563
+ logMigrationFailure("intent", "unlink", error);
564
+ }
565
+ }
566
+
567
+ async #migratePendingTopicSidecar(file: string, pendingTopic: TelegramPendingTopic): Promise<void> {
568
+ const destination = pendingTopicFilePath(this.#agentDir, pendingTopic.topicId);
569
+ try {
570
+ await this.#writeSidecar(
571
+ destination,
572
+ { version: TELEGRAM_ADOPTION_INTENT_VERSION, pendingTopic },
573
+ destination === file,
574
+ );
575
+ } catch (error) {
576
+ if (error instanceof SidecarDurabilityUncertainError) logMigrationDurabilityUncertain("pending_topic", error);
577
+ else logMigrationFailure("pending_topic", "write", error);
578
+ return;
579
+ }
580
+ if (destination === file) return;
581
+ try {
582
+ await this.#fsImpl.unlink(file);
583
+ } catch (error) {
584
+ logMigrationFailure("pending_topic", "unlink", error);
585
+ }
586
+ }
587
+
532
588
  async #syncParentDirectory(): Promise<void> {
533
589
  let handle: AdoptionIntentFileHandle;
534
590
  try {
@@ -556,6 +612,40 @@ export class TelegramAdoptionIntentStore {
556
612
  }
557
613
  }
558
614
 
615
+ function migrationFailureMessage(error: unknown): string {
616
+ const message =
617
+ error instanceof Error ? error.message : typeof error === "string" ? error : "non-error migration failure";
618
+ return message.length <= MIGRATION_DIAGNOSTIC_MAX_LENGTH
619
+ ? message
620
+ : `${message.slice(0, MIGRATION_DIAGNOSTIC_MAX_LENGTH - 1)}…`;
621
+ }
622
+
623
+ class SidecarDurabilityUncertainError extends Error {
624
+ constructor(error: unknown) {
625
+ super(migrationFailureMessage(error));
626
+ this.name = "SidecarDurabilityUncertainError";
627
+ }
628
+ }
629
+
630
+ function logMigrationDurabilityUncertain(sidecar: "intent" | "pending_topic", error: unknown): void {
631
+ logger.warn(
632
+ "notifications: Telegram adoption sidecar migration durability is uncertain; retained migrated v2 sidecar",
633
+ {
634
+ sidecar,
635
+ stage: "durability",
636
+ error: migrationFailureMessage(error),
637
+ },
638
+ );
639
+ }
640
+
641
+ function logMigrationFailure(sidecar: "intent" | "pending_topic", stage: "write" | "unlink", error: unknown): void {
642
+ logger.warn("notifications: Telegram adoption sidecar migration failed; retaining legacy sidecar", {
643
+ sidecar,
644
+ stage,
645
+ error: migrationFailureMessage(error),
646
+ });
647
+ }
648
+
559
649
  async function syncRequired(handle: AdoptionIntentFileHandle): Promise<void> {
560
650
  if (typeof handle.sync !== "function")
561
651
  throw new Error("adoption sidecar durability requires filesystem sync support");