@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,6 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { logger } from "@gajae-code/utils";
2
3
  import { SdkClientError } from "../client/client";
3
- import { readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
4
+ import { type SessionAttachment, SessionRouterError } from "../router";
5
+
4
6
  import type { ChatDeliveryError } from "./chat-daemon-runtime";
5
7
  import { ConversationStore } from "./conversation-store";
6
8
  import {
@@ -56,19 +58,35 @@ function slackPublicationClientMsgId(publicationId: string): string {
56
58
 
57
59
  // Durable filesystem publication leases must outlast one event-loop and persistence turn.
58
60
  const MIN_PUBLICATION_LEASE_MS = 100;
61
+ const RETIREMENT_DRAIN_MS = 5_000;
59
62
 
60
- export interface SlackEndpoint extends SdkSessionEndpoint {
61
- generation: number;
62
- }
63
+ type SlackProviderWorkScope = {
64
+ sessionId: string;
65
+ endpointGeneration: number;
66
+ attachmentAuthorityId?: string;
67
+ };
68
+
69
+ type SlackProviderWork = {
70
+ scopeKey: string;
71
+ invalidate(): Promise<void>;
72
+ abort(): void;
73
+ terminalize(): Promise<void>;
74
+ settled: Promise<void>;
75
+ };
63
76
 
64
- export interface SlackSdkClient {
65
- send(frame: Record<string, unknown>): void;
77
+ type SlackRetiredProviderWorkScope = {
78
+ retirements: number;
79
+ terminalizations: Set<Promise<void>>;
80
+ };
81
+
82
+ function slackProviderWorkScopeKey(scope: SlackProviderWorkScope): string {
83
+ return JSON.stringify([scope.sessionId, scope.endpointGeneration, scope.attachmentAuthorityId]);
66
84
  }
67
85
 
68
- /** Proven right to bind one session at one endpoint generation. */
69
86
  export interface SlackBindingAuthority {
70
87
  sessionId: string;
71
88
  endpointGeneration: number;
89
+ attachmentAuthorityId?: string;
72
90
  }
73
91
 
74
92
  export interface SlackNotificationDaemonOptions {
@@ -87,18 +105,13 @@ export interface SlackNotificationDaemonOptions {
87
105
  publicationOwnerId?: string;
88
106
  publicationLeaseMs?: number;
89
107
 
90
- resolveEndpoint?: (sessionId: string) => Promise<SlackEndpoint | null>;
91
- /**
92
- * Exact discovery/attachment authority for adopting an existing root. The
93
- * runtime supplies index, endpoint, and attachment proof; without it only the
94
- * resolvable endpoint generation can be proven.
95
- */
108
+ resolveAttachment: (sessionId: string) => Promise<SessionAttachment | null>;
109
+ /** Exact Router proof for adopting an existing root. */
96
110
  resolveBindingAuthority?: (sessionId: string) => Promise<SlackBindingAuthority | undefined>;
97
- createClient: (endpoint: SlackEndpoint) => SlackSdkClient;
98
111
  onCommand?: (
99
112
  sessionId: string,
100
113
  content: string,
101
- endpoint: SlackEndpoint,
114
+ attachment: SessionAttachment,
102
115
  idempotencyKey: string,
103
116
  ) => Promise<boolean>;
104
117
  }
@@ -126,6 +139,7 @@ type SlackInboundRouting = {
126
139
  teamId: string;
127
140
  channelId: string;
128
141
  rootTs: string;
142
+ attachmentAuthorityId?: string;
129
143
  actorId: string;
130
144
  eventId: string;
131
145
  interactionId: string;
@@ -188,14 +202,18 @@ export class SlackNotificationDaemon {
188
202
  readonly store: ConversationStore<SlackConversation>;
189
203
  readonly #now: () => number;
190
204
  readonly #randomId: () => string;
191
- readonly #resolveEndpoint: (sessionId: string) => Promise<SlackEndpoint | null>;
205
+ readonly #resolveAttachment: (sessionId: string) => Promise<SessionAttachment | null>;
206
+
192
207
  readonly #publicationOwnerId: string;
193
208
  readonly #publicationLeaseMs: number;
194
209
  readonly #journal: ChatEffectJournal;
195
210
 
196
211
  readonly #inflightInbound = new Set<string>();
197
212
  readonly #activeWork = new Set<Promise<unknown>>();
213
+ readonly #workInvalidators = new Set<SlackProviderWork>();
214
+ readonly #retiredProviderWorkScopes = new Map<string, SlackRetiredProviderWorkScope>();
198
215
  readonly #rollovers = new Map<string, Promise<SlackRootPublication>>();
216
+ #workGeneration = 0;
199
217
  #lifecycleTail: Promise<void> = Promise.resolve();
200
218
  #lifecycleGeneration = 0;
201
219
  #startedGeneration: number | undefined;
@@ -203,9 +221,12 @@ export class SlackNotificationDaemon {
203
221
  #startOperationGeneration: number | undefined;
204
222
  #stopOperation: Promise<void> | undefined;
205
223
  #started = false;
206
- #providerStarting = false;
207
- #providerRunning = false;
208
- #providerStopRequestedGeneration: number | undefined;
224
+ #providerStartingGeneration: number | undefined;
225
+ #providerRunningGeneration: number | undefined;
226
+ readonly #providerStopRequestedGenerations = new Set<number>();
227
+ #providerLifecycleTail: Promise<void> | undefined;
228
+ #providerLifecycleError: unknown;
229
+ #providerLifecycleErrorSet = false;
209
230
  #leaseRecoveryTimer: ReturnType<typeof setTimeout> | undefined;
210
231
  #leaseRecoveryAt: number | undefined;
211
232
  #leaseRecoveryFailures = 0;
@@ -221,15 +242,30 @@ export class SlackNotificationDaemon {
221
242
  this.#publicationOwnerId = options.publicationOwnerId ?? randomUUID();
222
243
  this.#publicationLeaseMs = Math.max(options.publicationLeaseMs ?? 30_000, MIN_PUBLICATION_LEASE_MS);
223
244
  this.#journal = new ChatEffectJournal({ agentDir: options.agentDir, transport: "slack", now: this.#now });
224
- this.#resolveEndpoint =
225
- options.resolveEndpoint ??
226
- (async sessionId => {
227
- const endpoint = await readSdkSessionEndpoint(options.repo, sessionId);
228
- return endpoint ? { ...endpoint, generation: 1 } : null;
229
- });
245
+ this.#resolveAttachment = options.resolveAttachment;
246
+ }
247
+
248
+ restartBlocked(): boolean {
249
+ return this.#providerLifecycleTail !== undefined || this.#providerLifecycleErrorSet;
230
250
  }
231
251
 
232
252
  async start(): Promise<void> {
253
+ if (this.#providerLifecycleErrorSet) throw this.#providerLifecycleError;
254
+ if (this.#providerLifecycleTail) {
255
+ const tail = this.#providerLifecycleTail;
256
+ const settled = await Promise.race([
257
+ Promise.allSettled([tail]).then(() => true),
258
+ Bun.sleep(5_000).then(() => false),
259
+ ]);
260
+ if (!settled) throw new Error("Prior Slack provider shutdown did not settle before restart.");
261
+ try {
262
+ await tail;
263
+ } catch (error) {
264
+ this.#recordProviderLifecycleError(error);
265
+ throw error;
266
+ }
267
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
268
+ }
233
269
  const lifecycleGeneration = this.#lifecycleGeneration;
234
270
  if (this.#started && this.#startedGeneration === lifecycleGeneration) return;
235
271
  if (this.#startOperation && this.#startOperationGeneration === lifecycleGeneration)
@@ -253,26 +289,38 @@ export class SlackNotificationDaemon {
253
289
  !this.#started ||
254
290
  this.#startedGeneration !== lifecycleGeneration
255
291
  ) {
256
- this.#started = false;
257
- this.#startedGeneration = undefined;
258
- this.#clearLeaseRecoveryTimer();
292
+ if (this.#startedGeneration === lifecycleGeneration) {
293
+ this.#started = false;
294
+ this.#startedGeneration = undefined;
295
+ this.#clearLeaseRecoveryTimer();
296
+ }
259
297
  return;
260
298
  }
261
- this.#providerStarting = true;
299
+ this.#providerStartingGeneration = lifecycleGeneration;
262
300
  try {
263
301
  await this.options.provider.start(async envelope => {
302
+ if (
303
+ !this.#started ||
304
+ this.#startedGeneration !== lifecycleGeneration ||
305
+ lifecycleGeneration !== this.#lifecycleGeneration
306
+ )
307
+ return;
264
308
  await this.#track(this.handleEnvelope(envelope));
265
309
  });
266
- this.#providerRunning = true;
310
+ if (this.#startedGeneration === lifecycleGeneration && this.#lifecycleGeneration === lifecycleGeneration)
311
+ this.#providerRunningGeneration = lifecycleGeneration;
267
312
  } finally {
268
- this.#providerStarting = false;
313
+ if (this.#providerStartingGeneration === lifecycleGeneration)
314
+ this.#providerStartingGeneration = undefined;
269
315
  }
270
316
  if (lifecycleGeneration !== this.#lifecycleGeneration || this.#startedGeneration !== lifecycleGeneration) {
271
- this.#started = false;
272
- this.#startedGeneration = undefined;
273
- this.#providerRunning = false;
274
- this.#clearLeaseRecoveryTimer();
275
- if (this.#providerStopRequestedGeneration !== lifecycleGeneration) await this.options.provider.stop();
317
+ if (this.#startedGeneration === lifecycleGeneration) {
318
+ this.#started = false;
319
+ this.#startedGeneration = undefined;
320
+ this.#clearLeaseRecoveryTimer();
321
+ }
322
+ if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
323
+ if (!this.#providerStopRequestedGenerations.has(lifecycleGeneration)) await this.options.provider.stop();
276
324
  }
277
325
  } catch (error) {
278
326
  if (this.#startedGeneration === lifecycleGeneration) {
@@ -298,23 +346,90 @@ export class SlackNotificationDaemon {
298
346
  async stop(): Promise<void> {
299
347
  if (this.#stopOperation) return await this.#stopOperation;
300
348
  const lifecycleGeneration = this.#lifecycleGeneration++;
301
- const stopProvider = this.#providerStarting || this.#providerRunning;
349
+ const stopProvider =
350
+ this.#providerStartingGeneration === lifecycleGeneration ||
351
+ this.#providerRunningGeneration === lifecycleGeneration;
302
352
  this.#started = false;
303
353
  this.#startedGeneration = undefined;
304
- this.#providerRunning = false;
354
+ if (this.#providerRunningGeneration === lifecycleGeneration) this.#providerRunningGeneration = undefined;
305
355
  this.#clearLeaseRecoveryTimer();
306
- if (stopProvider) this.#providerStopRequestedGeneration = lifecycleGeneration;
307
- const providerStop = stopProvider ? this.options.provider.stop() : undefined;
308
- const operation = this.#enqueueLifecycle(async () => {
356
+ if (stopProvider) this.#providerStopRequestedGenerations.add(lifecycleGeneration);
357
+ const providerStop = stopProvider ? Promise.resolve().then(() => this.options.provider.stop()) : undefined;
358
+ const providerStopResult = providerStop?.then(
359
+ () => ({ kind: "stopped" as const }),
360
+ error => ({ kind: "rejected" as const, error }),
361
+ );
362
+ let providerStopError: unknown;
363
+ let providerStopRejected = false;
364
+ const shutdownDeadline = this.#now() + 5_000;
365
+ const predecessor = this.#lifecycleTail;
366
+ const operation = (async () => {
367
+ const predecessorRemaining = shutdownDeadline - this.#now();
368
+ const predecessorSettled =
369
+ predecessorRemaining > 0 &&
370
+ (await Promise.race([
371
+ predecessor.then(
372
+ () => true,
373
+ () => true,
374
+ ),
375
+ Bun.sleep(predecessorRemaining).then(() => false),
376
+ ]));
377
+ if (!predecessorSettled) {
378
+ await this.#invalidateActiveWork();
379
+ logger.warn(
380
+ "Slack lifecycle predecessor exceeded the shutdown deadline; continuing with Router revocation.",
381
+ );
382
+ }
309
383
  // Calling provider.stop() before joining the lifecycle queue lets a provider
310
- // cancel an open that resolves only after its socket is stopped.
311
- if (providerStop) await providerStop;
312
- // Drain until quiescent: a tracked task can schedule further tracked work
313
- // (recovery/reconciliation) while we await, and any that outlives stop() would
314
- // bleed timing pressure into the next daemon/test. #started is already false,
315
- // so no new lease-recovery timers can be armed and the loop terminates.
316
- while (this.#activeWork.size > 0) await Promise.all([...this.#activeWork]);
317
- });
384
+ // cancel an open that resolves only after its socket is stopped. The same
385
+ // operation deadline covers provider teardown and admitted work draining.
386
+ if (providerStop && providerStopResult) {
387
+ const remaining = shutdownDeadline - this.#now();
388
+ const outcome = await Promise.race([
389
+ providerStopResult,
390
+ Bun.sleep(Math.max(0, remaining)).then(() => ({ kind: "timeout" as const })),
391
+ ]);
392
+ if (outcome.kind === "rejected") {
393
+ providerStopRejected = true;
394
+ providerStopError = outcome.error;
395
+ } else if (outcome.kind === "timeout") {
396
+ this.#retainProviderLifecycle(providerStop);
397
+ await this.#invalidateActiveWork();
398
+ logger.warn("Slack provider stop exceeded the shutdown deadline; continuing with Router revocation.");
399
+ }
400
+ }
401
+ // Drain until quiescent, but never let hung provider work prevent
402
+ // SessionRouter authority revocation and daemon ownership release.
403
+ while (this.#activeWork.size > 0) {
404
+ const remaining = shutdownDeadline - this.#now();
405
+ if (remaining <= 0) {
406
+ await this.#invalidateActiveWork();
407
+ logger.warn(
408
+ "Slack provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.",
409
+ );
410
+ break;
411
+ }
412
+ const settled = await Promise.race([
413
+ Promise.allSettled([...this.#activeWork]).then(() => true),
414
+ Bun.sleep(remaining).then(() => false),
415
+ ]);
416
+ if (!settled) {
417
+ await this.#invalidateActiveWork();
418
+ logger.warn(
419
+ "Slack provider work exceeded the 5000ms shutdown drain; continuing with Router revocation.",
420
+ );
421
+ break;
422
+ }
423
+ }
424
+ if (providerStopRejected) {
425
+ this.#recordProviderLifecycleError(providerStopError);
426
+ throw providerStopError;
427
+ }
428
+ })();
429
+ this.#lifecycleTail = operation.then(
430
+ () => undefined,
431
+ () => undefined,
432
+ );
318
433
  this.#stopOperation = operation;
319
434
  try {
320
435
  await operation;
@@ -367,6 +482,102 @@ export class SlackNotificationDaemon {
367
482
  return work.finally(() => this.#activeWork.delete(work));
368
483
  }
369
484
 
485
+ async #invalidateActiveWork(): Promise<void> {
486
+ this.#workGeneration += 1;
487
+ await Promise.allSettled([...this.#workInvalidators].map(work => work.invalidate()));
488
+ }
489
+ #fenceRetiredProviderWorkScope(scope: SlackProviderWorkScope): string {
490
+ const scopeKey = slackProviderWorkScopeKey(scope);
491
+ const retired = this.#retiredProviderWorkScopes.get(scopeKey);
492
+ if (retired) retired.retirements++;
493
+ else this.#retiredProviderWorkScopes.set(scopeKey, { retirements: 1, terminalizations: new Set() });
494
+ return scopeKey;
495
+ }
496
+ #tryReleaseRetiredProviderWorkScope(scopeKey: string): void {
497
+ const retired = this.#retiredProviderWorkScopes.get(scopeKey);
498
+ if (
499
+ retired?.retirements !== 0 ||
500
+ retired.terminalizations.size > 0 ||
501
+ [...this.#workInvalidators].some(work => work.scopeKey === scopeKey)
502
+ )
503
+ return;
504
+ this.#retiredProviderWorkScopes.delete(scopeKey);
505
+ }
506
+ #trackRetirementTerminalization(scopeKey: string, terminalization: Promise<void>): void {
507
+ const retired = this.#retiredProviderWorkScopes.get(scopeKey);
508
+ if (!retired) return;
509
+ retired.terminalizations.add(terminalization);
510
+ void terminalization.then(() => {
511
+ const current = this.#retiredProviderWorkScopes.get(scopeKey);
512
+ if (!current) return;
513
+ current.terminalizations.delete(terminalization);
514
+ this.#tryReleaseRetiredProviderWorkScope(scopeKey);
515
+ });
516
+ }
517
+ #retirementTerminalization(terminalizations: readonly Promise<unknown>[]): Promise<void> {
518
+ return Promise.all(
519
+ terminalizations.map(terminalization =>
520
+ terminalization.then(
521
+ () => undefined,
522
+ error => {
523
+ logger.warn(`Slack retired attachment terminalization failed: ${String(error)}`);
524
+ },
525
+ ),
526
+ ),
527
+ ).then(() => undefined);
528
+ }
529
+ async #invalidateAndDrainProviderWork(scope: SlackProviderWorkScope, deadline: number): Promise<void> {
530
+ const scopeKey = this.#fenceRetiredProviderWorkScope(scope);
531
+ for (;;) {
532
+ const active = [...this.#workInvalidators].filter(work => work.scopeKey === scopeKey);
533
+ if (active.length === 0) return;
534
+ for (const work of active) work.abort();
535
+ const remaining = deadline - this.#now();
536
+ if (remaining <= 0) {
537
+ const terminalization = this.#retirementTerminalization(active.map(work => work.terminalize()));
538
+ this.#trackRetirementTerminalization(scopeKey, terminalization);
539
+ logger.warn("Slack attachment provider work exceeded the 5000ms retirement drain; fenced pending effects.");
540
+ return;
541
+ }
542
+ const drained = await Promise.race([
543
+ Promise.all(active.map(work => work.settled)).then(() => true),
544
+ Bun.sleep(remaining).then(() => false),
545
+ ]);
546
+ if (drained) continue;
547
+ const lingering = [...this.#workInvalidators].filter(work => work.scopeKey === scopeKey);
548
+ for (const work of lingering) work.abort();
549
+ const terminalization = this.#retirementTerminalization(lingering.map(work => work.terminalize()));
550
+ this.#trackRetirementTerminalization(scopeKey, terminalization);
551
+ logger.warn("Slack attachment provider work exceeded the 5000ms retirement drain; fenced pending effects.");
552
+ return;
553
+ }
554
+ }
555
+ #releaseRetiredProviderWorkScope(scope: SlackProviderWorkScope): void {
556
+ const scopeKey = slackProviderWorkScopeKey(scope);
557
+ const retired = this.#retiredProviderWorkScopes.get(scopeKey);
558
+ if (!retired || retired.retirements === 0) return;
559
+ retired.retirements--;
560
+ this.#tryReleaseRetiredProviderWorkScope(scopeKey);
561
+ }
562
+
563
+ #recordProviderLifecycleError(error: unknown): void {
564
+ this.#providerLifecycleError = error;
565
+ this.#providerLifecycleErrorSet = true;
566
+ }
567
+
568
+ #retainProviderLifecycle(tail: Promise<void>): void {
569
+ this.#providerLifecycleTail = tail;
570
+ void tail.then(
571
+ () => {
572
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
573
+ },
574
+ error => {
575
+ this.#recordProviderLifecycleError(error);
576
+ if (this.#providerLifecycleTail === tail) this.#providerLifecycleTail = undefined;
577
+ },
578
+ );
579
+ }
580
+
370
581
  #enqueueLifecycle<T>(operation: () => Promise<T>): Promise<T> {
371
582
  const scheduled = this.#lifecycleTail.then(operation, operation);
372
583
  this.#lifecycleTail = scheduled.then(
@@ -405,6 +616,11 @@ export class SlackNotificationDaemon {
405
616
  "session_not_live",
406
617
  "Slack thread binding requires an exact live session endpoint.",
407
618
  );
619
+ const attachment =
620
+ authority.attachmentAuthorityId === undefined ? await this.#resolveAttachment(sessionId) : undefined;
621
+ const attachmentAuthorityId =
622
+ authority.attachmentAuthorityId ??
623
+ (attachment?.generation === authority.endpointGeneration ? attachment.authorityId : undefined);
408
624
  await this.#verifyExistingRoot(rootTs);
409
625
  return await claimSlackThreadBinding({
410
626
  store: this.store,
@@ -414,8 +630,14 @@ export class SlackNotificationDaemon {
414
630
  sessionId,
415
631
  rootTs,
416
632
  endpointGeneration: authority.endpointGeneration,
633
+ ...(attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId }),
417
634
  revalidate: async () => {
418
- if ((await this.#bindingAuthority(sessionId))?.endpointGeneration !== authority.endpointGeneration)
635
+ const current = await this.#bindingAuthority(sessionId);
636
+ if (
637
+ !current ||
638
+ current.endpointGeneration !== authority.endpointGeneration ||
639
+ current.attachmentAuthorityId !== attachmentAuthorityId
640
+ )
419
641
  return false;
420
642
  return commitAuthority ? await commitAuthority() : true;
421
643
  },
@@ -429,10 +651,28 @@ export class SlackNotificationDaemon {
429
651
  * usable generation, which is all a directly constructed daemon can prove.
430
652
  */
431
653
  async #bindingAuthority(sessionId: string): Promise<SlackBindingAuthority | undefined> {
432
- if (this.options.resolveBindingAuthority) return await this.options.resolveBindingAuthority(sessionId);
433
- const endpoint = await this.#resolveEndpoint(sessionId);
434
- if (!endpoint || !Number.isSafeInteger(endpoint.generation) || endpoint.generation <= 0) return undefined;
435
- return { sessionId, endpointGeneration: endpoint.generation };
654
+ const resolved = this.options.resolveBindingAuthority
655
+ ? await this.options.resolveBindingAuthority(sessionId)
656
+ : undefined;
657
+ if (this.options.resolveBindingAuthority && !resolved) return undefined;
658
+ const endpoint = await this.#resolveAttachment(sessionId);
659
+ if (
660
+ !endpoint?.isCurrent() ||
661
+ endpoint.sessionId !== sessionId ||
662
+ !Number.isSafeInteger(endpoint.generation) ||
663
+ endpoint.generation <= 0 ||
664
+ (resolved !== undefined &&
665
+ (resolved.sessionId !== sessionId ||
666
+ resolved.endpointGeneration !== endpoint.generation ||
667
+ (resolved.attachmentAuthorityId !== undefined &&
668
+ resolved.attachmentAuthorityId !== endpoint.authorityId)))
669
+ )
670
+ return undefined;
671
+ return {
672
+ sessionId,
673
+ endpointGeneration: endpoint.generation,
674
+ ...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
675
+ };
436
676
  }
437
677
 
438
678
  /** Prove the operator-supplied root exists in the configured channel before persisting anything. */
@@ -456,14 +696,14 @@ export class SlackNotificationDaemon {
456
696
  endpointGeneration?: number,
457
697
  requestedClientMsgId?: string,
458
698
  ): Promise<SlackRootPublication> {
459
- const endpoint = await this.#resolveEndpoint(sessionId);
699
+ const endpoint = await this.#resolveAttachment(sessionId);
460
700
  if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
461
701
  throw new SlackEndpointBindingError("Slack root publication requires the current session endpoint.");
462
702
  const generation = endpoint.generation;
463
703
  const pendingKey = this.#intentKey(sessionId);
464
704
  let claimed = false;
465
705
  const pending = await this.store.transact(pendingKey, current => {
466
- if (current?.state === "active") return current;
706
+ if (current?.state === "active" && current.attachmentAuthorityId === endpoint.authorityId) return current;
467
707
  const now = this.#now();
468
708
  if (
469
709
  current?.rootPublicationOwner !== undefined &&
@@ -493,12 +733,17 @@ export class SlackNotificationDaemon {
493
733
  rootPublicationLeaseExpiresAt: now + this.#publicationLeaseMs,
494
734
  rootPublicationFence: (current?.rootPublicationFence ?? 0) + 1,
495
735
  endpointGeneration: generation,
736
+ attachmentAuthorityId: endpoint.authorityId,
496
737
  updatedAt: now,
497
- seenEventIds: current?.seenEventIds ?? [],
498
- seenContextIds: current?.seenContextIds ?? [],
499
- seenRetryKeys: current?.seenRetryKeys ?? [],
500
- seenInteractionIds: current?.seenInteractionIds ?? [],
501
- inboundDispatches: current?.inboundDispatches ?? [],
738
+ seenEventIds: current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenEventIds ?? []) : [],
739
+ seenContextIds:
740
+ current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenContextIds ?? []) : [],
741
+ seenRetryKeys:
742
+ current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenRetryKeys ?? []) : [],
743
+ seenInteractionIds:
744
+ current?.attachmentAuthorityId === endpoint.authorityId ? (current?.seenInteractionIds ?? []) : [],
745
+ inboundDispatches:
746
+ current?.attachmentAuthorityId === endpoint.authorityId ? (current?.inboundDispatches ?? []) : [],
502
747
  };
503
748
  });
504
749
  // An already-active session root is authoritative whether this daemon
@@ -524,6 +769,7 @@ export class SlackNotificationDaemon {
524
769
  channel: this.options.channelId,
525
770
  text: body,
526
771
  clientMsgId,
772
+ ...(endpoint.authorityId === undefined ? {} : { attachmentAuthorityId: endpoint.authorityId }),
527
773
  });
528
774
  createdRootEffect = !durable.existed;
529
775
  posted = await this.#postDurable(effectId, sessionId, generation, durable.payload);
@@ -567,6 +813,7 @@ export class SlackNotificationDaemon {
567
813
  state: "active",
568
814
  rootTs: confirmedPosted.ts,
569
815
  endpointGeneration: generation,
816
+ attachmentAuthorityId: endpoint.authorityId,
570
817
  updatedAt: this.#now(),
571
818
  lastError: undefined,
572
819
  rootPublicationOwner: undefined,
@@ -587,7 +834,17 @@ export class SlackNotificationDaemon {
587
834
  endpointGeneration?: number,
588
835
  publicationId?: string,
589
836
  ): Promise<SlackConversation> {
590
- const endpoint = await this.#resolveEndpoint(sessionId);
837
+ return await this.#track(this.#notify(sessionId, body, actionId, endpointGeneration, publicationId));
838
+ }
839
+
840
+ async #notify(
841
+ sessionId: string,
842
+ body: string,
843
+ actionId?: string,
844
+ endpointGeneration?: number,
845
+ publicationId?: string,
846
+ ): Promise<SlackConversation> {
847
+ const endpoint = await this.#resolveAttachment(sessionId);
591
848
  if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
592
849
  throw new SlackEndpointBindingError("Slack notification requires the current session endpoint.");
593
850
  const generation = endpoint.generation;
@@ -602,7 +859,8 @@ export class SlackNotificationDaemon {
602
859
  const usedExistingRoot =
603
860
  existing?.record.state === "active" &&
604
861
  !!existing.record.rootTs &&
605
- existing.record.endpointGeneration === generation;
862
+ existing.record.endpointGeneration === generation &&
863
+ existing.record.attachmentAuthorityId === endpoint.authorityId;
606
864
  let conversation: SlackConversation;
607
865
  let bodyWasUsedAsRoot =
608
866
  usedExistingRoot &&
@@ -648,6 +906,9 @@ export class SlackNotificationDaemon {
648
906
  threadTs: conversation.rootTs,
649
907
  text: body,
650
908
  clientMsgId: publication.clientMsgId,
909
+ ...(conversation.attachmentAuthorityId === undefined
910
+ ? {}
911
+ : { attachmentAuthorityId: conversation.attachmentAuthorityId }),
651
912
  });
652
913
  return conversation;
653
914
  }
@@ -696,6 +957,9 @@ export class SlackNotificationDaemon {
696
957
  threadTs: conversation.rootTs,
697
958
  text: body,
698
959
  clientMsgId,
960
+ ...(conversation.attachmentAuthorityId === undefined
961
+ ? {}
962
+ : { attachmentAuthorityId: conversation.attachmentAuthorityId }),
699
963
  });
700
964
  published = await this.#postDurable(effectId, sessionId, conversationGeneration, durable.payload);
701
965
  });
@@ -742,6 +1006,10 @@ export class SlackNotificationDaemon {
742
1006
 
743
1007
  /** Posts a safe command outcome to the active mapped root thread. */
744
1008
  async postCommandResult(sessionId: string, content: string): Promise<boolean> {
1009
+ return await this.#track(this.#postCommandResult(sessionId, content));
1010
+ }
1011
+
1012
+ async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
745
1013
  const found = await this.findSession(sessionId, false);
746
1014
  if (found?.record.state !== "active" || !found.record.rootTs) return false;
747
1015
  const generation = this.#requireEndpointGeneration(found.record);
@@ -750,6 +1018,9 @@ export class SlackNotificationDaemon {
750
1018
  threadTs: found.record.rootTs,
751
1019
  text: content,
752
1020
  clientMsgId: this.#randomId(),
1021
+ ...(found.record.attachmentAuthorityId === undefined
1022
+ ? {}
1023
+ : { attachmentAuthorityId: found.record.attachmentAuthorityId }),
753
1024
  });
754
1025
  return true;
755
1026
  }
@@ -764,10 +1035,117 @@ export class SlackNotificationDaemon {
764
1035
  );
765
1036
  }
766
1037
 
767
- async close(sessionId: string, marker = "Session closed."): Promise<boolean> {
1038
+ async close(sessionId: string, marker = "Session closed.", endpointGeneration?: number): Promise<boolean> {
1039
+ return await this.#track(this.#close(sessionId, marker, true, endpointGeneration));
1040
+ }
1041
+
1042
+ async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
1043
+ const found = await this.findSession(sessionId, true);
1044
+ if (
1045
+ !found?.record.rootTs ||
1046
+ found.record.state !== "active" ||
1047
+ found.record.endpointGeneration !== endpointGeneration
1048
+ )
1049
+ return;
1050
+ const effectId = `close-marker-cleanup:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
1051
+ if (found.record.cleanupEffectId !== effectId) return;
1052
+ const effect = await this.#journal.read(effectId);
1053
+ if (!effect || (effect.state === "terminal" && !effect.receipt?.messageId && !effect.receipt?.timestamp)) {
1054
+ await this.store.transact(found.key, current =>
1055
+ current?.cleanupEffectId === effectId
1056
+ ? nextRecord(current, { cleanupEffectId: undefined, updatedAt: this.#now() })
1057
+ : current,
1058
+ );
1059
+ return;
1060
+ }
1061
+ await this.close(sessionId, "Session closed.", endpointGeneration);
1062
+ }
1063
+
1064
+ async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
1065
+ const found = await this.findSession(sessionId, true);
1066
+ if (found?.record.endpointGeneration !== endpointGeneration) return;
1067
+ const attachmentAuthorityId = found.record.attachmentAuthorityId;
1068
+ const scope: SlackProviderWorkScope = {
1069
+ sessionId,
1070
+ endpointGeneration,
1071
+ ...(attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId }),
1072
+ };
1073
+ const deadline = this.#now() + RETIREMENT_DRAIN_MS;
1074
+ let releaseScope = false;
1075
+ try {
1076
+ const draining = this.#invalidateAndDrainProviderWork(scope, deadline);
1077
+ let inboundDispatches: SlackInboundDispatchReceipt[] = [];
1078
+ // Persist the mapping fence before waiting on terminal effects. A detached
1079
+ // terminalization can then never be replayed through this attachment.
1080
+ await this.store.transact(found.key, record => {
1081
+ if (
1082
+ record?.sessionId !== sessionId ||
1083
+ record.endpointGeneration !== endpointGeneration ||
1084
+ record.attachmentAuthorityId !== attachmentAuthorityId
1085
+ )
1086
+ return record;
1087
+ inboundDispatches = [...(record.inboundDispatches ?? [])];
1088
+ return nextRecord(record, {
1089
+ state: "closed_marker",
1090
+ pendingActionId: undefined,
1091
+ cleanupEffectId: undefined,
1092
+ inboundDispatches: [],
1093
+ updatedAt: this.#now(),
1094
+ });
1095
+ });
1096
+ releaseScope = true;
1097
+ await draining;
1098
+ if (inboundDispatches.length === 0) return;
1099
+ const terminalization = this.#retirementTerminalization(
1100
+ inboundDispatches.map(receipt => this.#journal.terminalize(receipt.effectId, { status: "stale_binding" })),
1101
+ );
1102
+ this.#trackRetirementTerminalization(slackProviderWorkScopeKey(scope), terminalization);
1103
+ const remaining = deadline - this.#now();
1104
+ if (remaining <= 0) {
1105
+ logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
1106
+ return;
1107
+ }
1108
+ const terminalized = await Promise.race([
1109
+ terminalization.then(() => true),
1110
+ Bun.sleep(remaining).then(() => false),
1111
+ ]);
1112
+ if (!terminalized)
1113
+ logger.warn("Slack attachment retirement exceeded the 5000ms deadline; fenced pending inbound effects.");
1114
+ } finally {
1115
+ if (releaseScope) this.#releaseRetiredProviderWorkScope(scope);
1116
+ }
1117
+ }
1118
+
1119
+ async #close(
1120
+ sessionId: string,
1121
+ marker: string,
1122
+ allowRemovedAttachment: boolean,
1123
+ expectedEndpointGeneration?: number,
1124
+ ): Promise<boolean> {
768
1125
  const found = await this.findSession(sessionId, true);
769
1126
  if (!found?.record.rootTs || found.record.state !== "active") return false;
770
- const effectId = `close-marker:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
1127
+ if (expectedEndpointGeneration !== undefined && found.record.endpointGeneration !== expectedEndpointGeneration)
1128
+ return false;
1129
+ const effectId = `${allowRemovedAttachment ? "close-marker-cleanup" : "close-marker"}:${sessionId}:${found.record.clientMsgId ?? found.record.rootTs}`;
1130
+ if (allowRemovedAttachment) {
1131
+ let admitted = false;
1132
+ await this.store.transact(found.key, current => {
1133
+ if (
1134
+ current?.state !== "active" ||
1135
+ current.sessionId !== found.record.sessionId ||
1136
+ current.rootTs !== found.record.rootTs ||
1137
+ current.endpointGeneration !== found.record.endpointGeneration ||
1138
+ current.clientMsgId !== found.record.clientMsgId ||
1139
+ (current.cleanupEffectId !== undefined && current.cleanupEffectId !== effectId)
1140
+ )
1141
+ return current;
1142
+ admitted = true;
1143
+ return current.cleanupEffectId === effectId
1144
+ ? current
1145
+ : nextRecord(current, { cleanupEffectId: effectId, updatedAt: this.#now() });
1146
+ });
1147
+ if (!admitted) return false;
1148
+ }
771
1149
  const existing = await this.#journal.read<{
772
1150
  channel: string;
773
1151
  text: string;
@@ -783,16 +1161,33 @@ export class SlackNotificationDaemon {
783
1161
  threadTs: found.record.rootTs,
784
1162
  text: marker,
785
1163
  clientMsgId: existing?.payload.clientMsgId ?? this.#randomId(),
1164
+ ...(found.record.attachmentAuthorityId === undefined
1165
+ ? {}
1166
+ : { attachmentAuthorityId: found.record.attachmentAuthorityId }),
786
1167
  },
787
1168
  );
788
1169
  await this.#postDurable(effectId, sessionId, this.#requireEndpointGeneration(found.record), durable.payload);
789
- await this.store.transact(found.key, current =>
790
- current
791
- ? nextRecord(current, { state: "closed_marker", pendingActionId: undefined, updatedAt: this.#now() })
792
- : current,
793
- );
1170
+ let closed = false;
1171
+ await this.store.transact(found.key, current => {
1172
+ if (
1173
+ current?.state !== "active" ||
1174
+ current.sessionId !== found.record.sessionId ||
1175
+ current.rootTs !== found.record.rootTs ||
1176
+ current.endpointGeneration !== found.record.endpointGeneration ||
1177
+ current.clientMsgId !== found.record.clientMsgId ||
1178
+ (allowRemovedAttachment && current.cleanupEffectId !== effectId)
1179
+ )
1180
+ return current;
1181
+ closed = true;
1182
+ return nextRecord(current, {
1183
+ state: "closed_marker",
1184
+ cleanupEffectId: undefined,
1185
+ pendingActionId: undefined,
1186
+ updatedAt: this.#now(),
1187
+ });
1188
+ });
794
1189
 
795
- return true;
1190
+ return closed;
796
1191
  }
797
1192
 
798
1193
  async resume(
@@ -800,6 +1195,15 @@ export class SlackNotificationDaemon {
800
1195
  body: string,
801
1196
  endpointGeneration?: number,
802
1197
  publicationId?: string,
1198
+ ): Promise<SlackConversation> {
1199
+ return await this.#track(this.#resume(sessionId, body, endpointGeneration, publicationId));
1200
+ }
1201
+
1202
+ async #resume(
1203
+ sessionId: string,
1204
+ body: string,
1205
+ endpointGeneration?: number,
1206
+ publicationId?: string,
803
1207
  ): Promise<SlackConversation> {
804
1208
  const rootPublication =
805
1209
  publicationId === undefined
@@ -843,7 +1247,7 @@ export class SlackNotificationDaemon {
843
1247
  endpointGeneration?: number,
844
1248
  requestedClientMsgId?: string,
845
1249
  ): Promise<SlackRootPublication> {
846
- const endpoint = await this.#resolveEndpoint(sessionId);
1250
+ const endpoint = await this.#resolveAttachment(sessionId);
847
1251
  if (!endpoint || (endpointGeneration !== undefined && endpoint.generation !== endpointGeneration))
848
1252
  throw new SlackEndpointBindingError("Slack root rollover requires the current session endpoint.");
849
1253
  const generation = endpoint.generation;
@@ -872,7 +1276,11 @@ export class SlackNotificationDaemon {
872
1276
  const previousGeneration = this.#requireEndpointGeneration(previous.record);
873
1277
  if (previousGeneration > generation)
874
1278
  throw new SlackEndpointBindingError("Slack root belongs to a newer endpoint generation.");
875
- if (previousGeneration === generation && (!forced || waitedForRollover))
1279
+ if (
1280
+ previousGeneration === generation &&
1281
+ previous.record.attachmentAuthorityId === endpoint.authorityId &&
1282
+ (!forced || waitedForRollover)
1283
+ )
876
1284
  return { conversation: previous.record, created: false };
877
1285
 
878
1286
  let claimed = false;
@@ -907,7 +1315,7 @@ export class SlackNotificationDaemon {
907
1315
  }
908
1316
  let closed = false;
909
1317
  try {
910
- closed = await this.close(sessionId);
1318
+ closed = await this.#track(this.#close(sessionId, "Session closed.", false, previousGeneration));
911
1319
  } catch (error) {
912
1320
  if (!(error instanceof SlackStaleEffectError)) throw error;
913
1321
  // The old-generation close marker was deliberately suppressed after
@@ -946,7 +1354,7 @@ export class SlackNotificationDaemon {
946
1354
  },
947
1355
  actorId: string,
948
1356
  ): Promise<
949
- { key: string; endpoint: SlackEndpoint; sessionId: string; receipt: SlackInboundDispatchReceipt } | undefined
1357
+ { key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt } | undefined
950
1358
  > {
951
1359
  const document = await this.store.load();
952
1360
  const matched = Object.entries(document.conversations)
@@ -971,8 +1379,13 @@ export class SlackNotificationDaemon {
971
1379
  !acceptsSlackInbound(record, inbound.rootTs, record.endpointGeneration)
972
1380
  )
973
1381
  return undefined;
974
- const endpoint = await this.#resolveEndpoint(record.sessionId);
975
- if (!endpoint || endpoint.generation !== record.endpointGeneration) return undefined;
1382
+ const endpoint = await this.#resolveAttachment(record.sessionId);
1383
+ if (
1384
+ !endpoint ||
1385
+ endpoint.generation !== record.endpointGeneration ||
1386
+ record.attachmentAuthorityId !== endpoint.authorityId
1387
+ )
1388
+ return undefined;
976
1389
  const interactionId = text(inbound.event.client_msg_id) ?? inbound.eventId;
977
1390
  const retryKey = `${inbound.eventId}:${interactionId}`;
978
1391
  const inboundText = text(inbound.event.text);
@@ -984,6 +1397,7 @@ export class SlackNotificationDaemon {
984
1397
  teamId: inbound.teamId,
985
1398
  channelId: inbound.channelId,
986
1399
  rootTs: inbound.rootTs,
1400
+ ...(record.attachmentAuthorityId === undefined ? {} : { attachmentAuthorityId: record.attachmentAuthorityId }),
987
1401
  eventId: inbound.eventId,
988
1402
  interactionId,
989
1403
  actorId,
@@ -1013,7 +1427,12 @@ export class SlackNotificationDaemon {
1013
1427
  let sessionId: string | undefined;
1014
1428
  let receipt: SlackInboundDispatchReceipt | undefined;
1015
1429
  await this.store.transact(key, current => {
1016
- if (!current?.sessionId || !acceptsSlackInbound(current, inbound.rootTs, endpoint.generation)) return current;
1430
+ if (
1431
+ !current?.sessionId ||
1432
+ !acceptsSlackInbound(current, inbound.rootTs, endpoint.generation) ||
1433
+ current.attachmentAuthorityId !== endpoint.authorityId
1434
+ )
1435
+ return current;
1017
1436
  const existing = (current.inboundDispatches ?? []).find(
1018
1437
  candidate =>
1019
1438
  candidate.eventId === inbound.eventId ||
@@ -1044,6 +1463,9 @@ export class SlackNotificationDaemon {
1044
1463
  kind: command ? "command" : "action",
1045
1464
  ...(command ? {} : { actionId: current.pendingActionId }),
1046
1465
  endpointGeneration: endpoint.generation,
1466
+ ...(current.attachmentAuthorityId === undefined
1467
+ ? {}
1468
+ : { attachmentAuthorityId: current.attachmentAuthorityId }),
1047
1469
  effectId,
1048
1470
  idempotencyKey,
1049
1471
  };
@@ -1057,7 +1479,8 @@ export class SlackNotificationDaemon {
1057
1479
 
1058
1480
  async #dispatchInbound(claim: {
1059
1481
  key: string;
1060
- endpoint: SlackEndpoint;
1482
+ endpoint: SessionAttachment;
1483
+
1061
1484
  sessionId: string;
1062
1485
  receipt: SlackInboundDispatchReceipt;
1063
1486
  }): Promise<boolean> {
@@ -1079,7 +1502,8 @@ export class SlackNotificationDaemon {
1079
1502
  }
1080
1503
 
1081
1504
  async #dispatchEffect(
1082
- claim: { key: string; endpoint: SlackEndpoint; sessionId: string; receipt: SlackInboundDispatchReceipt },
1505
+ claim: { key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt },
1506
+
1083
1507
  effectId: string,
1084
1508
  ): Promise<boolean> {
1085
1509
  const effect = await this.#rescheduleAfterEffectTransition(
@@ -1095,10 +1519,17 @@ export class SlackNotificationDaemon {
1095
1519
  return false;
1096
1520
  }
1097
1521
  const lease: ChatEffectLease = { owner: this.#publicationOwnerId, epoch: effect.epoch };
1522
+ const workScope: SlackProviderWorkScope = {
1523
+ sessionId: claim.sessionId,
1524
+ endpointGeneration: claim.receipt.endpointGeneration,
1525
+ ...(claim.receipt.attachmentAuthorityId === undefined
1526
+ ? {}
1527
+ : { attachmentAuthorityId: claim.receipt.attachmentAuthorityId }),
1528
+ };
1098
1529
  try {
1099
1530
  if (effect.payload.type === "command") {
1100
1531
  const payload = effect.payload;
1101
- const accepted = await this.#withEffectLease(effect.id, lease, async () => {
1532
+ const accepted = await this.#withEffectLease(effect.id, lease, workScope, async () => {
1102
1533
  if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
1103
1534
  return await (this.options.onCommand?.(
1104
1535
  claim.sessionId,
@@ -1117,9 +1548,9 @@ export class SlackNotificationDaemon {
1117
1548
  if (recorded) await this.#finishDispatch(claim, "terminal");
1118
1549
  return accepted && !!recorded;
1119
1550
  }
1120
- await this.#withEffectLease(effect.id, lease, async () => {
1551
+ await this.#withEffectLease(effect.id, lease, workScope, async () => {
1121
1552
  if (!(await this.#inboundEffectCurrent(claim, effect.id))) throw new SlackStaleEffectError();
1122
- this.options.createClient(claim.endpoint).send(effect.payload);
1553
+ await claim.endpoint.send(effect.payload);
1123
1554
  });
1124
1555
  if (!(await this.#inboundEffectCurrent(claim, effect.id))) {
1125
1556
  await this.#terminalizeStaleInboundDispatch(claim.key, claim.receipt, "stale_binding");
@@ -1145,11 +1576,18 @@ export class SlackNotificationDaemon {
1145
1576
  }
1146
1577
 
1147
1578
  async #inboundEffectCurrent(
1148
- claim: { key: string; endpoint: SlackEndpoint; sessionId: string; receipt: SlackInboundDispatchReceipt },
1579
+ claim: { key: string; endpoint: SessionAttachment; sessionId: string; receipt: SlackInboundDispatchReceipt },
1580
+
1149
1581
  effectId: string,
1150
1582
  ): Promise<boolean> {
1151
- const endpoint = await this.#resolveEndpoint(claim.sessionId);
1152
- if (!endpoint || endpoint.generation !== claim.endpoint.generation) return false;
1583
+ const endpoint = await this.#resolveAttachment(claim.sessionId);
1584
+ if (!endpoint?.isCurrent()) return false;
1585
+ if (
1586
+ endpoint.authorityId !== undefined && claim.endpoint.authorityId !== undefined
1587
+ ? endpoint.authorityId !== claim.endpoint.authorityId
1588
+ : endpoint.generation !== claim.endpoint.generation
1589
+ )
1590
+ return false;
1153
1591
  const [current, effect] = await Promise.all([
1154
1592
  this.store.read(claim.key),
1155
1593
  this.#journal.read<SlackInboundEffectPayload>(effectId),
@@ -1160,27 +1598,107 @@ export class SlackNotificationDaemon {
1160
1598
  current.sessionId === claim.sessionId &&
1161
1599
  acceptsSlackInbound(current, current.rootTs ?? "", claim.endpoint.generation) &&
1162
1600
  claim.receipt.endpointGeneration === claim.endpoint.generation &&
1601
+ current.attachmentAuthorityId === claim.receipt.attachmentAuthorityId &&
1602
+ current.attachmentAuthorityId === endpoint.authorityId &&
1163
1603
  this.#matchesInboundEffect(effect, claim.receipt) &&
1164
1604
  (await this.#actorAuthorized(effect.payload.routing.actorId)) &&
1165
1605
  (current.inboundDispatches ?? []).some(receipt => this.#sameInboundReceipt(receipt, claim.receipt))
1166
1606
  );
1167
1607
  }
1168
1608
 
1169
- async #withEffectLease<T>(id: string, lease: ChatEffectLease, operation: () => Promise<T>): Promise<T> {
1170
- // The effect was just claimed, so its lease is current. The periodic renewal
1171
- // still protects the external operation without another durable write first.
1172
- return await this.#withRenewal(
1173
- operation,
1174
- async () => {
1175
- if (
1176
- !(await this.#rescheduleAfterEffectTransition(
1177
- this.#journal.renew(id, lease, Math.max(this.#publicationLeaseMs, 100)),
1178
- ))
1179
- )
1180
- throw new Error("Slack effect lease renewal failed");
1181
- },
1182
- false,
1183
- );
1609
+ async #withEffectLease<T>(
1610
+ id: string,
1611
+ lease: ChatEffectLease,
1612
+ scope: SlackProviderWorkScope,
1613
+ operation: (signal: AbortSignal) => Promise<T>,
1614
+ workGeneration = this.#workGeneration,
1615
+ ): Promise<T> {
1616
+ const scopeKey = slackProviderWorkScopeKey(scope);
1617
+ const controller = new AbortController();
1618
+ let invalidation: Promise<void> | undefined;
1619
+ let terminalization: Promise<void> | undefined;
1620
+ let invalidated = false;
1621
+ const abort = (): void => {
1622
+ invalidated = true;
1623
+ controller.abort();
1624
+ };
1625
+ const invalidate = async (): Promise<void> => {
1626
+ abort();
1627
+ invalidation ??= this.#rescheduleAfterEffectTransition(
1628
+ this.#journal.record(id, lease, "uncertain", { status: "shutdown_timeout" }),
1629
+ ).then(() => undefined);
1630
+ await invalidation;
1631
+ };
1632
+ const terminalize = async (): Promise<void> => {
1633
+ abort();
1634
+ terminalization ??= (async (): Promise<void> => {
1635
+ const receipt = { status: "stale_binding" };
1636
+ const terminalized = await this.#rescheduleAfterEffectTransition(
1637
+ this.#journal.terminalize(id, receipt, lease),
1638
+ );
1639
+ if (!terminalized) await this.#rescheduleAfterEffectTransition(this.#journal.terminalize(id, receipt));
1640
+ })();
1641
+ await terminalization;
1642
+ };
1643
+ const settled = Promise.withResolvers<void>();
1644
+ const work: SlackProviderWork = {
1645
+ scopeKey,
1646
+ invalidate,
1647
+ abort,
1648
+ terminalize,
1649
+ settled: settled.promise,
1650
+ };
1651
+ this.#workInvalidators.add(work);
1652
+ const retiring = (): boolean => this.#retiredProviderWorkScopes.has(scopeKey);
1653
+ const ensureLive = async (): Promise<void> => {
1654
+ if (retiring()) {
1655
+ await terminalize();
1656
+ throw new SlackStaleEffectError();
1657
+ }
1658
+ if (invalidated || workGeneration !== this.#workGeneration) {
1659
+ await invalidate();
1660
+ throw new Error(`Slack effect ${id} was admitted after shutdown drain expiry`);
1661
+ }
1662
+ if (
1663
+ !(await this.#rescheduleAfterEffectTransition(
1664
+ this.#journal.renew(id, lease, Math.max(this.#publicationLeaseMs, 100)),
1665
+ ))
1666
+ )
1667
+ throw new Error("Slack effect lease renewal failed");
1668
+ if (retiring()) {
1669
+ await terminalize();
1670
+ throw new SlackStaleEffectError();
1671
+ }
1672
+ if (workGeneration !== this.#workGeneration) {
1673
+ await invalidate();
1674
+ throw new Error(`Slack effect ${id} lost its shutdown fence`);
1675
+ }
1676
+ };
1677
+ try {
1678
+ await ensureLive();
1679
+ const result = await this.#withRenewal(
1680
+ async () => {
1681
+ await ensureLive();
1682
+ try {
1683
+ return await operation(controller.signal);
1684
+ } catch (error) {
1685
+ if (retiring()) {
1686
+ await terminalize();
1687
+ throw new SlackStaleEffectError();
1688
+ }
1689
+ throw error;
1690
+ }
1691
+ },
1692
+ ensureLive,
1693
+ false,
1694
+ );
1695
+ await ensureLive();
1696
+ return result;
1697
+ } finally {
1698
+ this.#workInvalidators.delete(work);
1699
+ settled.resolve();
1700
+ this.#tryReleaseRetiredProviderWorkScope(scopeKey);
1701
+ }
1184
1702
  }
1185
1703
 
1186
1704
  async #drainPendingDispatches(): Promise<void> {
@@ -1198,8 +1716,12 @@ export class SlackNotificationDaemon {
1198
1716
  await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_mapping");
1199
1717
  continue;
1200
1718
  }
1201
- const endpoint = await this.#resolveEndpoint(record.sessionId!);
1202
- if (!endpoint || endpoint.generation !== receipt.endpointGeneration) {
1719
+ const endpoint = await this.#resolveAttachment(record.sessionId!);
1720
+ if (
1721
+ !endpoint ||
1722
+ endpoint.generation !== receipt.endpointGeneration ||
1723
+ record.attachmentAuthorityId !== endpoint.authorityId
1724
+ ) {
1203
1725
  await this.#terminalizeStaleInboundDispatch(key, receipt, "stale_binding");
1204
1726
  continue;
1205
1727
  }
@@ -1230,7 +1752,7 @@ export class SlackNotificationDaemon {
1230
1752
  continue;
1231
1753
  const adopted = await this.#adoptOrphanInbound(effect as ChatEffect<SlackInboundEffectPayload>);
1232
1754
  if (!adopted) continue;
1233
- const endpoint = await this.#resolveEndpoint(adopted.sessionId);
1755
+ const endpoint = await this.#resolveAttachment(adopted.sessionId);
1234
1756
  if (
1235
1757
  !endpoint ||
1236
1758
  endpoint.generation !== adopted.receipt.endpointGeneration ||
@@ -1299,6 +1821,7 @@ export class SlackNotificationDaemon {
1299
1821
  receipt.kind === routing.kind &&
1300
1822
  receipt.actionId === routing.actionId &&
1301
1823
  receipt.endpointGeneration === effect.endpointGeneration &&
1824
+ receipt.attachmentAuthorityId === routing.attachmentAuthorityId &&
1302
1825
  receipt.effectId === effect.id &&
1303
1826
  receipt.idempotencyKey === payload.idempotencyKey &&
1304
1827
  ((payload.type === "command" && routing.kind === "command" && effect.kind === "sdk.inbound.command") ||
@@ -1318,6 +1841,7 @@ export class SlackNotificationDaemon {
1318
1841
  left.kind === right.kind &&
1319
1842
  left.actionId === right.actionId &&
1320
1843
  left.endpointGeneration === right.endpointGeneration &&
1844
+ left.attachmentAuthorityId === right.attachmentAuthorityId &&
1321
1845
  left.effectId === right.effectId &&
1322
1846
  left.idempotencyKey === right.idempotencyKey
1323
1847
  );
@@ -1333,6 +1857,8 @@ export class SlackNotificationDaemon {
1333
1857
  !!record.sessionId &&
1334
1858
  !!record.rootTs &&
1335
1859
  acceptsSlackInbound(record, record.rootTs, receipt.endpointGeneration) &&
1860
+ record.attachmentAuthorityId === receipt.attachmentAuthorityId &&
1861
+ effect.payload.routing.attachmentAuthorityId === receipt.attachmentAuthorityId &&
1336
1862
  record.sessionId === effect.sessionId &&
1337
1863
  this.#matchesInboundEffect(effect, receipt)
1338
1864
  );
@@ -1440,7 +1966,8 @@ export class SlackNotificationDaemon {
1440
1966
  !current ||
1441
1967
  !effect.sessionId ||
1442
1968
  current.sessionId !== effect.sessionId ||
1443
- !acceptsSlackInbound(current, routing.rootTs, effect.endpointGeneration)
1969
+ !acceptsSlackInbound(current, routing.rootTs, effect.endpointGeneration) ||
1970
+ current.attachmentAuthorityId !== routing.attachmentAuthorityId
1444
1971
  )
1445
1972
  return current;
1446
1973
  const existing = (current.inboundDispatches ?? []).find(
@@ -1471,6 +1998,9 @@ export class SlackNotificationDaemon {
1471
1998
  kind: routing.kind,
1472
1999
  ...(routing.kind === "action" ? { actionId: routing.actionId } : {}),
1473
2000
  endpointGeneration: effect.endpointGeneration,
2001
+ ...(routing.attachmentAuthorityId === undefined
2002
+ ? {}
2003
+ : { attachmentAuthorityId: routing.attachmentAuthorityId }),
1474
2004
  effectId: effect.id,
1475
2005
  idempotencyKey: payload.idempotencyKey,
1476
2006
  };
@@ -1489,7 +2019,8 @@ export class SlackNotificationDaemon {
1489
2019
  }
1490
2020
 
1491
2021
  async #finishDispatch(
1492
- claim: { key: string; endpoint: SlackEndpoint; receipt: SlackInboundDispatchReceipt },
2022
+ claim: { key: string; endpoint: SessionAttachment; receipt: SlackInboundDispatchReceipt },
2023
+
1493
2024
  state: "terminal" | "uncertain",
1494
2025
  ): Promise<void> {
1495
2026
  await this.store.transact(claim.key, current => {
@@ -1503,7 +2034,7 @@ export class SlackNotificationDaemon {
1503
2034
 
1504
2035
  async #releaseDispatch(claim: {
1505
2036
  key: string;
1506
- endpoint: SlackEndpoint;
2037
+ endpoint: SessionAttachment;
1507
2038
  receipt: SlackInboundDispatchReceipt;
1508
2039
  }): Promise<void> {
1509
2040
  await this.store.transact(claim.key, current => {
@@ -1734,6 +2265,7 @@ export class SlackNotificationDaemon {
1734
2265
  text?: unknown;
1735
2266
  threadTs?: unknown;
1736
2267
  clientMsgId?: unknown;
2268
+ attachmentAuthorityId?: unknown;
1737
2269
  };
1738
2270
  if (
1739
2271
  typeof payload.channel !== "string" ||
@@ -1747,6 +2279,9 @@ export class SlackNotificationDaemon {
1747
2279
  text: payload.text,
1748
2280
  ...(typeof payload.threadTs === "string" ? { threadTs: payload.threadTs } : {}),
1749
2281
  clientMsgId: payload.clientMsgId,
2282
+ ...(typeof payload.attachmentAuthorityId === "string"
2283
+ ? { attachmentAuthorityId: payload.attachmentAuthorityId }
2284
+ : {}),
1750
2285
  });
1751
2286
  } catch {
1752
2287
  failed = true;
@@ -1855,22 +2390,32 @@ export class SlackNotificationDaemon {
1855
2390
  async #providerEffectCurrent(effect: ChatEffect): Promise<boolean> {
1856
2391
  if (!effect.sessionId || !Number.isSafeInteger(effect.endpointGeneration) || effect.endpointGeneration <= 0)
1857
2392
  return false;
1858
- const endpoint = await this.#resolveEndpoint(effect.sessionId);
1859
- if (!endpoint || endpoint.generation !== effect.endpointGeneration) return false;
1860
- const payload = effect.payload as { threadTs?: unknown };
2393
+ if (!effect.id.startsWith("close-marker-cleanup:")) {
2394
+ const endpoint = await this.#resolveAttachment(effect.sessionId);
2395
+ const payload = effect.payload as { attachmentAuthorityId?: unknown; threadTs?: unknown };
2396
+ if (
2397
+ !endpoint ||
2398
+ endpoint.generation !== effect.endpointGeneration ||
2399
+ payload.attachmentAuthorityId !== endpoint.authorityId
2400
+ )
2401
+ return false;
2402
+ }
2403
+ const payload = effect.payload as { attachmentAuthorityId?: unknown; threadTs?: unknown };
1861
2404
  const threadTs = payload.threadTs;
1862
2405
  const records = Object.values((await this.store.load()).conversations);
1863
2406
  if (typeof threadTs === "string") {
1864
2407
  return records.some(
1865
2408
  record =>
1866
2409
  record.sessionId === effect.sessionId &&
1867
- acceptsSlackInbound(record, threadTs, effect.endpointGeneration),
2410
+ acceptsSlackInbound(record, threadTs, effect.endpointGeneration) &&
2411
+ record.attachmentAuthorityId === payload.attachmentAuthorityId,
1868
2412
  );
1869
2413
  }
1870
2414
  return records.some(
1871
2415
  record =>
1872
2416
  record.sessionId === effect.sessionId &&
1873
2417
  record.endpointGeneration === effect.endpointGeneration &&
2418
+ record.attachmentAuthorityId === payload.attachmentAuthorityId &&
1874
2419
  record.state === "posting_root",
1875
2420
  );
1876
2421
  }
@@ -1897,23 +2442,34 @@ export class SlackNotificationDaemon {
1897
2442
  id: string,
1898
2443
  sessionId: string,
1899
2444
  endpointGeneration: number,
1900
- expected: { channel: string; text: string; threadTs?: string; clientMsgId: string },
1901
- ): Promise<{ payload: typeof expected; existed: boolean }> {
1902
- const existing = await this.#journal.read<typeof expected>(id);
1903
- if (!existing) return { payload: expected, existed: false };
2445
+ expected: {
2446
+ channel: string;
2447
+ text: string;
2448
+ threadTs?: string;
2449
+ clientMsgId: string;
2450
+ attachmentAuthorityId?: string;
2451
+ },
2452
+ ): Promise<{
2453
+ payload: typeof expected & { attachmentAuthorityId?: string };
2454
+ existed: boolean;
2455
+ }> {
2456
+ const durableExpected = { ...expected };
2457
+ const existing = await this.#journal.read<typeof durableExpected>(id);
2458
+ if (!existing) return { payload: durableExpected, existed: false };
1904
2459
  const payload = existing.payload;
1905
2460
  if (
1906
2461
  existing.kind !== "provider-post" ||
1907
2462
  existing.transport !== "slack" ||
1908
2463
  existing.sessionId !== sessionId ||
1909
2464
  existing.endpointGeneration !== endpointGeneration ||
2465
+ payload.attachmentAuthorityId !== durableExpected.attachmentAuthorityId ||
1910
2466
  typeof payload.channel !== "string" ||
1911
2467
  typeof payload.text !== "string" ||
1912
2468
  typeof payload.clientMsgId !== "string" ||
1913
2469
  (payload.threadTs !== undefined && typeof payload.threadTs !== "string") ||
1914
- payload.channel !== expected.channel ||
1915
- payload.threadTs !== expected.threadTs ||
1916
- payload.clientMsgId !== expected.clientMsgId
2470
+ payload.channel !== durableExpected.channel ||
2471
+ payload.threadTs !== durableExpected.threadTs ||
2472
+ payload.clientMsgId !== durableExpected.clientMsgId
1917
2473
  ) {
1918
2474
  throw new SlackEndpointBindingError("Slack provider effect identity does not match the durable occurrence.");
1919
2475
  }
@@ -1924,10 +2480,17 @@ export class SlackNotificationDaemon {
1924
2480
  id: string,
1925
2481
  sessionId: string,
1926
2482
  endpointGeneration: number,
1927
- payload: { channel: string; text: string; threadTs?: string; clientMsgId: string },
2483
+ payload: {
2484
+ channel: string;
2485
+ text: string;
2486
+ threadTs?: string;
2487
+ clientMsgId: string;
2488
+ attachmentAuthorityId?: string;
2489
+ },
1928
2490
  ): Promise<SlackPostedMessage> {
1929
2491
  if (!Number.isSafeInteger(endpointGeneration) || endpointGeneration <= 0)
1930
2492
  throw new SlackEndpointBindingError("Slack provider effects require a positive endpoint generation.");
2493
+ const workGeneration = this.#workGeneration;
1931
2494
  const initial = await this.#rescheduleAfterEffectTransition(
1932
2495
  this.#journal.enqueue({
1933
2496
  id,
@@ -1971,6 +2534,13 @@ export class SlackNotificationDaemon {
1971
2534
  }
1972
2535
  if (!effect) throw new Error("Slack provider effect is owned by another worker");
1973
2536
  const lease: ChatEffectLease = { owner: this.#publicationOwnerId, epoch: effect.epoch };
2537
+ const workScope: SlackProviderWorkScope = {
2538
+ sessionId,
2539
+ endpointGeneration,
2540
+ ...(payload.attachmentAuthorityId === undefined
2541
+ ? {}
2542
+ : { attachmentAuthorityId: payload.attachmentAuthorityId }),
2543
+ };
1974
2544
  // A recovered lease may have crossed the provider boundary before its owner
1975
2545
  // died, and a fresh post may have reached Slack at a now-superseded generation.
1976
2546
  // Reconcile first in all cases. Only an effect that may have crossed the
@@ -1979,19 +2549,30 @@ export class SlackNotificationDaemon {
1979
2549
  const requiresReconciliation =
1980
2550
  initial.state === "accepted" || initial.state === "uncertain" || initial.state === "leased";
1981
2551
  try {
1982
- const posted = await this.#withEffectLease(id, lease, async () => {
1983
- const found = await this.options.provider.findMessageByClientMsgId({
1984
- channel: effect!.payload.channel,
1985
- threadTs: effect!.payload.threadTs,
1986
- clientMsgId: effect!.payload.clientMsgId,
1987
- });
1988
- if (found) return found;
1989
- if (!(await this.#providerEffectCurrent(effect!))) {
1990
- if (requiresReconciliation) throw new SlackReconciledAbsentEffectError();
1991
- throw new SlackStaleEffectError();
1992
- }
1993
- return await this.options.provider.postMessage(effect!.payload);
1994
- });
2552
+ const posted = await this.#withEffectLease(
2553
+ id,
2554
+ lease,
2555
+ workScope,
2556
+ async signal => {
2557
+ const found = await this.options.provider.findMessageByClientMsgId({
2558
+ channel: effect!.payload.channel,
2559
+ threadTs: effect!.payload.threadTs,
2560
+ clientMsgId: effect!.payload.clientMsgId,
2561
+ signal,
2562
+ });
2563
+ if (found) return found;
2564
+ if (workGeneration !== this.#workGeneration)
2565
+ throw new Error(`Slack effect ${id} lost its shutdown fence`);
2566
+ if (!(await this.#providerEffectCurrent(effect!))) {
2567
+ if (requiresReconciliation) throw new SlackReconciledAbsentEffectError();
2568
+ throw new SlackStaleEffectError();
2569
+ }
2570
+ if (workGeneration !== this.#workGeneration)
2571
+ throw new Error(`Slack effect ${id} lost its shutdown fence`);
2572
+ return await this.options.provider.postMessage({ ...effect!.payload, signal });
2573
+ },
2574
+ workGeneration,
2575
+ );
1995
2576
 
1996
2577
  if (
1997
2578
  !(await this.#journal.record(id, lease, "terminal", {
@@ -2018,12 +2599,15 @@ export class SlackNotificationDaemon {
2018
2599
  const reconciled = await this.#withEffectLease(
2019
2600
  id,
2020
2601
  lease,
2021
- async () =>
2602
+ workScope,
2603
+ async signal =>
2022
2604
  await this.options.provider.findMessageByClientMsgId({
2023
2605
  channel: effect!.payload.channel,
2024
2606
  threadTs: effect!.payload.threadTs,
2025
2607
  clientMsgId: effect!.payload.clientMsgId,
2608
+ signal,
2026
2609
  }),
2610
+ workGeneration,
2027
2611
  );
2028
2612
  if (reconciled) {
2029
2613
  if (
@@ -2096,7 +2680,18 @@ export class SlackNotificationDaemon {
2096
2680
  }
2097
2681
 
2098
2682
  async #withPublicationLease<T>(operation: () => Promise<T>, renew: () => Promise<void>): Promise<T> {
2099
- return await this.#withRenewal(operation, renew);
2683
+ const workGeneration = this.#workGeneration;
2684
+ const fencedRenew = async (): Promise<void> => {
2685
+ if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
2686
+ await renew();
2687
+ if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
2688
+ };
2689
+ const result = await this.#withRenewal(async () => {
2690
+ if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
2691
+ return await operation();
2692
+ }, fencedRenew);
2693
+ if (workGeneration !== this.#workGeneration) throw new Error("Slack publication lost its shutdown fence");
2694
+ return result;
2100
2695
  }
2101
2696
 
2102
2697
  async #withRenewal<T>(
@@ -2191,6 +2786,9 @@ export class SlackNotificationDaemon {
2191
2786
  channel: payload.channel,
2192
2787
  text: payload.text,
2193
2788
  clientMsgId,
2789
+ ...(current.attachmentAuthorityId === undefined
2790
+ ? {}
2791
+ : { attachmentAuthorityId: current.attachmentAuthorityId }),
2194
2792
  });
2195
2793
  } catch (error) {
2196
2794
  if (!(error instanceof SlackStaleEffectError)) throw error;
@@ -2302,6 +2900,7 @@ export class SlackNotificationDaemon {
2302
2900
  }
2303
2901
  #isDefiniteSdkPreSendFailure(error: unknown): boolean {
2304
2902
  if (error instanceof SlackEndpointBindingError) return true;
2903
+ if (error instanceof SessionRouterError) return error.phase === "pre_send";
2305
2904
  if (error instanceof SdkClientError) return error.code === "connection_closed";
2306
2905
  return (
2307
2906
  error instanceof Error &&