@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,5 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { logger } from "@gajae-code/utils";
2
3
  import { SdkClientError } from "../client/client";
4
+ import { type SessionAttachment, SessionRouterError } from "../router";
3
5
  import type { ChatDeliveryError } from "./chat-daemon-runtime";
4
6
 
5
7
  import {
@@ -20,10 +22,10 @@ import type { DiscordInboundEvent, DiscordMessageComponent, DiscordProvider, Dis
20
22
 
21
23
  const FAILURE = "This conversation is no longer available.";
22
24
 
23
- export class DiscordEndpointBindingError extends Error {
24
- constructor(message = "Discord session endpoint changed before outbound publication.") {
25
+ export class DiscordAttachmentBindingError extends Error {
26
+ constructor(message = "Discord session attachment changed before outbound publication.") {
25
27
  super(message);
26
- this.name = "DiscordEndpointBindingError";
28
+ this.name = "DiscordAttachmentBindingError";
27
29
  }
28
30
  }
29
31
 
@@ -71,13 +73,6 @@ function withoutClosingIntent(record: DiscordConversation, closedAt: number): Di
71
73
  };
72
74
  }
73
75
 
74
- /** A captured live SDK transport. `isCurrent` must fail closed after replacement or removal. */
75
- export interface DiscordEndpointBinding {
76
- generation: number;
77
- isCurrent(): boolean;
78
- send(frame: Record<string, unknown>): void | Promise<void>;
79
- }
80
-
81
76
  export interface DiscordLeaseRecoveryScheduler {
82
77
  setTimeout(callback: () => void | Promise<void>, delayMs: number): unknown;
83
78
  clearTimeout(handle: unknown): void;
@@ -85,17 +80,19 @@ export interface DiscordLeaseRecoveryScheduler {
85
80
 
86
81
  export interface DiscordNotificationDaemonOptions {
87
82
  agentDir: string;
88
- repo: string;
89
83
  guildId: string;
90
84
  parentChannelId: string;
91
85
  provider: DiscordProvider;
92
86
  now?: () => number;
93
87
  leaseRecoveryScheduler?: DiscordLeaseRecoveryScheduler;
94
- resolveEndpoint: (sessionId: string, expectedGeneration?: number) => Promise<DiscordEndpointBinding | null>;
88
+ resolveAttachment: (
89
+ sessionId: string,
90
+ expectedGeneration?: number,
91
+ ) => SessionAttachment | null | Promise<SessionAttachment | null>;
95
92
  onCommand?: (
96
93
  sessionId: string,
97
94
  content: string,
98
- endpoint: DiscordEndpointBinding,
95
+ attachment: SessionAttachment,
99
96
  idempotencyKey: string,
100
97
  ) => Promise<boolean>;
101
98
  }
@@ -103,6 +100,7 @@ export interface DiscordNotificationDaemonOptions {
103
100
  export interface DiscordNotificationInput {
104
101
  sessionId: string;
105
102
  endpointGeneration: number;
103
+ attachmentAuthorityId?: string;
106
104
  content: string;
107
105
  threadName?: string;
108
106
  actionId?: string;
@@ -130,6 +128,7 @@ type DiscordInboundRouting = {
130
128
  parentId: string;
131
129
  threadId: string;
132
130
  eventId: string;
131
+ attachmentAuthorityId?: string;
133
132
  interactionId?: string;
134
133
  kind: "command" | "action";
135
134
  actionId?: string;
@@ -162,18 +161,23 @@ export class DiscordNotificationDaemon {
162
161
  readonly #creates = new Map<string, Promise<DiscordConversation>>();
163
162
  readonly #resumes = new Map<string, Promise<DiscordConversation | undefined>>();
164
163
  readonly #archives = new Map<string, Promise<void>>();
165
- readonly #resolveEndpoint: (
164
+ readonly #resolveAttachment: (
166
165
  sessionId: string,
167
166
  expectedGeneration?: number,
168
- ) => Promise<DiscordEndpointBinding | null>;
167
+ ) => SessionAttachment | null | Promise<SessionAttachment | null>;
169
168
  readonly #effects: ChatEffectJournal;
170
169
  readonly #activeWork = new Set<Promise<unknown>>();
170
+ readonly #workInvalidators = new Set<() => Promise<void>>();
171
171
  readonly #inflightInbound = new Set<string>();
172
172
  #started = false;
173
173
  #lifecycleGeneration = 0;
174
+ #workGeneration = 0;
174
175
  #startTask: Promise<void> | undefined;
175
176
  #stopTask: Promise<void> | undefined;
176
177
  #providerStarting = false;
178
+ #providerLifecycleTail: Promise<void> | undefined;
179
+ #providerLifecycleError: unknown;
180
+ #providerLifecycleErrorSet = false;
177
181
 
178
182
  #leaseRecoveryTimer: unknown;
179
183
  #leaseRecoveryAt: number | undefined;
@@ -191,11 +195,30 @@ export class DiscordNotificationDaemon {
191
195
  setTimeout: (callback, delayMs) => setTimeout(() => void callback(), delayMs),
192
196
  clearTimeout: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
193
197
  };
194
- this.#resolveEndpoint = options.resolveEndpoint;
198
+ this.#resolveAttachment = options.resolveAttachment;
199
+ }
200
+ restartBlocked(): boolean {
201
+ return this.#providerLifecycleTail !== undefined || this.#providerLifecycleErrorSet;
195
202
  }
196
203
 
197
204
  async start(): Promise<void> {
198
205
  if (this.#started && !this.#stopTask) return;
206
+ if (this.#providerLifecycleErrorSet) throw this.#providerLifecycleError;
207
+ if (this.#providerLifecycleTail) {
208
+ const tail = this.#providerLifecycleTail;
209
+ const settled = await Promise.race([
210
+ Promise.allSettled([tail]).then(() => true),
211
+ Bun.sleep(5_000).then(() => false),
212
+ ]);
213
+ if (!settled) throw new Error("Prior Discord provider shutdown did not settle before restart.");
214
+ try {
215
+ await tail;
216
+ } catch (error) {
217
+ this.#recordProviderLifecycleError(error);
218
+ throw error;
219
+ }
220
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
221
+ }
199
222
  if (this.#stopTask) {
200
223
  await this.#stopTask;
201
224
  return await this.start();
@@ -239,7 +262,10 @@ export class DiscordNotificationDaemon {
239
262
  this.#providerStarting = true;
240
263
  try {
241
264
  await this.options.provider.start(
242
- event => this.#track(this.handleInbound(event)),
265
+ async event => {
266
+ if (lifecycleGeneration !== this.#lifecycleGeneration || !this.#started) return;
267
+ await this.#track(this.handleInbound(event));
268
+ },
243
269
  () => {},
244
270
  );
245
271
  } finally {
@@ -247,7 +273,12 @@ export class DiscordNotificationDaemon {
247
273
  }
248
274
  if (lifecycleGeneration !== this.#lifecycleGeneration) {
249
275
  this.#started = false;
250
- await this.options.provider.stop();
276
+ try {
277
+ await this.options.provider.stop();
278
+ } catch (error) {
279
+ this.#recordProviderLifecycleError(error);
280
+ throw error;
281
+ }
251
282
  }
252
283
  } catch (error) {
253
284
  if (lifecycleGeneration === this.#lifecycleGeneration) {
@@ -282,36 +313,127 @@ export class DiscordNotificationDaemon {
282
313
  }
283
314
 
284
315
  async #stop(starting: Promise<void> | undefined, stopProvider: boolean): Promise<void> {
285
- // Calling provider.stop() before awaiting start lets a provider cancel a
286
- // Gateway open that is already in flight. #start rechecks the generation
287
- // after it resolves and closes any late open.
288
- if (stopProvider) await this.options.provider.stop();
289
- try {
290
- await starting;
291
- } catch {
292
- // The caller of start() owns its recovery/start error; stop still drains it.
316
+ const drainDeadline = this.#now() + 5_000;
317
+ let providerStopError: unknown;
318
+ let providerStopRejected = false;
319
+ const providerLifecycle = (async () => {
320
+ // Calling provider.stop() before awaiting start lets a provider cancel a
321
+ // Gateway open that is already in flight. #start rechecks the generation
322
+ // after it resolves and closes any late open.
323
+ if (stopProvider) {
324
+ try {
325
+ await this.options.provider.stop();
326
+ } catch (error) {
327
+ providerStopRejected = true;
328
+ providerStopError = error;
329
+ }
330
+ }
331
+ try {
332
+ await starting;
333
+ } catch {
334
+ // The caller of start() owns its recovery/start error; stop still drains it.
335
+ }
336
+ if (this.#providerStarting || this.#started) {
337
+ this.#started = false;
338
+ try {
339
+ await this.options.provider.stop();
340
+ } catch (error) {
341
+ providerStopRejected = true;
342
+ providerStopError ??= error;
343
+ }
344
+ }
345
+ if (providerStopRejected) throw providerStopError;
346
+ })();
347
+ const providerOutcome = await Promise.race([
348
+ providerLifecycle.then(
349
+ () => ({ kind: "settled" as const }),
350
+ error => ({ kind: "rejected" as const, error }),
351
+ ),
352
+ Bun.sleep(Math.max(0, drainDeadline - this.#now())).then(() => ({ kind: "timeout" as const })),
353
+ ]);
354
+ if (providerOutcome.kind === "rejected") {
355
+ this.#recordProviderLifecycleError(providerOutcome.error);
356
+ await this.#invalidateActiveWork();
357
+ await this.#drainActiveWork(drainDeadline);
358
+ throw providerOutcome.error;
293
359
  }
294
- if (this.#providerStarting || this.#started) {
295
- this.#started = false;
296
- await this.options.provider.stop();
360
+ if (providerOutcome.kind === "timeout") {
361
+ this.#retainProviderLifecycle(providerLifecycle);
362
+ await this.#invalidateActiveWork();
363
+ await this.#drainActiveWork(drainDeadline);
364
+ logger.warn(
365
+ "Discord provider lifecycle exceeded the 5000ms shutdown drain; continuing with Router revocation.",
366
+ );
367
+ return;
297
368
  }
298
- // Drain until quiescent: a tracked task can schedule further tracked work while
299
- // we await, and any that outlives stop() would bleed timing pressure into the
300
- // next daemon/test. #started is false, so no new recovery timers arm and the
301
- // loop terminates.
302
- while (this.#activeWork.size > 0) await Promise.all([...this.#activeWork]);
369
+ // Drain until quiescent, but never let a hung provider REST request prevent
370
+ // SessionRouter authority revocation and daemon ownership release.
371
+ await this.#drainActiveWork(drainDeadline);
372
+ }
373
+
374
+ async #invalidateActiveWork(): Promise<void> {
375
+ this.#workGeneration += 1;
376
+ await Promise.allSettled([...this.#workInvalidators].map(invalidate => invalidate()));
377
+ }
378
+
379
+ async #drainActiveWork(deadline: number): Promise<void> {
380
+ while (this.#activeWork.size > 0) {
381
+ const remaining = deadline - this.#now();
382
+ if (remaining <= 0) {
383
+ await this.#invalidateActiveWork();
384
+ logger.warn("Discord provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.");
385
+ return;
386
+ }
387
+ const settled = await Promise.race([
388
+ Promise.allSettled([...this.#activeWork]).then(() => true),
389
+ Bun.sleep(remaining).then(() => false),
390
+ ]);
391
+ if (!settled) {
392
+ await this.#invalidateActiveWork();
393
+ logger.warn("Discord provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.");
394
+ return;
395
+ }
396
+ }
397
+ }
398
+
399
+ #recordProviderLifecycleError(error: unknown): void {
400
+ this.#providerLifecycleError = error;
401
+ this.#providerLifecycleErrorSet = true;
303
402
  }
304
403
 
404
+ #retainProviderLifecycle(tail: Promise<void>): void {
405
+ this.#providerLifecycleTail = tail;
406
+ void tail.then(
407
+ () => {
408
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
409
+ },
410
+ error => {
411
+ this.#recordProviderLifecycleError(error);
412
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
413
+ },
414
+ );
415
+ }
416
+
417
+ /**
418
+ * Frame-driven publication work is tracked like inbound events so that stop()
419
+ * drains it while the Router still holds attachment authority. A session
420
+ * replayed during teardown otherwise reaches its post-provider binding
421
+ * revalidation only after Router.stop() has revoked the attachment, turning a
422
+ * legitimate create into a spurious teardown rejection.
423
+ */
305
424
  async notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
306
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
425
+ return await this.#track(this.#notify(input));
426
+ }
427
+ async #notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
428
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
307
429
  const conversation = await this.#ensureConversation(input);
308
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
430
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
309
431
  if (conversation.state !== "active" || !conversation.threadId) throw new Error("Discord thread is unavailable");
310
432
  const pendingActionId = input.actionId;
311
433
  const authoritative = pendingActionId
312
434
  ? await this.#ensureActionPublication(conversation, pendingActionId)
313
435
  : conversation;
314
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
436
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
315
437
  const components =
316
438
  pendingActionId && authoritative.pendingActionNonce && input.options && input.options.length > 0
317
439
  ? actionComponents(
@@ -335,17 +457,64 @@ export class DiscordNotificationDaemon {
335
457
 
336
458
  /** Posts a safe command outcome to the active mapped conversation. */
337
459
  async postCommandResult(sessionId: string, content: string): Promise<boolean> {
460
+ return await this.#track(this.#postCommandResult(sessionId, content));
461
+ }
462
+ async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
338
463
  const record = await this.#bySession(sessionId);
339
464
  if (record?.state !== "active" || !record.threadId) return false;
340
465
  await this.#postEffect(`command-result:${record.threadId}:${randomUUID()}`, record, content);
341
466
  return true;
342
467
  }
343
468
 
344
- async close(sessionId: string): Promise<void> {
345
- const closing = await this.#markClosing(sessionId);
469
+ async close(sessionId: string, endpointGeneration?: number): Promise<void> {
470
+ const closing = await this.#markClosing(sessionId, endpointGeneration);
346
471
  if (closing) await this.#driveClose(closing);
347
472
  }
348
473
 
474
+ async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
475
+ const record = await this.#bySession(sessionId);
476
+ if (record?.endpointGeneration !== endpointGeneration || !closingIntent(record)) return;
477
+ await this.#driveClose(record);
478
+ }
479
+
480
+ async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
481
+ const intentKey = this.#intentKey(sessionId);
482
+ const now = this.#now();
483
+ await this.#store.transact(intentKey, current => {
484
+ if (
485
+ current?.state !== "creating" ||
486
+ current.sessionId !== sessionId ||
487
+ current.endpointGeneration !== endpointGeneration
488
+ )
489
+ return current;
490
+ return normalizeDiscordConversation({
491
+ ...current,
492
+ generation: current.generation + 1,
493
+ state: "closed",
494
+ createNonce: undefined,
495
+ createOwner: undefined,
496
+ createLeaseExpiresAt: undefined,
497
+ closedAt: now,
498
+ updatedAt: now,
499
+ });
500
+ });
501
+ const record = await this.#bySession(sessionId);
502
+ if (!record?.threadId || record.endpointGeneration !== endpointGeneration) return;
503
+ for (const receipt of record.inboundDispatches ?? [])
504
+ await this.#terminalizeInbound(record, receipt, "stale_binding");
505
+ const key = discordConversationKey({
506
+ appId: record.appId,
507
+ guildId: record.guildId,
508
+ parentChannelId: record.parentChannelId,
509
+ threadId: record.threadId,
510
+ });
511
+ await this.#store.transact(key, current =>
512
+ current?.sessionId === sessionId && current.endpointGeneration === endpointGeneration
513
+ ? withoutClosingIntent(current, now)
514
+ : current,
515
+ );
516
+ }
517
+
349
518
  async archive(sessionId: string): Promise<void> {
350
519
  const running = this.#archives.get(sessionId);
351
520
  if (running) return await running;
@@ -361,19 +530,34 @@ export class DiscordNotificationDaemon {
361
530
  async #archive(sessionId: string): Promise<void> {
362
531
  const record = await this.#bySession(sessionId);
363
532
  if (!record?.threadId || record.state !== "active") return;
364
- await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!);
533
+ await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!, record.attachmentAuthorityId);
365
534
  const archiving = await this.#beginArchive(record);
366
535
  const occurrenceId = archiving.archiveOccurrenceId;
367
536
  if (!occurrenceId || !archiving.archiveEffectId) throw new Error("Discord archive occurrence is unavailable");
368
537
  await this.#threadEffect(archiving.archiveEffectId, archiving, "archive", false, false, occurrenceId);
369
- await this.#requireLiveBinding(archiving.sessionId!, archiving.endpointGeneration!);
538
+ await this.#requireLiveBinding(
539
+ archiving.sessionId!,
540
+ archiving.endpointGeneration!,
541
+ archiving.attachmentAuthorityId,
542
+ );
370
543
  await this.#completeArchive(archiving, occurrenceId);
371
544
  }
372
545
 
373
- async resume(sessionId: string, endpointGeneration: number): Promise<DiscordConversation | undefined> {
546
+ async resume(
547
+ sessionId: string,
548
+ endpointGeneration: number,
549
+ attachmentAuthorityId?: string,
550
+ ): Promise<DiscordConversation | undefined> {
551
+ return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId));
552
+ }
553
+ async #resumeAdmission(
554
+ sessionId: string,
555
+ endpointGeneration: number,
556
+ attachmentAuthorityId?: string,
557
+ ): Promise<DiscordConversation | undefined> {
374
558
  const running = this.#resumes.get(sessionId);
375
559
  if (running) return await running;
376
- const task = this.#resume(sessionId, endpointGeneration);
560
+ const task = this.#resume(sessionId, endpointGeneration, attachmentAuthorityId);
377
561
  this.#resumes.set(sessionId, task);
378
562
  try {
379
563
  return await task;
@@ -382,20 +566,29 @@ export class DiscordNotificationDaemon {
382
566
  }
383
567
  }
384
568
 
385
- async #resume(sessionId: string, endpointGeneration: number): Promise<DiscordConversation | undefined> {
569
+ async #resume(
570
+ sessionId: string,
571
+ endpointGeneration: number,
572
+ attachmentAuthorityId?: string,
573
+ ): Promise<DiscordConversation | undefined> {
386
574
  const record = await this.#bySession(sessionId);
387
575
  if (!record?.threadId || record.state === "closed") return undefined;
576
+ if (attachmentAuthorityId !== undefined && record.attachmentAuthorityId !== attachmentAuthorityId) {
577
+ await this.retireAttachment(sessionId, record.endpointGeneration ?? endpointGeneration);
578
+ return undefined;
579
+ }
388
580
  if (closingIntent(record)) {
389
581
  await this.#driveClose(record);
390
582
  return undefined;
391
583
  }
392
- await this.#requireLiveBinding(sessionId, endpointGeneration);
584
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
393
585
  const resumeOccurrenceId = randomUUID();
394
586
  const effectIncarnationId = backfilledEffectIncarnationId(record);
395
587
  const resuming = await this.#replace(record, {
396
588
  ...record,
397
589
  state: "resuming",
398
590
  endpointGeneration,
591
+ attachmentAuthorityId,
399
592
  effectIncarnationId,
400
593
  resumeOccurrenceId,
401
594
  resumeEffectId: `unarchive:${record.threadId}:${effectIncarnationId}:${resumeOccurrenceId}`,
@@ -407,10 +600,10 @@ export class DiscordNotificationDaemon {
407
600
  const occurrenceId = resuming.resumeOccurrenceId;
408
601
  if (!occurrenceId || !resuming.resumeEffectId) throw new Error("Discord resume occurrence is unavailable");
409
602
  await this.#threadEffect(resuming.resumeEffectId, resuming, "unarchive", false, false, occurrenceId);
410
- await this.#requireLiveBinding(sessionId, endpointGeneration);
603
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
411
604
  return await this.#completeResume(resuming, occurrenceId);
412
605
  } catch {
413
- await this.#requireLiveBinding(sessionId, endpointGeneration);
606
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
414
607
  const superseded = await this.#replace(resuming, {
415
608
  ...resuming,
416
609
  state: "archived",
@@ -421,8 +614,14 @@ export class DiscordNotificationDaemon {
421
614
  pendingActionEffectId: undefined,
422
615
  });
423
616
 
424
- const replacement = await this.#create(sessionId, endpointGeneration, `resume-${randomUUID()}`);
425
- await this.#requireLiveBinding(sessionId, endpointGeneration);
617
+ const replacement = await this.#create(
618
+ sessionId,
619
+ endpointGeneration,
620
+ `resume-${randomUUID()}`,
621
+ undefined,
622
+ attachmentAuthorityId,
623
+ );
624
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
426
625
  await this.#replace(superseded, { ...superseded, supersededByThreadId: replacement.threadId });
427
626
  return replacement;
428
627
  }
@@ -451,15 +650,15 @@ export class DiscordNotificationDaemon {
451
650
  this.#inflightInbound.add(claim.receipt.effectId);
452
651
  try {
453
652
  // Interaction callbacks have a short provider deadline. The mapping and
454
- // journal claim are sufficient to acknowledge; endpoint discovery waits.
455
- const endpoint = event.interaction
653
+ // journal claim are sufficient to acknowledge; attachment discovery waits.
654
+ const attachment = event.interaction
456
655
  ? null
457
- : await this.#resolveEndpoint(record.sessionId, record.endpointGeneration);
458
- if (!event.interaction && !this.#matches(record, endpoint)) {
656
+ : await this.#resolveAttachment(record.sessionId, record.endpointGeneration);
657
+ if (!event.interaction && !this.#matches(record, attachment)) {
459
658
  await this.#fail(event.threadId);
460
659
  return;
461
660
  }
462
- await this.#dispatchInbound(record, endpoint, claim.receipt, event.interaction, claim.liveCallbackEffect);
661
+ await this.#dispatchInbound(record, attachment, claim.receipt, event.interaction, claim.liveCallbackEffect);
463
662
  } finally {
464
663
  this.#inflightInbound.delete(claim.receipt.effectId);
465
664
  await this.#scheduleLeaseRecovery();
@@ -495,6 +694,7 @@ export class DiscordNotificationDaemon {
495
694
  parentId: event.parentId,
496
695
  threadId: event.threadId,
497
696
  eventId: event.id,
697
+ ...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
498
698
  ...(event.interaction ? { interactionId: event.interaction.id } : {}),
499
699
  kind: command ? "command" : "action",
500
700
  ...(!command ? { actionId: route!.actionId, actionNonce: route!.actionNonce } : {}),
@@ -570,6 +770,9 @@ export class DiscordNotificationDaemon {
570
770
  eventId: event.id,
571
771
  kind: "command",
572
772
  endpointGeneration: record.endpointGeneration!,
773
+ ...(record.attachmentAuthorityId === undefined
774
+ ? {}
775
+ : { attachmentAuthorityId: record.attachmentAuthorityId }),
573
776
  effectId,
574
777
  idempotencyKey,
575
778
  }
@@ -581,6 +784,9 @@ export class DiscordNotificationDaemon {
581
784
  actionId: route!.actionId,
582
785
  actionNonce: route!.actionNonce,
583
786
  endpointGeneration: record.endpointGeneration!,
787
+ ...(record.attachmentAuthorityId === undefined
788
+ ? {}
789
+ : { attachmentAuthorityId: record.attachmentAuthorityId }),
584
790
  effectId,
585
791
  idempotencyKey,
586
792
  };
@@ -609,7 +815,7 @@ export class DiscordNotificationDaemon {
609
815
  }
610
816
  async #dispatchInbound(
611
817
  record: DiscordConversation,
612
- initialEndpoint: DiscordEndpointBinding | null,
818
+ initialAttachment: SessionAttachment | null,
613
819
  receipt: DiscordInboundDispatchReceipt,
614
820
  interaction?: DiscordInboundEvent["interaction"],
615
821
  liveCallbackEffect?: ChatEffect<DiscordInboundEffectPayload>,
@@ -711,8 +917,9 @@ export class DiscordNotificationDaemon {
711
917
  return;
712
918
  }
713
919
  record = dispatchable;
714
- const endpoint = initialEndpoint ?? (await this.#resolveEndpoint(record.sessionId!, receipt.endpointGeneration));
715
- if (!this.#matches(record, endpoint) || receipt.endpointGeneration !== endpoint.generation) {
920
+ const attachment =
921
+ initialAttachment ?? (await this.#resolveAttachment(record.sessionId!, receipt.endpointGeneration));
922
+ if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
716
923
  await this.#rescheduleAfterEffectTransition(
717
924
  this.#effects.record(effect.id, lease, "accepted", {
718
925
  status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
@@ -750,7 +957,7 @@ export class DiscordNotificationDaemon {
750
957
  );
751
958
  try {
752
959
  if (!(await renew())) return;
753
- if (!this.#matches(record, endpoint) || receipt.endpointGeneration !== endpoint.generation) {
960
+ if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
754
961
  await this.#rescheduleAfterEffectTransition(
755
962
  this.#effects.record(effect.id, lease, "accepted", {
756
963
  status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
@@ -767,7 +974,7 @@ export class DiscordNotificationDaemon {
767
974
  return;
768
975
  }
769
976
  record = beforeSend;
770
- if (!this.#matches(record, endpoint) || receipt.endpointGeneration !== endpoint.generation) {
977
+ if (!this.#matches(record, attachment) || receipt.endpointGeneration !== attachment.generation) {
771
978
  await this.#rescheduleAfterEffectTransition(
772
979
  this.#effects.record(effect.id, lease, "accepted", {
773
980
  status: this.#inboundAcceptedStatus(effect, "pre_send_binding_changed"),
@@ -780,10 +987,10 @@ export class DiscordNotificationDaemon {
780
987
  await this.options.onCommand?.(
781
988
  record.sessionId!,
782
989
  effect.payload.content,
783
- endpoint,
990
+ attachment,
784
991
  effect.payload.idempotencyKey,
785
992
  );
786
- else await endpoint.send(effect.payload);
993
+ else await attachment.send(effect.payload);
787
994
  if (!leaseLost && (await renew()) && (await this.#effects.record(effect.id, lease, "terminal")))
788
995
  await this.#finishInbound(record, receipt);
789
996
  } catch (error) {
@@ -824,8 +1031,8 @@ export class DiscordNotificationDaemon {
824
1031
  const dispatched = new Set<string>();
825
1032
  for (const record of Object.values((await this.#store.load()).conversations)) {
826
1033
  if (!record.threadId || !record.sessionId || record.state !== "active") continue;
827
- let endpoint: DiscordEndpointBinding | null = null;
828
- let endpointResolved = false;
1034
+ let attachment: SessionAttachment | null = null;
1035
+ let attachmentResolved = false;
829
1036
  for (const [index, receipt] of (record.inboundDispatches ?? []).entries()) {
830
1037
  // Never race a foreground handleInbound() dispatch: while an effect is
831
1038
  // in-flight it briefly unowns itself (record "accepted"/"deferred" before
@@ -864,16 +1071,16 @@ export class DiscordNotificationDaemon {
864
1071
  dispatched.add(receipt.effectId);
865
1072
  continue;
866
1073
  }
867
- if (!endpointResolved) {
868
- endpoint = await this.#resolveEndpoint(record.sessionId, receipt.endpointGeneration);
869
- endpointResolved = true;
1074
+ if (!attachmentResolved) {
1075
+ attachment = await this.#resolveAttachment(record.sessionId, receipt.endpointGeneration);
1076
+ attachmentResolved = true;
870
1077
  }
871
- if (!this.#matches(record, endpoint) || endpoint.generation !== receipt.endpointGeneration) {
1078
+ if (!this.#matches(record, attachment) || attachment.generation !== receipt.endpointGeneration) {
872
1079
  await this.#terminalizeInbound(record, receipt, "stale_binding");
873
1080
  continue;
874
1081
  }
875
1082
  dispatched.add(receipt.effectId);
876
- await this.#dispatchInbound(record, endpoint, receipt);
1083
+ await this.#dispatchInbound(record, attachment, receipt);
877
1084
  }
878
1085
  }
879
1086
 
@@ -901,12 +1108,12 @@ export class DiscordNotificationDaemon {
901
1108
  routing,
902
1109
  );
903
1110
  if (!adopted) continue;
904
- const endpoint = await this.#resolveEndpoint(adopted.record.sessionId!, effect.endpointGeneration);
905
- if (!this.#matches(adopted.record, endpoint) || endpoint.generation !== effect.endpointGeneration) {
1111
+ const attachment = await this.#resolveAttachment(adopted.record.sessionId!, effect.endpointGeneration);
1112
+ if (!this.#matches(adopted.record, attachment) || attachment.generation !== effect.endpointGeneration) {
906
1113
  await this.#terminalizeInbound(adopted.record, adopted.receipt, "stale_binding");
907
1114
  continue;
908
1115
  }
909
- await this.#dispatchInbound(adopted.record, endpoint, adopted.receipt);
1116
+ await this.#dispatchInbound(adopted.record, attachment, adopted.receipt);
910
1117
  }
911
1118
  return recoveryFailed;
912
1119
  }
@@ -1003,6 +1210,7 @@ export class DiscordNotificationDaemon {
1003
1210
  left.actionId === right.actionId &&
1004
1211
  left.actionNonce === right.actionNonce &&
1005
1212
  left.endpointGeneration === right.endpointGeneration &&
1213
+ left.attachmentAuthorityId === right.attachmentAuthorityId &&
1006
1214
  left.effectId === right.effectId &&
1007
1215
  left.idempotencyKey === right.idempotencyKey
1008
1216
  );
@@ -1083,6 +1291,7 @@ export class DiscordNotificationDaemon {
1083
1291
  current?.state !== "active" ||
1084
1292
  current.sessionId !== record.sessionId ||
1085
1293
  current.endpointGeneration !== receipt.endpointGeneration ||
1294
+ current.attachmentAuthorityId !== receipt.attachmentAuthorityId ||
1086
1295
  !claimed
1087
1296
  )
1088
1297
  return undefined;
@@ -1106,6 +1315,9 @@ export class DiscordNotificationDaemon {
1106
1315
  eventId: routing.eventId,
1107
1316
  kind: "command",
1108
1317
  endpointGeneration,
1318
+ ...(routing.attachmentAuthorityId === undefined
1319
+ ? {}
1320
+ : { attachmentAuthorityId: routing.attachmentAuthorityId }),
1109
1321
  effectId,
1110
1322
  idempotencyKey,
1111
1323
  }
@@ -1117,6 +1329,9 @@ export class DiscordNotificationDaemon {
1117
1329
  actionId: routing.actionId!,
1118
1330
  actionNonce: routing.actionNonce!,
1119
1331
  endpointGeneration,
1332
+ ...(routing.attachmentAuthorityId === undefined
1333
+ ? {}
1334
+ : { attachmentAuthorityId: routing.attachmentAuthorityId }),
1120
1335
  effectId,
1121
1336
  idempotencyKey,
1122
1337
  };
@@ -1176,23 +1391,42 @@ export class DiscordNotificationDaemon {
1176
1391
  }
1177
1392
  }
1178
1393
 
1179
- #matches(record: DiscordConversation, endpoint: DiscordEndpointBinding | null): endpoint is DiscordEndpointBinding {
1180
- if (!endpoint?.isCurrent()) return false;
1181
- return record.state === "active" && record.endpointGeneration === endpoint.generation;
1394
+ #matches(record: DiscordConversation, attachment: SessionAttachment | null): attachment is SessionAttachment {
1395
+ if (!attachment?.isCurrent()) return false;
1396
+ return (
1397
+ record.state === "active" &&
1398
+ record.endpointGeneration === attachment.generation &&
1399
+ record.attachmentAuthorityId === attachment.authorityId
1400
+ );
1182
1401
  }
1183
- async #bindingCurrent(sessionId: string, endpointGeneration: number): Promise<boolean> {
1402
+ async #bindingCurrent(
1403
+ sessionId: string,
1404
+ endpointGeneration: number,
1405
+ attachmentAuthorityId?: string,
1406
+ ): Promise<boolean> {
1184
1407
  try {
1185
- const endpoint = await this.#resolveEndpoint(sessionId, endpointGeneration);
1186
- return !!endpoint && endpoint.isCurrent() && endpoint.generation === endpointGeneration;
1408
+ const attachment = await this.#resolveAttachment(sessionId, endpointGeneration);
1409
+ return (
1410
+ !!attachment &&
1411
+ attachment.isCurrent() &&
1412
+ attachment.generation === endpointGeneration &&
1413
+ attachment.authorityId === attachmentAuthorityId
1414
+ );
1187
1415
  } catch {
1188
1416
  return false;
1189
1417
  }
1190
1418
  }
1191
- async #requireLiveBinding(sessionId: string, endpointGeneration: number): Promise<void> {
1192
- if (!(await this.#bindingCurrent(sessionId, endpointGeneration))) throw new DiscordEndpointBindingError();
1419
+ async #requireLiveBinding(
1420
+ sessionId: string,
1421
+ endpointGeneration: number,
1422
+ attachmentAuthorityId?: string,
1423
+ ): Promise<void> {
1424
+ if (!(await this.#bindingCurrent(sessionId, endpointGeneration, attachmentAuthorityId)))
1425
+ throw new DiscordAttachmentBindingError();
1193
1426
  }
1194
1427
  #isDefiniteSdkPreSendFailure(error: unknown): boolean {
1195
- if (error instanceof DiscordEndpointBindingError) return true;
1428
+ if (error instanceof DiscordAttachmentBindingError) return true;
1429
+ if (error instanceof SessionRouterError) return error.phase === "pre_send";
1196
1430
  if (error instanceof SdkClientError) return error.code === "connection_closed";
1197
1431
  return (
1198
1432
  error instanceof Error &&
@@ -1215,9 +1449,26 @@ export class DiscordNotificationDaemon {
1215
1449
  throw new Error("Discord thread is closing");
1216
1450
  }
1217
1451
  if (existing?.state === "active" && existing.threadId) {
1218
- if (existing.endpointGeneration === input.endpointGeneration) return existing;
1219
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
1220
- return await this.#replace(existing, { ...existing, endpointGeneration: input.endpointGeneration });
1452
+ if (
1453
+ existing.endpointGeneration === input.endpointGeneration &&
1454
+ (input.attachmentAuthorityId === undefined ||
1455
+ existing.attachmentAuthorityId === input.attachmentAuthorityId)
1456
+ )
1457
+ return existing;
1458
+ if (
1459
+ existing.endpointGeneration === input.endpointGeneration &&
1460
+ input.attachmentAuthorityId !== undefined &&
1461
+ existing.attachmentAuthorityId !== input.attachmentAuthorityId
1462
+ ) {
1463
+ await this.retireAttachment(input.sessionId, input.endpointGeneration);
1464
+ return await this.#ensureConversation(input);
1465
+ }
1466
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
1467
+ return await this.#replace(existing, {
1468
+ ...existing,
1469
+ endpointGeneration: input.endpointGeneration,
1470
+ attachmentAuthorityId: input.attachmentAuthorityId,
1471
+ });
1221
1472
  }
1222
1473
  const inFlight = this.#creates.get(input.sessionId);
1223
1474
  if (inFlight) {
@@ -1225,14 +1476,28 @@ export class DiscordNotificationDaemon {
1225
1476
  try {
1226
1477
  created = await inFlight;
1227
1478
  } catch {
1228
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
1479
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
1229
1480
  return await this.#ensureConversation(input);
1230
1481
  }
1231
- if (created.endpointGeneration === input.endpointGeneration) return created;
1232
- await this.#requireLiveBinding(input.sessionId, input.endpointGeneration);
1233
- return await this.#replace(created, { ...created, endpointGeneration: input.endpointGeneration });
1482
+ if (
1483
+ created.endpointGeneration === input.endpointGeneration &&
1484
+ (input.attachmentAuthorityId === undefined || created.attachmentAuthorityId === input.attachmentAuthorityId)
1485
+ )
1486
+ return created;
1487
+ await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
1488
+ return await this.#replace(created, {
1489
+ ...created,
1490
+ endpointGeneration: input.endpointGeneration,
1491
+ attachmentAuthorityId: input.attachmentAuthorityId,
1492
+ });
1234
1493
  }
1235
- const pending = this.#create(input.sessionId, input.endpointGeneration, randomUUID(), input.threadName);
1494
+ const pending = this.#create(
1495
+ input.sessionId,
1496
+ input.endpointGeneration,
1497
+ randomUUID(),
1498
+ input.threadName,
1499
+ input.attachmentAuthorityId,
1500
+ );
1236
1501
  this.#creates.set(input.sessionId, pending);
1237
1502
  try {
1238
1503
  return await pending;
@@ -1246,6 +1511,7 @@ export class DiscordNotificationDaemon {
1246
1511
  endpointGeneration: number,
1247
1512
  nonce: string,
1248
1513
  name = "GJC session",
1514
+ attachmentAuthorityId?: string,
1249
1515
  ): Promise<DiscordConversation> {
1250
1516
  const intentKey = this.#intentKey(sessionId);
1251
1517
  const owner = randomUUID();
@@ -1257,12 +1523,24 @@ export class DiscordNotificationDaemon {
1257
1523
  throw new Error("Discord thread is closing");
1258
1524
  }
1259
1525
  if (active?.state === "active" && active.threadId) {
1260
- if (active.endpointGeneration === endpointGeneration) return active;
1261
- await this.#requireLiveBinding(sessionId, endpointGeneration);
1262
- return await this.#replace(active, { ...active, endpointGeneration });
1526
+ if (
1527
+ active.endpointGeneration === endpointGeneration &&
1528
+ (attachmentAuthorityId === undefined || active.attachmentAuthorityId === attachmentAuthorityId)
1529
+ )
1530
+ return active;
1531
+ if (
1532
+ active.endpointGeneration === endpointGeneration &&
1533
+ attachmentAuthorityId !== undefined &&
1534
+ active.attachmentAuthorityId !== attachmentAuthorityId
1535
+ ) {
1536
+ await this.retireAttachment(sessionId, endpointGeneration);
1537
+ continue;
1538
+ }
1539
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
1540
+ return await this.#replace(active, { ...active, endpointGeneration, attachmentAuthorityId });
1263
1541
  }
1264
1542
  const now = this.#now();
1265
- await this.#requireLiveBinding(sessionId, endpointGeneration);
1543
+ await this.#requireLiveBinding(sessionId, endpointGeneration, attachmentAuthorityId);
1266
1544
  intent = await this.#store.transact(intentKey, old => {
1267
1545
  if (old?.state === "creating" && old.createOwner && (old.createLeaseExpiresAt ?? 0) > now) return old;
1268
1546
  return {
@@ -1273,6 +1551,7 @@ export class DiscordNotificationDaemon {
1273
1551
  parentChannelId: this.options.parentChannelId,
1274
1552
  sessionId,
1275
1553
  endpointGeneration,
1554
+ attachmentAuthorityId,
1276
1555
  createNonce: old?.createNonce ?? nonce,
1277
1556
  createOwner: owner,
1278
1557
  createLeaseExpiresAt: now + 60_000,
@@ -1288,13 +1567,13 @@ export class DiscordNotificationDaemon {
1288
1567
  const active = await this.#bySession(sessionId);
1289
1568
  if (active?.state === "active" && active.threadId) {
1290
1569
  if (active.endpointGeneration === endpointGeneration) return active;
1291
- await this.#requireLiveBinding(sessionId, endpointGeneration);
1570
+ await this.#requireLiveBinding(sessionId, endpointGeneration, active.attachmentAuthorityId);
1292
1571
  return await this.#replace(active, { ...active, endpointGeneration });
1293
1572
  }
1294
1573
  let thread: DiscordThread | null;
1295
1574
  try {
1296
1575
  thread = await this.#withCreateIntentLease(intent, () => this.#createThreadEffect(intent, name));
1297
- await this.#requireLiveBinding(sessionId, endpointGeneration);
1576
+ await this.#requireLiveBinding(sessionId, endpointGeneration, intent.attachmentAuthorityId);
1298
1577
  } catch (error) {
1299
1578
  await this.#abandonCreator(intentKey, intent);
1300
1579
  throw error;
@@ -1304,19 +1583,31 @@ export class DiscordNotificationDaemon {
1304
1583
  currentIntent?.state !== "creating" ||
1305
1584
  currentIntent.createOwner !== intent.createOwner ||
1306
1585
  currentIntent.generation !== intent.generation ||
1586
+ currentIntent.attachmentAuthorityId !== intent.attachmentAuthorityId ||
1307
1587
  (currentIntent.createLeaseExpiresAt ?? 0) <= this.#now()
1308
1588
  ) {
1309
1589
  throw new Error("Discord create intent lost its fence before mapping commit");
1310
1590
  }
1311
- await this.#requireLiveBinding(sessionId, endpointGeneration);
1591
+ await this.#requireLiveBinding(sessionId, endpointGeneration, intent.attachmentAuthorityId);
1312
1592
  const key = discordConversationKey({
1313
1593
  appId: intent.appId,
1314
1594
  guildId: intent.guildId,
1315
1595
  parentChannelId: intent.parentChannelId,
1316
1596
  threadId: thread.id,
1317
1597
  });
1318
- const record = await this.#store.transact(key, old =>
1319
- normalizeDiscordConversation({
1598
+ let committed = false;
1599
+ const record = await this.#store.transactWithSnapshot(key, (old, conversations) => {
1600
+ const current = conversations[intentKey];
1601
+ if (
1602
+ current?.state !== "creating" ||
1603
+ current.createOwner !== intent.createOwner ||
1604
+ current.generation !== intent.generation ||
1605
+ current.attachmentAuthorityId !== intent.attachmentAuthorityId ||
1606
+ (current.createLeaseExpiresAt ?? 0) <= this.#now()
1607
+ )
1608
+ return old;
1609
+ committed = true;
1610
+ return normalizeDiscordConversation({
1320
1611
  generation: (old?.generation ?? 0) + 1,
1321
1612
  state: "active",
1322
1613
  appId: intent.appId,
@@ -1325,15 +1616,18 @@ export class DiscordNotificationDaemon {
1325
1616
  threadId: thread.id,
1326
1617
  sessionId,
1327
1618
  endpointGeneration,
1619
+ attachmentAuthorityId: intent.attachmentAuthorityId,
1328
1620
  createNonce: intent.createNonce,
1329
1621
  effectIncarnationId: old?.effectIncarnationId ?? intent.effectIncarnationId ?? randomUUID(),
1330
1622
  updatedAt: this.#now(),
1331
- seenEventIds: old?.seenEventIds ?? [],
1332
- seenInteractionIds: old?.seenInteractionIds ?? [],
1333
- inboundDispatches: old?.inboundDispatches,
1334
- }),
1335
- );
1336
- if (!record) throw new Error("Unable to persist Discord thread mapping");
1623
+ seenEventIds: old?.attachmentAuthorityId === intent.attachmentAuthorityId ? (old?.seenEventIds ?? []) : [],
1624
+ seenInteractionIds:
1625
+ old?.attachmentAuthorityId === intent.attachmentAuthorityId ? (old?.seenInteractionIds ?? []) : [],
1626
+ inboundDispatches:
1627
+ old?.attachmentAuthorityId === intent.attachmentAuthorityId ? old?.inboundDispatches : undefined,
1628
+ });
1629
+ });
1630
+ if (!committed || !record) throw new Error("Discord create intent lost its fence before mapping commit");
1337
1631
  await this.#store.delete(intentKey, intent.generation);
1338
1632
  return record;
1339
1633
  }
@@ -1373,9 +1667,10 @@ export class DiscordNotificationDaemon {
1373
1667
  if (!record.threadId || !intent) throw new Error("Discord close intent is unavailable.");
1374
1668
  return `close-archive:${record.threadId}:${intent.nonce}`;
1375
1669
  }
1376
- async #markClosing(sessionId: string): Promise<DiscordConversation | undefined> {
1670
+ async #markClosing(sessionId: string, endpointGeneration?: number): Promise<DiscordConversation | undefined> {
1377
1671
  const record = await this.#bySession(sessionId);
1378
1672
  if (!record?.threadId || record.state === "closed") return undefined;
1673
+ if (endpointGeneration !== undefined && record.endpointGeneration !== endpointGeneration) return undefined;
1379
1674
  const key = discordConversationKey({
1380
1675
  appId: record.appId,
1381
1676
  guildId: record.guildId,
@@ -1384,7 +1679,13 @@ export class DiscordNotificationDaemon {
1384
1679
  });
1385
1680
  let closing: DiscordConversation | undefined;
1386
1681
  await this.#store.transact(key, current => {
1387
- if (!current || current.sessionId !== sessionId || !current.threadId || current.state === "closed")
1682
+ if (
1683
+ !current ||
1684
+ current.sessionId !== sessionId ||
1685
+ !current.threadId ||
1686
+ current.state === "closed" ||
1687
+ (endpointGeneration !== undefined && current.endpointGeneration !== endpointGeneration)
1688
+ )
1388
1689
  return current;
1389
1690
  if (closingIntent(current)) {
1390
1691
  closing = current;
@@ -1574,8 +1875,22 @@ export class DiscordNotificationDaemon {
1574
1875
  let lost = false;
1575
1876
  let renewal: Promise<boolean> | undefined;
1576
1877
  let expectedGeneration = intent.generation;
1878
+ const workGeneration = this.#workGeneration;
1879
+ const abandonCurrent = async (): Promise<void> => {
1880
+ await this.#abandonCreator(this.#intentKey(intent.sessionId!), intent);
1881
+ };
1882
+ const invalidate = async (): Promise<void> => {
1883
+ lost = true;
1884
+ const activeRenewal = renewal;
1885
+ if (activeRenewal) await activeRenewal.catch(() => false);
1886
+ await abandonCurrent();
1887
+ };
1888
+ this.#workInvalidators.add(invalidate);
1577
1889
  const renew = async (): Promise<boolean> => {
1578
- if (lost) return false;
1890
+ if (lost || workGeneration !== this.#workGeneration) {
1891
+ await invalidate();
1892
+ return false;
1893
+ }
1579
1894
  if (renewal) return await renewal;
1580
1895
  const currentRenewal = (async (): Promise<boolean> => {
1581
1896
  const now = this.#now();
@@ -1594,6 +1909,15 @@ export class DiscordNotificationDaemon {
1594
1909
  updatedAt: now,
1595
1910
  };
1596
1911
  });
1912
+ if (workGeneration !== this.#workGeneration) {
1913
+ if (current?.state === "creating" && current.createOwner === intent.createOwner) {
1914
+ intent.generation = current.generation;
1915
+ intent.createLeaseExpiresAt = current.createLeaseExpiresAt;
1916
+ }
1917
+ lost = true;
1918
+ await abandonCurrent();
1919
+ return false;
1920
+ }
1597
1921
  if (
1598
1922
  current?.state !== "creating" ||
1599
1923
  current.createOwner !== intent.createOwner ||
@@ -1628,6 +1952,7 @@ export class DiscordNotificationDaemon {
1628
1952
  return result;
1629
1953
  } finally {
1630
1954
  clearInterval(timer);
1955
+ this.#workInvalidators.delete(invalidate);
1631
1956
  }
1632
1957
  }
1633
1958
  async #clearPending(record: DiscordConversation, actionId: string, actionNonce: string): Promise<void> {
@@ -1658,7 +1983,7 @@ export class DiscordNotificationDaemon {
1658
1983
  parentChannelId: record.parentChannelId,
1659
1984
  threadId: record.threadId!,
1660
1985
  });
1661
- await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!);
1986
+ await this.#requireLiveBinding(record.sessionId!, record.endpointGeneration!, record.attachmentAuthorityId);
1662
1987
  const result = await this.#store.transact(key, current => {
1663
1988
  if (
1664
1989
  current?.state !== "active" ||
@@ -1704,6 +2029,7 @@ export class DiscordNotificationDaemon {
1704
2029
  revalidate: () => boolean | Promise<boolean>,
1705
2030
  terminalizeStaleBeforeProvider = false,
1706
2031
  ): Promise<ChatEffectReceipt> {
2032
+ const workGeneration = this.#workGeneration;
1707
2033
  const claimed = await this.#rescheduleAfterEffectTransition(
1708
2034
  this.#effects.enqueueAndClaim<TPayload>(
1709
2035
  { id, kind, transport: "discord", sessionId, endpointGeneration, payload },
@@ -1723,7 +2049,7 @@ export class DiscordNotificationDaemon {
1723
2049
  if (initial?.state === "terminal") {
1724
2050
  if (!initial.receipt) throw new Error(`Discord effect ${id} has no receipt`);
1725
2051
  if (terminalizeStaleBeforeProvider && initial.receipt.status === "stale_noop")
1726
- throw new DiscordEndpointBindingError("Discord thread effect is no longer current.");
2052
+ throw new DiscordAttachmentBindingError("Discord thread effect is no longer current.");
1727
2053
  return initial.receipt;
1728
2054
  }
1729
2055
  const reclaimed = await this.#rescheduleAfterEffectTransition(
@@ -1733,22 +2059,38 @@ export class DiscordNotificationDaemon {
1733
2059
  effect = reclaimed;
1734
2060
  }
1735
2061
  const lease: ChatEffectLease = { owner: this.#providerOwner, epoch: effect.epoch };
2062
+ if (workGeneration !== this.#workGeneration) {
2063
+ await this.#rescheduleAfterEffectTransition(
2064
+ this.#effects.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
2065
+ );
2066
+ throw new Error(`Discord effect ${id} was admitted after shutdown drain expiry`);
2067
+ }
1736
2068
  let renewalLost = false;
1737
2069
  let revalidationFailed = false;
1738
2070
  let providerEffectStarted = false;
1739
2071
  let renewal: Promise<boolean> | undefined;
1740
- const renew = async (): Promise<boolean> => {
1741
- if (renewalLost) return false;
2072
+ const invalidate = async (): Promise<void> => {
2073
+ renewalLost = true;
2074
+ await this.#rescheduleAfterEffectTransition(
2075
+ this.#effects.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
2076
+ );
2077
+ };
2078
+ this.#workInvalidators.add(invalidate);
2079
+ const renewLease = async (): Promise<boolean> => {
2080
+ if (renewalLost || workGeneration !== this.#workGeneration) {
2081
+ renewalLost = true;
2082
+ return false;
2083
+ }
1742
2084
  if (renewal) return await renewal;
1743
2085
  const currentRenewal = (async (): Promise<boolean> => {
1744
2086
  const renewed = await this.#rescheduleAfterEffectTransition(
1745
2087
  this.#effects.renew(id, lease, this.#providerLeaseMs),
1746
2088
  );
1747
- if (!renewed) renewalLost = true;
1748
- else if (!(await revalidate())) {
1749
- revalidationFailed = true;
1750
- renewalLost = true;
2089
+ if (workGeneration !== this.#workGeneration) {
2090
+ await invalidate();
2091
+ return false;
1751
2092
  }
2093
+ if (!renewed) renewalLost = true;
1752
2094
  return !renewalLost;
1753
2095
  })();
1754
2096
  renewal = currentRenewal;
@@ -1760,19 +2102,24 @@ export class DiscordNotificationDaemon {
1760
2102
  };
1761
2103
  const timer = setInterval(
1762
2104
  () => {
1763
- void renew().catch(() => {});
2105
+ void renewLease().catch(() => {});
1764
2106
  },
1765
2107
  Math.max(1, Math.floor(this.#providerLeaseMs / 3)),
1766
2108
  );
1767
2109
  const ensure = async (): Promise<void> => {
1768
- if (!(await renew())) throw new Error(`Discord effect ${id} lost its fence`);
2110
+ if (!(await renewLease())) throw new Error(`Discord effect ${id} lost its fence`);
2111
+ if (!(await revalidate())) {
2112
+ revalidationFailed = true;
2113
+ renewalLost = true;
2114
+ throw new Error(`Discord effect ${id} lost its fence`);
2115
+ }
1769
2116
  };
1770
2117
  try {
1771
2118
  await ensure();
1772
2119
  const receipt = await operation(ensure, () => {
1773
2120
  providerEffectStarted = true;
1774
2121
  });
1775
- await ensure();
2122
+ if (!(await renewLease())) throw new Error(`Discord effect ${id} lost its fence`);
1776
2123
  const committed = await this.#effects.record(id, lease, "terminal", receipt);
1777
2124
  if (!committed) throw new Error(`Discord effect ${id} lost its fence before commit`);
1778
2125
  return receipt;
@@ -1788,6 +2135,7 @@ export class DiscordNotificationDaemon {
1788
2135
  throw error;
1789
2136
  } finally {
1790
2137
  clearInterval(timer);
2138
+ this.#workInvalidators.delete(invalidate);
1791
2139
  }
1792
2140
  }
1793
2141
 
@@ -1806,13 +2154,17 @@ export class DiscordNotificationDaemon {
1806
2154
  content: string;
1807
2155
  nonce: string;
1808
2156
  components?: DiscordMessageComponent[];
2157
+ attachmentAuthorityId?: string;
1809
2158
  }>(id);
1810
2159
  const payload = existingEffect?.payload ?? {
1811
2160
  threadId: record.threadId!,
1812
2161
  content,
1813
2162
  nonce,
2163
+ ...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
1814
2164
  ...(components ? { components } : {}),
1815
2165
  };
2166
+ if (payload.attachmentAuthorityId !== record.attachmentAuthorityId)
2167
+ throw new DiscordAttachmentBindingError("Discord provider effect belongs to another attachment.");
1816
2168
  await this.#runEffect(
1817
2169
  id,
1818
2170
  "post-message",
@@ -1853,10 +2205,17 @@ export class DiscordNotificationDaemon {
1853
2205
  (allowInactive || current.state === "active") &&
1854
2206
  current.generation === record.generation &&
1855
2207
  current.endpointGeneration === record.endpointGeneration &&
2208
+ current.attachmentAuthorityId === payload.attachmentAuthorityId &&
1856
2209
  (!actionPublication || current.pendingActionEffectId === id);
1857
2210
  return (
1858
2211
  mappingCurrent &&
1859
- (closing || allowInactive || (await this.#bindingCurrent(record.sessionId!, record.endpointGeneration!)))
2212
+ (closing ||
2213
+ allowInactive ||
2214
+ (await this.#bindingCurrent(
2215
+ record.sessionId!,
2216
+ record.endpointGeneration!,
2217
+ record.attachmentAuthorityId,
2218
+ )))
1860
2219
  );
1861
2220
  },
1862
2221
  );
@@ -1869,12 +2228,22 @@ export class DiscordNotificationDaemon {
1869
2228
  closing = false,
1870
2229
  occurrenceId?: string,
1871
2230
  ): Promise<void> {
2231
+ const existingEffect = await this.#effects.read<{ attachmentAuthorityId?: string }>(id);
2232
+ if (existingEffect && existingEffect.payload.attachmentAuthorityId !== record.attachmentAuthorityId)
2233
+ throw new DiscordAttachmentBindingError("Discord thread effect belongs to another attachment.");
1872
2234
  await this.#runEffect(
1873
2235
  id,
1874
2236
  operation,
1875
2237
  record.sessionId,
1876
2238
  record.endpointGeneration!,
1877
- { threadId: record.threadId!, locked, ...(occurrenceId === undefined ? {} : { occurrenceId }) },
2239
+ {
2240
+ threadId: record.threadId!,
2241
+ locked,
2242
+ ...(record.attachmentAuthorityId === undefined
2243
+ ? {}
2244
+ : { attachmentAuthorityId: record.attachmentAuthorityId }),
2245
+ ...(occurrenceId === undefined ? {} : { occurrenceId }),
2246
+ },
1878
2247
 
1879
2248
  async (ensure, beforeProvider) => {
1880
2249
  await ensure();
@@ -1899,6 +2268,7 @@ export class DiscordNotificationDaemon {
1899
2268
  current.state === (operation === "archive" ? "active" : "resuming") &&
1900
2269
  current.generation === record.generation &&
1901
2270
  current.endpointGeneration === record.endpointGeneration &&
2271
+ current.attachmentAuthorityId === record.attachmentAuthorityId &&
1902
2272
  (occurrenceId === undefined ||
1903
2273
  (operation === "archive"
1904
2274
  ? current.archiveEffectId === id && current.archiveOccurrenceId === occurrenceId
@@ -1906,7 +2276,12 @@ export class DiscordNotificationDaemon {
1906
2276
  return (
1907
2277
  mappingCurrent &&
1908
2278
  (closing ||
1909
- (!!record.sessionId && (await this.#bindingCurrent(record.sessionId, record.endpointGeneration!))))
2279
+ (!!record.sessionId &&
2280
+ (await this.#bindingCurrent(
2281
+ record.sessionId,
2282
+ record.endpointGeneration!,
2283
+ record.attachmentAuthorityId,
2284
+ ))))
1910
2285
  );
1911
2286
  },
1912
2287
  !closing,
@@ -1920,7 +2295,15 @@ export class DiscordNotificationDaemon {
1920
2295
  "create-thread",
1921
2296
  intent.sessionId,
1922
2297
  intent.endpointGeneration!,
1923
- { guildId: intent.guildId, parentId: intent.parentChannelId, name, nonce },
2298
+ {
2299
+ guildId: intent.guildId,
2300
+ parentId: intent.parentChannelId,
2301
+ name,
2302
+ nonce,
2303
+ ...(intent.attachmentAuthorityId === undefined
2304
+ ? {}
2305
+ : { attachmentAuthorityId: intent.attachmentAuthorityId }),
2306
+ },
1924
2307
  async ensure => {
1925
2308
  await ensure();
1926
2309
  const existing = await this.options.provider.findThreadByNonce({
@@ -1951,7 +2334,8 @@ export class DiscordNotificationDaemon {
1951
2334
  current.createOwner === intent.createOwner &&
1952
2335
  current.generation === intent.generation &&
1953
2336
  (current.createLeaseExpiresAt ?? 0) > this.#now() &&
1954
- (await this.#bindingCurrent(intent.sessionId!, intent.endpointGeneration!))
2337
+ current.attachmentAuthorityId === intent.attachmentAuthorityId &&
2338
+ (await this.#bindingCurrent(intent.sessionId!, intent.endpointGeneration!, intent.attachmentAuthorityId))
1955
2339
  );
1956
2340
  },
1957
2341
  );
@@ -1960,7 +2344,13 @@ export class DiscordNotificationDaemon {
1960
2344
  }
1961
2345
  async #recoverCreateThread(
1962
2346
  effect: ChatEffect,
1963
- payload: { guildId?: string; parentId?: string; name?: string; nonce?: string },
2347
+ payload: {
2348
+ guildId?: string;
2349
+ parentId?: string;
2350
+ name?: string;
2351
+ nonce?: string;
2352
+ attachmentAuthorityId?: string;
2353
+ },
1964
2354
  ): Promise<void> {
1965
2355
  if (!effect.sessionId || !payload.nonce) return;
1966
2356
  const intentKey = this.#intentKey(effect.sessionId);
@@ -1970,18 +2360,25 @@ export class DiscordNotificationDaemon {
1970
2360
  intent.sessionId === effect.sessionId &&
1971
2361
  intent.guildId === payload.guildId &&
1972
2362
  intent.parentChannelId === payload.parentId &&
2363
+ intent.attachmentAuthorityId === payload.attachmentAuthorityId &&
1973
2364
  discordEffectNonce(`create:${intent.sessionId}:${intent.createNonce}`) === payload.nonce;
1974
2365
  if (!matchesIntent || !intent) {
1975
2366
  if (effect.state !== "terminal") await this.#terminalizeEffect(effect.id, "rejected");
1976
2367
 
1977
2368
  return;
1978
2369
  }
1979
- if (!(await this.#bindingCurrent(effect.sessionId, effect.endpointGeneration))) {
2370
+ if (!(await this.#bindingCurrent(effect.sessionId, effect.endpointGeneration, payload.attachmentAuthorityId))) {
1980
2371
  await this.#store.delete(intentKey, intent.generation);
1981
2372
  return;
1982
2373
  }
1983
2374
  if (effect.state !== "terminal") {
1984
- await this.#create(effect.sessionId, effect.endpointGeneration, intent.createNonce!, payload.name);
2375
+ await this.#create(
2376
+ effect.sessionId,
2377
+ effect.endpointGeneration,
2378
+ intent.createNonce!,
2379
+ payload.name,
2380
+ intent.attachmentAuthorityId,
2381
+ );
1985
2382
  return;
1986
2383
  }
1987
2384
  const threadId = effect.receipt?.threadId;
@@ -1994,7 +2391,7 @@ export class DiscordNotificationDaemon {
1994
2391
  await this.#store.delete(intentKey, intent.generation);
1995
2392
  return;
1996
2393
  }
1997
- await this.#requireLiveBinding(effect.sessionId, effect.endpointGeneration);
2394
+ await this.#requireLiveBinding(effect.sessionId, effect.endpointGeneration, payload.attachmentAuthorityId);
1998
2395
  const key = discordConversationKey({
1999
2396
  appId: intent.appId,
2000
2397
  guildId: intent.guildId,
@@ -2014,6 +2411,7 @@ export class DiscordNotificationDaemon {
2014
2411
  threadId,
2015
2412
  sessionId: intent.sessionId,
2016
2413
  endpointGeneration: intent.endpointGeneration,
2414
+ attachmentAuthorityId: intent.attachmentAuthorityId,
2017
2415
  createNonce: intent.createNonce,
2018
2416
  effectIncarnationId: intent.createNonce,
2019
2417
  updatedAt: this.#now(),