@openparachute/agent 0.1.2 → 0.2.2

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 (608) hide show
  1. package/.parachute/module.json +124 -8
  2. package/LICENSE +2 -16
  3. package/README.md +118 -166
  4. package/package.json +35 -42
  5. package/scripts/spawn-agent.ts +371 -0
  6. package/src/_parked/interactive-spawn.test.ts +324 -0
  7. package/src/_parked/interactive-spawn.ts +701 -0
  8. package/src/agent-defs.test.ts +1504 -0
  9. package/src/agent-defs.ts +1702 -0
  10. package/src/agent-mcp-config.test.ts +115 -0
  11. package/src/agent-mcp-config.ts +115 -0
  12. package/src/agents.test.ts +360 -0
  13. package/src/agents.ts +379 -0
  14. package/src/auth.test.ts +46 -0
  15. package/src/auth.ts +140 -0
  16. package/src/backends/attached-queue.test.ts +376 -0
  17. package/src/backends/attached-queue.ts +372 -0
  18. package/src/backends/programmatic.test.ts +1715 -0
  19. package/src/backends/programmatic.ts +927 -0
  20. package/src/backends/registry.test.ts +1494 -0
  21. package/src/backends/registry.ts +1202 -0
  22. package/src/backends/stream-json.test.ts +570 -0
  23. package/src/backends/stream-json.ts +392 -0
  24. package/src/backends/types.ts +223 -0
  25. package/src/bridge.ts +417 -0
  26. package/src/channel-backend-wiring.test.ts +237 -0
  27. package/src/credentials.test.ts +274 -0
  28. package/src/credentials.ts +380 -0
  29. package/src/cron.test.ts +342 -0
  30. package/src/cron.ts +380 -0
  31. package/src/daemon-agent-def-api.test.ts +166 -0
  32. package/src/daemon-agent-defs-api.test.ts +953 -0
  33. package/src/daemon-agent-env-api.test.ts +338 -0
  34. package/src/daemon-attached-queue-store.test.ts +65 -0
  35. package/src/daemon-config-api.test.ts +962 -0
  36. package/src/daemon-jobs-api.test.ts +271 -0
  37. package/src/daemon-vault-chat.test.ts +250 -0
  38. package/src/daemon.test.ts +746 -0
  39. package/src/daemon.ts +3314 -0
  40. package/src/def-vaults.test.ts +136 -0
  41. package/src/def-vaults.ts +165 -0
  42. package/src/delivery-state.test.ts +110 -0
  43. package/src/delivery-state.ts +154 -0
  44. package/src/effective-env.test.ts +114 -0
  45. package/src/effective-env.ts +184 -0
  46. package/src/env-compat.ts +39 -0
  47. package/src/grants.test.ts +638 -0
  48. package/src/grants.ts +675 -0
  49. package/src/hub-jwt.test.ts +161 -0
  50. package/src/hub-jwt.ts +182 -0
  51. package/src/jobs.test.ts +245 -0
  52. package/src/jobs.ts +266 -0
  53. package/src/mcp-http.test.ts +265 -0
  54. package/src/mcp-http.ts +771 -0
  55. package/src/mint-token.test.ts +152 -0
  56. package/src/mint-token.ts +139 -0
  57. package/src/module-manifest.test.ts +158 -0
  58. package/src/oauth-discovery.ts +134 -0
  59. package/src/programmatic-wiring.test.ts +838 -0
  60. package/src/registry.test.ts +227 -0
  61. package/src/registry.ts +228 -0
  62. package/src/resolve-port.test.ts +64 -0
  63. package/src/routing.test.ts +184 -0
  64. package/src/routing.ts +76 -0
  65. package/src/runner.test.ts +506 -0
  66. package/src/runner.ts +255 -0
  67. package/src/sandbox/config.test.ts +150 -0
  68. package/src/sandbox/config.ts +102 -0
  69. package/src/sandbox/egress.test.ts +113 -0
  70. package/src/sandbox/egress.ts +123 -0
  71. package/src/sandbox/index.ts +180 -0
  72. package/src/sandbox/live-seatbelt.test.ts +277 -0
  73. package/src/sandbox/mounts.test.ts +154 -0
  74. package/src/sandbox/mounts.ts +133 -0
  75. package/src/sandbox/sandbox.test.ts +168 -0
  76. package/src/sandbox/types.ts +382 -0
  77. package/src/services-manifest.test.ts +106 -0
  78. package/src/services-manifest.ts +95 -0
  79. package/src/spa-serve.test.ts +116 -0
  80. package/src/spa-serve.ts +116 -0
  81. package/src/spawn-agent-cli.test.ts +172 -0
  82. package/src/spawn-agent.test.ts +1218 -0
  83. package/src/spawn-agent.ts +569 -0
  84. package/src/spawn-deps.test.ts +54 -0
  85. package/src/spawn-deps.ts +166 -0
  86. package/src/telegram/api.ts +153 -0
  87. package/src/terminal-assets.test.ts +50 -0
  88. package/src/terminal-assets.ts +79 -0
  89. package/src/terminal-ui.ts +305 -0
  90. package/src/terminal.test.ts +530 -0
  91. package/src/terminal.ts +458 -0
  92. package/src/transport.ts +270 -0
  93. package/src/transports/http-ui.test.ts +455 -0
  94. package/src/transports/http-ui.ts +201 -0
  95. package/src/transports/telegram.test.ts +174 -0
  96. package/src/transports/telegram.ts +426 -0
  97. package/src/transports/vault.test.ts +2011 -0
  98. package/src/transports/vault.ts +1790 -0
  99. package/src/ui-kit.test.ts +178 -0
  100. package/src/ui-kit.ts +402 -0
  101. package/tsconfig.json +8 -14
  102. package/web/ui/dist/assets/index-C-iWdFFV.css +1 -0
  103. package/web/ui/dist/assets/index-VFETBk0a.js +60 -0
  104. package/web/ui/dist/index.html +15 -0
  105. package/web/ui/tsconfig.json +2 -1
  106. package/.claude/scheduled_tasks.lock +0 -1
  107. package/.claude/settings.json +0 -5
  108. package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
  109. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
  110. package/.claude/skills/add-codex/SKILL.md +0 -161
  111. package/.claude/skills/add-dashboard/SKILL.md +0 -138
  112. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
  113. package/.claude/skills/add-emacs/SKILL.md +0 -296
  114. package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
  115. package/.claude/skills/add-gchat/REMOVE.md +0 -6
  116. package/.claude/skills/add-gchat/SKILL.md +0 -92
  117. package/.claude/skills/add-gchat/VERIFY.md +0 -3
  118. package/.claude/skills/add-github/REMOVE.md +0 -6
  119. package/.claude/skills/add-github/SKILL.md +0 -148
  120. package/.claude/skills/add-github/VERIFY.md +0 -3
  121. package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
  122. package/.claude/skills/add-imessage/REMOVE.md +0 -6
  123. package/.claude/skills/add-imessage/SKILL.md +0 -113
  124. package/.claude/skills/add-imessage/VERIFY.md +0 -3
  125. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
  126. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
  127. package/.claude/skills/add-linear/REMOVE.md +0 -6
  128. package/.claude/skills/add-linear/SKILL.md +0 -168
  129. package/.claude/skills/add-linear/VERIFY.md +0 -3
  130. package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
  131. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
  132. package/.claude/skills/add-matrix/REMOVE.md +0 -6
  133. package/.claude/skills/add-matrix/SKILL.md +0 -148
  134. package/.claude/skills/add-matrix/VERIFY.md +0 -3
  135. package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
  136. package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
  137. package/.claude/skills/add-opencode/SKILL.md +0 -229
  138. package/.claude/skills/add-parallel/SKILL.md +0 -290
  139. package/.claude/skills/add-resend/REMOVE.md +0 -6
  140. package/.claude/skills/add-resend/SKILL.md +0 -93
  141. package/.claude/skills/add-resend/VERIFY.md +0 -3
  142. package/.claude/skills/add-signal/REMOVE.md +0 -13
  143. package/.claude/skills/add-signal/SKILL.md +0 -318
  144. package/.claude/skills/add-signal/VERIFY.md +0 -5
  145. package/.claude/skills/add-slack/REMOVE.md +0 -6
  146. package/.claude/skills/add-slack/SKILL.md +0 -112
  147. package/.claude/skills/add-slack/VERIFY.md +0 -3
  148. package/.claude/skills/add-teams/REMOVE.md +0 -6
  149. package/.claude/skills/add-teams/SKILL.md +0 -207
  150. package/.claude/skills/add-teams/VERIFY.md +0 -3
  151. package/.claude/skills/add-vercel/SKILL.md +0 -147
  152. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
  153. package/.claude/skills/add-webex/REMOVE.md +0 -6
  154. package/.claude/skills/add-webex/SKILL.md +0 -88
  155. package/.claude/skills/add-webex/VERIFY.md +0 -3
  156. package/.claude/skills/add-wechat/REMOVE.md +0 -49
  157. package/.claude/skills/add-wechat/SKILL.md +0 -170
  158. package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
  159. package/.claude/skills/add-whatsapp/SKILL.md +0 -264
  160. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
  161. package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
  162. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
  163. package/.claude/skills/claw/SKILL.md +0 -131
  164. package/.claude/skills/claw/scripts/claw +0 -374
  165. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
  166. package/.claude/skills/customize/SKILL.md +0 -110
  167. package/.claude/skills/debug/SKILL.md +0 -349
  168. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  169. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  170. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  171. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  172. package/.claude/skills/init-first-agent/SKILL.md +0 -120
  173. package/.claude/skills/init-onecli/SKILL.md +0 -270
  174. package/.claude/skills/manage-channels/SKILL.md +0 -87
  175. package/.claude/skills/manage-mounts/SKILL.md +0 -47
  176. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
  177. package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
  178. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
  179. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
  180. package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
  181. package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
  182. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  183. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  184. package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
  185. package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
  186. package/.claude/skills/update-skills/SKILL.md +0 -130
  187. package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
  188. package/.claude/skills/x-integration/SKILL.md +0 -417
  189. package/.claude/skills/x-integration/agent.ts +0 -243
  190. package/.claude/skills/x-integration/host.ts +0 -155
  191. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  192. package/.claude/skills/x-integration/lib/config.ts +0 -62
  193. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  194. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  195. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  196. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  197. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  198. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  199. package/.github/CODEOWNERS +0 -10
  200. package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
  201. package/.github/workflows/bump-version.yml +0 -35
  202. package/.github/workflows/ci.yml +0 -39
  203. package/.github/workflows/label-pr.yml +0 -40
  204. package/.github/workflows/update-tokens.yml +0 -43
  205. package/.husky/pre-commit +0 -1
  206. package/.mcp.json +0 -3
  207. package/.nvmrc +0 -1
  208. package/.prettierrc +0 -4
  209. package/CHANGELOG.md +0 -263
  210. package/CLAUDE.md +0 -307
  211. package/CODE_OF_CONDUCT.md +0 -128
  212. package/CONTRIBUTING.md +0 -159
  213. package/CONTRIBUTORS.md +0 -26
  214. package/LICENSE-NANOCLAW-MIT +0 -21
  215. package/README_ja.md +0 -194
  216. package/README_zh.md +0 -194
  217. package/assets/nanoclaw-favicon.png +0 -0
  218. package/assets/nanoclaw-icon.png +0 -0
  219. package/assets/nanoclaw-logo-dark.png +0 -0
  220. package/assets/nanoclaw-logo.png +0 -0
  221. package/assets/nanoclaw-profile.jpeg +0 -0
  222. package/assets/nanoclaw-sales.png +0 -0
  223. package/assets/social-preview.jpg +0 -0
  224. package/config-examples/mount-allowlist.json +0 -25
  225. package/container/.dockerignore +0 -2
  226. package/container/CLAUDE.md +0 -21
  227. package/container/Dockerfile +0 -121
  228. package/container/agent-runner/bun.lock +0 -243
  229. package/container/agent-runner/package.json +0 -22
  230. package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
  231. package/container/agent-runner/src/config.ts +0 -55
  232. package/container/agent-runner/src/db/connection.ts +0 -267
  233. package/container/agent-runner/src/db/index.ts +0 -20
  234. package/container/agent-runner/src/db/messages-in.ts +0 -138
  235. package/container/agent-runner/src/db/messages-out.ts +0 -143
  236. package/container/agent-runner/src/db/session-routing.ts +0 -30
  237. package/container/agent-runner/src/db/session-state.test.ts +0 -100
  238. package/container/agent-runner/src/db/session-state.ts +0 -79
  239. package/container/agent-runner/src/destinations.ts +0 -135
  240. package/container/agent-runner/src/formatter.test.ts +0 -167
  241. package/container/agent-runner/src/formatter.ts +0 -260
  242. package/container/agent-runner/src/index.ts +0 -110
  243. package/container/agent-runner/src/integration.test.ts +0 -121
  244. package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
  245. package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
  246. package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
  247. package/container/agent-runner/src/mcp-tools/core.ts +0 -262
  248. package/container/agent-runner/src/mcp-tools/index.ts +0 -22
  249. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
  250. package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
  251. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
  252. package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
  253. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
  254. package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
  255. package/container/agent-runner/src/mcp-tools/server.ts +0 -54
  256. package/container/agent-runner/src/mcp-tools/types.ts +0 -6
  257. package/container/agent-runner/src/poll-loop.test.ts +0 -248
  258. package/container/agent-runner/src/poll-loop.ts +0 -437
  259. package/container/agent-runner/src/providers/claude.ts +0 -379
  260. package/container/agent-runner/src/providers/factory.test.ts +0 -19
  261. package/container/agent-runner/src/providers/factory.ts +0 -13
  262. package/container/agent-runner/src/providers/index.ts +0 -6
  263. package/container/agent-runner/src/providers/mock.ts +0 -77
  264. package/container/agent-runner/src/providers/provider-registry.ts +0 -33
  265. package/container/agent-runner/src/providers/types.ts +0 -82
  266. package/container/agent-runner/src/scheduling/task-script.ts +0 -121
  267. package/container/agent-runner/src/timezone.test.ts +0 -93
  268. package/container/agent-runner/src/timezone.ts +0 -107
  269. package/container/agent-runner/tsconfig.json +0 -14
  270. package/container/build.sh +0 -48
  271. package/container/entrypoint.sh +0 -16
  272. package/container/skills/agent-browser/SKILL.md +0 -159
  273. package/container/skills/frontend-engineer/SKILL.md +0 -157
  274. package/container/skills/self-customize/SKILL.md +0 -87
  275. package/container/skills/slack-formatting/SKILL.md +0 -94
  276. package/container/skills/vercel-cli/SKILL.md +0 -111
  277. package/container/skills/welcome/SKILL.md +0 -85
  278. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  279. package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
  280. package/docs/README.md +0 -25
  281. package/docs/SDK_DEEP_DIVE.md +0 -643
  282. package/docs/SECURITY.md +0 -162
  283. package/docs/agent-runner-details.md +0 -749
  284. package/docs/api-details.md +0 -365
  285. package/docs/architecture-diagram.html +0 -422
  286. package/docs/architecture-diagram.md +0 -215
  287. package/docs/architecture.md +0 -751
  288. package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
  289. package/docs/build-and-runtime.md +0 -80
  290. package/docs/cross-mount-stress/README.md +0 -112
  291. package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
  292. package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
  293. package/docs/cross-mount-stress/container-writer.mjs +0 -47
  294. package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
  295. package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
  296. package/docs/cross-mount-stress/host-writer.mjs +0 -47
  297. package/docs/db-central.md +0 -316
  298. package/docs/db-session.md +0 -183
  299. package/docs/db.md +0 -119
  300. package/docs/design/2026-04-29-vault-management-ui.md +0 -231
  301. package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
  302. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
  303. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
  304. package/docs/docker-sandboxes.md +0 -359
  305. package/docs/isolation-model.md +0 -88
  306. package/docs/ollama.md +0 -79
  307. package/docs/parachute-integration.md +0 -109
  308. package/docs/post-night-rebirth-reflections.md +0 -151
  309. package/eslint.config.js +0 -32
  310. package/pnpm-workspace.yaml +0 -8
  311. package/repo-tokens/README.md +0 -113
  312. package/repo-tokens/action.yml +0 -186
  313. package/repo-tokens/badge.svg +0 -23
  314. package/repo-tokens/examples/green.svg +0 -14
  315. package/repo-tokens/examples/red.svg +0 -14
  316. package/repo-tokens/examples/yellow-green.svg +0 -14
  317. package/repo-tokens/examples/yellow.svg +0 -14
  318. package/scripts/chat.ts +0 -101
  319. package/scripts/cleanup-sessions.sh +0 -150
  320. package/scripts/init-cli-agent.ts +0 -172
  321. package/scripts/init-first-agent.ts +0 -378
  322. package/scripts/parachute.ts +0 -158
  323. package/scripts/run-migrations.ts +0 -105
  324. package/scripts/sanity-live-poll.ts +0 -95
  325. package/scripts/seed-discord.ts +0 -80
  326. package/scripts/test-v2-agent.ts +0 -106
  327. package/scripts/test-v2-channel-e2e.ts +0 -265
  328. package/scripts/test-v2-host.ts +0 -184
  329. package/src/channels/adapter.ts +0 -214
  330. package/src/channels/api-translator.test.ts +0 -306
  331. package/src/channels/api-translator.ts +0 -214
  332. package/src/channels/ask-question.ts +0 -46
  333. package/src/channels/channel-registry.test.ts +0 -421
  334. package/src/channels/channel-registry.ts +0 -313
  335. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  336. package/src/channels/chat-sdk-bridge.ts +0 -652
  337. package/src/channels/cli.ts +0 -276
  338. package/src/channels/discord.ts +0 -90
  339. package/src/channels/index.ts +0 -17
  340. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  341. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  342. package/src/channels/telegram-pairing.test.ts +0 -254
  343. package/src/channels/telegram-pairing.ts +0 -339
  344. package/src/channels/telegram.ts +0 -279
  345. package/src/channels/trust-hint.test.ts +0 -48
  346. package/src/channels/trust-hint.ts +0 -75
  347. package/src/claude-md-compose.migrate.test.ts +0 -64
  348. package/src/claude-md-compose.ts +0 -205
  349. package/src/command-gate.ts +0 -63
  350. package/src/config.test.ts +0 -93
  351. package/src/config.ts +0 -128
  352. package/src/container-config.ts +0 -167
  353. package/src/container-runner.test.ts +0 -32
  354. package/src/container-runner.ts +0 -576
  355. package/src/container-runtime.test.ts +0 -269
  356. package/src/container-runtime.ts +0 -167
  357. package/src/db/_bun-sqlite-shim.ts +0 -88
  358. package/src/db/agent-activity.test.ts +0 -155
  359. package/src/db/agent-activity.ts +0 -121
  360. package/src/db/agent-groups.ts +0 -77
  361. package/src/db/connection.migrate.test.ts +0 -176
  362. package/src/db/connection.ts +0 -259
  363. package/src/db/db-v2.test.ts +0 -440
  364. package/src/db/dropped-messages.ts +0 -44
  365. package/src/db/index.ts +0 -40
  366. package/src/db/messaging-groups.ts +0 -252
  367. package/src/db/migrations/001-initial.ts +0 -112
  368. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  369. package/src/db/migrations/008-dropped-messages.ts +0 -27
  370. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  371. package/src/db/migrations/010-engage-modes.ts +0 -103
  372. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  373. package/src/db/migrations/012-channel-registration.ts +0 -48
  374. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  375. package/src/db/migrations/014-secrets.ts +0 -44
  376. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  377. package/src/db/migrations/016-secret-assignments.ts +0 -30
  378. package/src/db/migrations/017-agent-activity.ts +0 -40
  379. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  380. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  381. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  382. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  383. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  384. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  385. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  386. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  387. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  388. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  389. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  390. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  391. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  392. package/src/db/migrations/027-provider-credentials.ts +0 -41
  393. package/src/db/migrations/_test-helpers.ts +0 -41
  394. package/src/db/migrations/index.ts +0 -127
  395. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  396. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  397. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  398. package/src/db/schema.ts +0 -258
  399. package/src/db/session-db.test.ts +0 -93
  400. package/src/db/session-db.ts +0 -325
  401. package/src/db/sessions.ts +0 -241
  402. package/src/delivery.test.ts +0 -148
  403. package/src/delivery.ts +0 -445
  404. package/src/env.ts +0 -74
  405. package/src/group-folder.test.ts +0 -35
  406. package/src/group-folder.ts +0 -44
  407. package/src/group-init.ts +0 -92
  408. package/src/host-core.test.ts +0 -456
  409. package/src/host-sweep.test.ts +0 -146
  410. package/src/host-sweep.ts +0 -287
  411. package/src/index.ts +0 -232
  412. package/src/install-slug.ts +0 -33
  413. package/src/log.test.ts +0 -81
  414. package/src/log.ts +0 -117
  415. package/src/mcp/http.ts +0 -72
  416. package/src/mcp/server.ts +0 -92
  417. package/src/mcp/stdio.ts +0 -51
  418. package/src/mcp/tools/activity.ts +0 -88
  419. package/src/mcp/tools/agent-groups.ts +0 -183
  420. package/src/mcp/tools/approvals.ts +0 -122
  421. package/src/mcp/tools/channels.test.ts +0 -126
  422. package/src/mcp/tools/channels.ts +0 -134
  423. package/src/mcp/tools/index.ts +0 -27
  424. package/src/mcp/tools/oauth.ts +0 -48
  425. package/src/mcp/tools/secrets.ts +0 -169
  426. package/src/mcp/tools/sessions.ts +0 -135
  427. package/src/mcp/types.ts +0 -51
  428. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  429. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  430. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  431. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  432. package/src/modules/agent-to-agent/index.ts +0 -22
  433. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  434. package/src/modules/approvals/agent.md +0 -45
  435. package/src/modules/approvals/index.ts +0 -21
  436. package/src/modules/approvals/picks.test.ts +0 -291
  437. package/src/modules/approvals/primitive.ts +0 -279
  438. package/src/modules/approvals/project.md +0 -27
  439. package/src/modules/approvals/response-handler.ts +0 -87
  440. package/src/modules/index.ts +0 -24
  441. package/src/modules/interactive/agent.md +0 -21
  442. package/src/modules/interactive/index.ts +0 -69
  443. package/src/modules/interactive/project.md +0 -12
  444. package/src/modules/mount-security/expand-path.test.ts +0 -82
  445. package/src/modules/mount-security/index.ts +0 -459
  446. package/src/modules/mount-security/migrate.test.ts +0 -91
  447. package/src/modules/permissions/access.ts +0 -28
  448. package/src/modules/permissions/channel-approval.test.ts +0 -389
  449. package/src/modules/permissions/channel-approval.ts +0 -188
  450. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  451. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  452. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  453. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  454. package/src/modules/permissions/db/user-dms.ts +0 -58
  455. package/src/modules/permissions/db/user-roles.ts +0 -85
  456. package/src/modules/permissions/db/users.ts +0 -38
  457. package/src/modules/permissions/index.ts +0 -421
  458. package/src/modules/permissions/permissions.test.ts +0 -358
  459. package/src/modules/permissions/sender-approval.test.ts +0 -641
  460. package/src/modules/permissions/sender-approval.ts +0 -165
  461. package/src/modules/permissions/user-dm.ts +0 -200
  462. package/src/modules/provider-credentials/db.ts +0 -121
  463. package/src/modules/provider-credentials/index.ts +0 -12
  464. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  465. package/src/modules/provider-credentials/spawn.ts +0 -114
  466. package/src/modules/scheduling/actions.ts +0 -113
  467. package/src/modules/scheduling/db.test.ts +0 -282
  468. package/src/modules/scheduling/db.ts +0 -148
  469. package/src/modules/scheduling/index.ts +0 -34
  470. package/src/modules/scheduling/recurrence.test.ts +0 -98
  471. package/src/modules/scheduling/recurrence.ts +0 -54
  472. package/src/modules/self-mod/agent.md +0 -30
  473. package/src/modules/self-mod/apply.ts +0 -85
  474. package/src/modules/self-mod/index.ts +0 -30
  475. package/src/modules/self-mod/project.md +0 -39
  476. package/src/modules/self-mod/request.ts +0 -91
  477. package/src/modules/typing/index.ts +0 -165
  478. package/src/oauth/agent-app-connections.ts +0 -103
  479. package/src/oauth/app-configs.test.ts +0 -64
  480. package/src/oauth/app-configs.ts +0 -114
  481. package/src/oauth/app-connections.test.ts +0 -109
  482. package/src/oauth/app-connections.ts +0 -178
  483. package/src/oauth/crypto.ts +0 -56
  484. package/src/oauth/flow.ts +0 -104
  485. package/src/oauth/providers/google.test.ts +0 -38
  486. package/src/oauth/providers/google.ts +0 -46
  487. package/src/oauth/providers/index.ts +0 -48
  488. package/src/oauth/state-store.test.ts +0 -54
  489. package/src/oauth/state-store.ts +0 -93
  490. package/src/parachute/README.md +0 -27
  491. package/src/parachute/create-agent.test.ts +0 -83
  492. package/src/parachute/create-agent.ts +0 -122
  493. package/src/parachute/group-status.test.ts +0 -165
  494. package/src/parachute/group-status.ts +0 -136
  495. package/src/parachute/types.ts +0 -41
  496. package/src/parachute/vault-mcp.test.ts +0 -251
  497. package/src/parachute/vault-mcp.ts +0 -232
  498. package/src/platform-id.test.ts +0 -104
  499. package/src/platform-id.ts +0 -109
  500. package/src/providers/index.ts +0 -6
  501. package/src/providers/provider-container-registry.ts +0 -58
  502. package/src/response-registry.ts +0 -45
  503. package/src/router.ts +0 -530
  504. package/src/secrets/crypto.test.ts +0 -45
  505. package/src/secrets/crypto.ts +0 -55
  506. package/src/secrets/index.ts +0 -461
  507. package/src/secrets/master-key.ts +0 -70
  508. package/src/secrets/secrets.test.ts +0 -651
  509. package/src/session-manager.attachments.test.ts +0 -171
  510. package/src/session-manager.dup-skip.test.ts +0 -173
  511. package/src/session-manager.migrate.test.ts +0 -59
  512. package/src/session-manager.ts +0 -451
  513. package/src/startup-bootstrap.test.ts +0 -226
  514. package/src/startup-bootstrap.ts +0 -207
  515. package/src/state-sqlite.ts +0 -182
  516. package/src/timezone.test.ts +0 -64
  517. package/src/timezone.ts +0 -37
  518. package/src/types.ts +0 -233
  519. package/src/web/auth.test.ts +0 -335
  520. package/src/web/auth.ts +0 -214
  521. package/src/web/discord-validate.test.ts +0 -77
  522. package/src/web/discord-validate.ts +0 -88
  523. package/src/web/hub-discovery.test.ts +0 -98
  524. package/src/web/hub-discovery.ts +0 -69
  525. package/src/web/routes/activity.ts +0 -106
  526. package/src/web/routes/agent-provider.test.ts +0 -282
  527. package/src/web/routes/agent-provider.ts +0 -309
  528. package/src/web/routes/approvals.ts +0 -185
  529. package/src/web/routes/apps.ts +0 -434
  530. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  531. package/src/web/routes/channels-mga-detail.test.ts +0 -472
  532. package/src/web/routes/channels.ts +0 -311
  533. package/src/web/routes/oauth-providers.ts +0 -42
  534. package/src/web/routes/secrets.test.ts +0 -220
  535. package/src/web/routes/secrets.ts +0 -317
  536. package/src/web/routes/sessions.ts +0 -123
  537. package/src/web/routes/settings.test.ts +0 -106
  538. package/src/web/routes/settings.ts +0 -247
  539. package/src/web/routes/setup-status.ts +0 -205
  540. package/src/web/routes/vaults.test.ts +0 -389
  541. package/src/web/routes/vaults.ts +0 -225
  542. package/src/web/server-version.test.ts +0 -16
  543. package/src/web/server.ts +0 -1024
  544. package/src/web/services-manifest.test.ts +0 -148
  545. package/src/web/services-manifest.ts +0 -66
  546. package/src/web/static-serve.test.ts +0 -255
  547. package/src/web/static-serve.ts +0 -104
  548. package/src/web/telegram-validate.test.ts +0 -116
  549. package/src/web/telegram-validate.ts +0 -107
  550. package/src/web/vault-proxy.test.ts +0 -214
  551. package/src/web/vault-proxy.ts +0 -120
  552. package/src/web/wire-channel.ts +0 -181
  553. package/src/webhook-server.ts +0 -134
  554. package/vitest.config.ts +0 -18
  555. package/web/README.md +0 -63
  556. package/web/ui/index.html +0 -13
  557. package/web/ui/package.json +0 -35
  558. package/web/ui/pnpm-lock.yaml +0 -2164
  559. package/web/ui/scripts/verify-base.mjs +0 -31
  560. package/web/ui/src/App.tsx +0 -88
  561. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  562. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  563. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  564. package/web/ui/src/components/CredentialForm.tsx +0 -214
  565. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  566. package/web/ui/src/components/StatusDot.tsx +0 -43
  567. package/web/ui/src/components/VaultPicker.tsx +0 -127
  568. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  569. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  570. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  571. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  572. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  573. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  574. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  575. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  576. package/web/ui/src/components/setup/types.ts +0 -105
  577. package/web/ui/src/lib/api.test.ts +0 -410
  578. package/web/ui/src/lib/api.ts +0 -1248
  579. package/web/ui/src/lib/auth.test.ts +0 -352
  580. package/web/ui/src/lib/auth.ts +0 -405
  581. package/web/ui/src/lib/channel-adapters.ts +0 -136
  582. package/web/ui/src/main.tsx +0 -19
  583. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  584. package/web/ui/src/routes/Apps.tsx +0 -613
  585. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  586. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  587. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  588. package/web/ui/src/routes/GroupDetail.test.tsx +0 -206
  589. package/web/ui/src/routes/GroupDetail.tsx +0 -880
  590. package/web/ui/src/routes/GroupList.tsx +0 -187
  591. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  592. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  593. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  594. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  595. package/web/ui/src/routes/SecretsList.tsx +0 -942
  596. package/web/ui/src/routes/SessionsList.tsx +0 -220
  597. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  598. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  599. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  600. package/web/ui/src/routes/VaultDetail.test.tsx +0 -363
  601. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  602. package/web/ui/src/routes/VaultsList.tsx +0 -295
  603. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  604. package/web/ui/src/styles.css +0 -608
  605. package/web/ui/src/test/setup.ts +0 -23
  606. package/web/ui/src/vite-env.d.ts +0 -10
  607. package/web/ui/vite.config.ts +0 -34
  608. package/web/ui/vitest.config.ts +0 -25
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Tests for the ATTACHED-backend queue registry (`src/backends/attached-queue.ts`) —
3
+ * the parallel-to-ProgrammaticAgentRegistry registry a `backend:attached` agent's
4
+ * connected Claude Code session pulls from (design 2026-06-18-channel-backend.md).
5
+ *
6
+ * A FAKE store (implements {@link AttachedQueueStore}) stands in for the channel's
7
+ * VaultTransport: it holds inbound notes in memory with a mutable `status`/`claimedAt`
8
+ * (the vault IS the queue + source of truth), and records outbound replies. So the
9
+ * claim/reply/release/sweep semantics + the restart-safety (re-read from the store)
10
+ * are asserted with no real vault.
11
+ */
12
+
13
+ import { describe, test, expect } from "bun:test";
14
+ import {
15
+ AttachedQueueRegistry,
16
+ type AttachedQueueStore,
17
+ } from "./attached-queue.ts";
18
+ import type { AgentSpec } from "../sandbox/types.ts";
19
+ import { InboundClaimConflictError } from "../transports/vault.ts";
20
+ import type { InboundQueueNote, InboundStatus } from "../transports/vault.ts";
21
+
22
+ /**
23
+ * A fake durable inbound-note store. Notes live in a Map (id → note); `reply` records
24
+ * outbound writes. Mirrors the VaultTransport methods the registry calls. The status
25
+ * mutations persist in the Map, so re-reading models the vault's restart-safety.
26
+ */
27
+ class FakeStore implements AttachedQueueStore {
28
+ readonly notes = new Map<string, InboundQueueNote>();
29
+ readonly outbound: Array<{ text: string; inReplyTo?: string }> = [];
30
+ /** If set, the NEXT `setInboundStatus` throws this (to test claim-fail safety). */
31
+ throwOnNextSetStatus: Error | null = null;
32
+ /** If set, `reply` throws this (to test reply-before-handled ordering). */
33
+ throwOnReply: Error | null = null;
34
+
35
+ add(note: InboundQueueNote): void {
36
+ this.notes.set(note.id, note);
37
+ }
38
+
39
+ async listInboundQueue(): Promise<InboundQueueNote[]> {
40
+ // Ascending by ts (the real transport sorts this way), returning COPIES so the
41
+ // registry can't mutate the store except through setInboundStatus (models the
42
+ // vault round-trip — the registry reads values, then PATCHes).
43
+ return [...this.notes.values()]
44
+ .map((n) => ({ ...n }))
45
+ .sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
46
+ }
47
+
48
+ async setInboundStatus(
49
+ id: string,
50
+ status: InboundStatus,
51
+ claimedAt?: string | null,
52
+ ifUpdatedAt?: string,
53
+ ): Promise<void> {
54
+ if (this.throwOnNextSetStatus) {
55
+ const e = this.throwOnNextSetStatus;
56
+ this.throwOnNextSetStatus = null;
57
+ throw e;
58
+ }
59
+ const note = this.notes.get(id);
60
+ if (!note) throw new Error(`fake store: no note ${id}`);
61
+ // CAS (FIX 3): when a precondition is supplied, the claim only lands if the note's
62
+ // `updatedAt` still matches what the caller last saw — else the race is lost (the
63
+ // real vault returns 409 → InboundClaimConflictError). On a successful CAS write we
64
+ // ADVANCE `updatedAt` (the vault bumps it on every write) so a second concurrent
65
+ // claimer with the now-stale precondition fails, modelling the real round-trip.
66
+ if (ifUpdatedAt !== undefined) {
67
+ if (note.updatedAt !== ifUpdatedAt) {
68
+ throw new InboundClaimConflictError(id, 409);
69
+ }
70
+ note.updatedAt = `${ifUpdatedAt}::bumped`;
71
+ }
72
+ note.status = status;
73
+ if (claimedAt === null) delete note.claimedAt;
74
+ else if (claimedAt !== undefined) note.claimedAt = claimedAt;
75
+ }
76
+
77
+ async reply(args: { text: string; inReplyTo?: string }): Promise<{ sent: string[] }> {
78
+ if (this.throwOnReply) throw this.throwOnReply;
79
+ this.outbound.push({ text: args.text, ...(args.inReplyTo ? { inReplyTo: args.inReplyTo } : {}) });
80
+ return { sent: [`outbound-${this.outbound.length}`] };
81
+ }
82
+ }
83
+
84
+ const specFor = (name: string, systemPrompt?: string): AgentSpec => ({
85
+ name,
86
+ channels: [name],
87
+ backend: "attached",
88
+ ...(systemPrompt ? { systemPrompt } : {}),
89
+ });
90
+
91
+ const inbound = (id: string, text: string, ts: string, status: InboundStatus = "pending"): InboundQueueNote => ({
92
+ id,
93
+ text,
94
+ sender: "operator",
95
+ ts,
96
+ status,
97
+ });
98
+
99
+ describe("AttachedQueueRegistry — registration", () => {
100
+ test("register indexes by channel + name; deregister drops it", () => {
101
+ const reg = new AttachedQueueRegistry();
102
+ const store = new FakeStore();
103
+ expect(reg.hasChannel("laptop")).toBe(false);
104
+ reg.register(specFor("laptop"), store);
105
+ expect(reg.hasChannel("laptop")).toBe(true);
106
+ expect(reg.hasName("laptop")).toBe(true);
107
+ expect(reg.channels()).toEqual(["laptop"]);
108
+ expect(reg.deregister("laptop")).toBe(true);
109
+ expect(reg.hasChannel("laptop")).toBe(false);
110
+ expect(reg.deregister("laptop")).toBe(false); // already gone.
111
+ });
112
+
113
+ test("register throws for a spec with no channel", () => {
114
+ const reg = new AttachedQueueRegistry();
115
+ expect(() => reg.register({ name: "x", channels: [], backend: "attached" }, new FakeStore())).toThrow(/no channel/);
116
+ });
117
+ });
118
+
119
+ describe("AttachedQueueRegistry — pending peek", () => {
120
+ test("counts only pending; previews the oldest-first", async () => {
121
+ const reg = new AttachedQueueRegistry();
122
+ const store = new FakeStore();
123
+ store.add(inbound("a", "first message", "2026-06-18T10:00:00Z"));
124
+ store.add(inbound("b", "second message", "2026-06-18T10:01:00Z"));
125
+ store.add(inbound("c", "already handled", "2026-06-18T09:00:00Z", "handled"));
126
+ reg.register(specFor("laptop"), store);
127
+
128
+ const view = await reg.pending("laptop");
129
+ expect(view.count).toBe(2); // c is handled, excluded.
130
+ expect(view.items.map((i) => i.id)).toEqual(["a", "b"]); // oldest-first.
131
+ expect(view.items[0]!.preview).toBe("first message");
132
+ });
133
+
134
+ test("a non-attached channel yields an empty no-op view", async () => {
135
+ const reg = new AttachedQueueRegistry();
136
+ const view = await reg.pending("unknown");
137
+ expect(view).toEqual({ count: 0, items: [] });
138
+ });
139
+ });
140
+
141
+ describe("AttachedQueueRegistry — claimNext (single-claim)", () => {
142
+ test("claims the OLDEST pending, sets in-flight + claimedAt, returns text + systemPrompt", async () => {
143
+ const reg = new AttachedQueueRegistry();
144
+ const store = new FakeStore();
145
+ store.add(inbound("b", "newer", "2026-06-18T10:05:00Z"));
146
+ store.add(inbound("a", "older", "2026-06-18T10:00:00Z"));
147
+ reg.register(specFor("laptop", "You are the laptop agent."), store);
148
+
149
+ const claimed = await reg.claimNext("laptop", () => new Date("2026-06-18T11:00:00Z"));
150
+ expect(claimed).not.toBeNull();
151
+ expect(claimed!.id).toBe("a"); // oldest.
152
+ expect(claimed!.text).toBe("older");
153
+ expect(claimed!.inReplyTo).toBe("a");
154
+ expect(claimed!.systemPrompt).toBe("You are the laptop agent."); // the def body → persona.
155
+ // The store note flipped to in-flight + got a claimedAt (the durable claim).
156
+ expect(store.notes.get("a")!.status).toBe("in-flight");
157
+ expect(store.notes.get("a")!.claimedAt).toBe("2026-06-18T11:00:00.000Z");
158
+ });
159
+
160
+ test("a SECOND claimNext does not return the same message (single-claim)", async () => {
161
+ const reg = new AttachedQueueRegistry();
162
+ const store = new FakeStore();
163
+ store.add(inbound("a", "older", "2026-06-18T10:00:00Z"));
164
+ store.add(inbound("b", "newer", "2026-06-18T10:05:00Z"));
165
+ reg.register(specFor("laptop"), store);
166
+
167
+ const first = await reg.claimNext("laptop");
168
+ const second = await reg.claimNext("laptop");
169
+ expect(first!.id).toBe("a");
170
+ expect(second!.id).toBe("b"); // a is now in-flight → not re-presented; b is next.
171
+ const third = await reg.claimNext("laptop");
172
+ expect(third).toBeNull(); // nothing pending left.
173
+ });
174
+
175
+ test("returns null when none pending", async () => {
176
+ const reg = new AttachedQueueRegistry();
177
+ const store = new FakeStore();
178
+ store.add(inbound("a", "done", "2026-06-18T10:00:00Z", "handled"));
179
+ reg.register(specFor("laptop"), store);
180
+ expect(await reg.claimNext("laptop")).toBeNull();
181
+ });
182
+
183
+ test("a claim PATCH failure surfaces + leaves the note pending (no hand-out)", async () => {
184
+ const reg = new AttachedQueueRegistry();
185
+ const store = new FakeStore();
186
+ store.add(inbound("a", "older", "2026-06-18T10:00:00Z"));
187
+ reg.register(specFor("laptop"), store);
188
+ store.throwOnNextSetStatus = new Error("vault 500");
189
+ await expect(reg.claimNext("laptop")).rejects.toThrow(/vault 500/);
190
+ expect(store.notes.get("a")!.status).toBe("pending"); // not lost — retryable.
191
+ });
192
+
193
+ test("FIX 3: a passed-through updatedAt is used as the CAS precondition on the claim", async () => {
194
+ const reg = new AttachedQueueRegistry();
195
+ const store = new FakeStore();
196
+ store.add({ ...inbound("a", "older", "2026-06-18T10:00:00Z"), updatedAt: "rev-1" });
197
+ reg.register(specFor("laptop"), store);
198
+ const claimed = await reg.claimNext("laptop");
199
+ expect(claimed!.id).toBe("a");
200
+ // CAS landed → the store bumped updatedAt (modelling the vault advancing it on write).
201
+ expect(store.notes.get("a")!.status).toBe("in-flight");
202
+ expect(store.notes.get("a")!.updatedAt).toBe("rev-1::bumped");
203
+ });
204
+
205
+ test("FIX 3: a 428/409 conflict on the claim PATCH makes claimNext skip to the NEXT pending (no double-claim)", async () => {
206
+ const reg = new AttachedQueueRegistry();
207
+ const store = new FakeStore();
208
+ // Two pending notes, each with a known revision for the CAS precondition.
209
+ store.add({ ...inbound("a", "older", "2026-06-18T10:00:00Z"), updatedAt: "rev-a" });
210
+ store.add({ ...inbound("b", "newer", "2026-06-18T10:05:00Z"), updatedAt: "rev-b" });
211
+ reg.register(specFor("laptop"), store);
212
+
213
+ // Simulate a CONCURRENT winner: between claimNext's list and its PATCH of "a",
214
+ // another session claims "a" and advances its revision. The next PATCH of "a" with the
215
+ // now-stale precondition will throw InboundClaimConflictError → re-list → claim "b".
216
+ const realSet = store.setInboundStatus.bind(store);
217
+ let firstPatch = true;
218
+ store.setInboundStatus = (async (id, status, claimedAt, ifUpdatedAt) => {
219
+ if (firstPatch && id === "a") {
220
+ firstPatch = false;
221
+ // The "other session" already claimed "a" (its revision moved on).
222
+ store.notes.get("a")!.updatedAt = "rev-a-claimed-by-someone-else";
223
+ store.notes.get("a")!.status = "in-flight";
224
+ }
225
+ return realSet(id, status, claimedAt, ifUpdatedAt);
226
+ }) as typeof store.setInboundStatus;
227
+
228
+ const claimed = await reg.claimNext("laptop");
229
+ // The conflict on "a" was caught + re-listed; we claimed "b" instead — never "a" twice.
230
+ expect(claimed!.id).toBe("b");
231
+ expect(store.notes.get("b")!.status).toBe("in-flight");
232
+ });
233
+
234
+ test("FIX 3: a conflict with NO other pending returns null (nothing claimable right now)", async () => {
235
+ const reg = new AttachedQueueRegistry();
236
+ const store = new FakeStore();
237
+ store.add({ ...inbound("a", "only", "2026-06-18T10:00:00Z"), updatedAt: "rev-a" });
238
+ reg.register(specFor("laptop"), store);
239
+ // Every CAS on "a" loses (the precondition is always stale → conflict). After the
240
+ // conflict, "a" is left in-flight (by the simulated winner), so the re-list finds no
241
+ // pending and returns null — not a double-claim, not an error.
242
+ store.setInboundStatus = (async (id: string) => {
243
+ store.notes.get(id)!.status = "in-flight";
244
+ throw new InboundClaimConflictError(id, 409);
245
+ }) as typeof store.setInboundStatus;
246
+ const claimed = await reg.claimNext("laptop");
247
+ expect(claimed).toBeNull();
248
+ });
249
+ });
250
+
251
+ describe("AttachedQueueRegistry — reply (outbound + mark handled)", () => {
252
+ test("writes the outbound via the store reply, THEN marks the inbound handled", async () => {
253
+ const reg = new AttachedQueueRegistry();
254
+ const store = new FakeStore();
255
+ store.add(inbound("a", "question", "2026-06-18T10:00:00Z", "in-flight"));
256
+ store.notes.get("a")!.claimedAt = "2026-06-18T10:01:00Z";
257
+ reg.register(specFor("laptop"), store);
258
+
259
+ const sent = await reg.reply("laptop", { inReplyTo: "a", text: "the answer" });
260
+ expect(sent.sent.length).toBe(1);
261
+ // Outbound recorded through the SAME reply seam (threads inReplyTo).
262
+ expect(store.outbound).toEqual([{ text: "the answer", inReplyTo: "a" }]);
263
+ // Inbound marked handled + claimedAt cleared.
264
+ expect(store.notes.get("a")!.status).toBe("handled");
265
+ expect(store.notes.get("a")!.claimedAt).toBeUndefined();
266
+ });
267
+
268
+ test("if the outbound write fails, the inbound is NOT marked handled (retryable)", async () => {
269
+ const reg = new AttachedQueueRegistry();
270
+ const store = new FakeStore();
271
+ store.add(inbound("a", "question", "2026-06-18T10:00:00Z", "in-flight"));
272
+ reg.register(specFor("laptop"), store);
273
+ store.throwOnReply = new Error("vault write 500");
274
+
275
+ await expect(reg.reply("laptop", { inReplyTo: "a", text: "x" })).rejects.toThrow(/vault write 500/);
276
+ expect(store.notes.get("a")!.status).toBe("in-flight"); // still claimed, not handled.
277
+ expect(store.outbound.length).toBe(0);
278
+ });
279
+
280
+ test("reply for an unregistered channel throws", async () => {
281
+ const reg = new AttachedQueueRegistry();
282
+ await expect(reg.reply("nope", { text: "x" })).rejects.toThrow(/no attached-backend agent/);
283
+ });
284
+ });
285
+
286
+ describe("AttachedQueueRegistry — release", () => {
287
+ test("returns an in-flight note to pending + clears claimedAt", async () => {
288
+ const reg = new AttachedQueueRegistry();
289
+ const store = new FakeStore();
290
+ store.add(inbound("a", "q", "2026-06-18T10:00:00Z", "in-flight"));
291
+ store.notes.get("a")!.claimedAt = "2026-06-18T10:01:00Z";
292
+ reg.register(specFor("laptop"), store);
293
+
294
+ await reg.release("laptop", "a");
295
+ expect(store.notes.get("a")!.status).toBe("pending");
296
+ expect(store.notes.get("a")!.claimedAt).toBeUndefined();
297
+ // It's claimable again.
298
+ const claimed = await reg.claimNext("laptop");
299
+ expect(claimed!.id).toBe("a");
300
+ });
301
+ });
302
+
303
+ describe("AttachedQueueRegistry — sweepExpired (TTL auto-release)", () => {
304
+ test("resets a STALE in-flight note to pending; leaves a fresh one alone", async () => {
305
+ const ttl = 15 * 60 * 1000; // 15 min.
306
+ const reg = new AttachedQueueRegistry({ claimTtlMs: ttl });
307
+ const store = new FakeStore();
308
+ const now = new Date("2026-06-18T12:00:00Z");
309
+ // 'stale' claimed 20 min ago (> TTL) → released; 'fresh' claimed 5 min ago → kept.
310
+ store.add({ id: "stale", text: "s", sender: "operator", ts: "2026-06-18T11:00:00Z", status: "in-flight", claimedAt: "2026-06-18T11:40:00Z" });
311
+ store.add({ id: "fresh", text: "f", sender: "operator", ts: "2026-06-18T11:30:00Z", status: "in-flight", claimedAt: "2026-06-18T11:55:00Z" });
312
+ store.add(inbound("pend", "p", "2026-06-18T11:45:00Z")); // already pending — untouched.
313
+ reg.register(specFor("laptop"), store);
314
+
315
+ const released = await reg.sweepExpired(now);
316
+ expect(released).toBe(1);
317
+ expect(store.notes.get("stale")!.status).toBe("pending");
318
+ expect(store.notes.get("stale")!.claimedAt).toBeUndefined();
319
+ expect(store.notes.get("fresh")!.status).toBe("in-flight"); // still fresh.
320
+ expect(store.notes.get("pend")!.status).toBe("pending");
321
+ });
322
+
323
+ test("an in-flight note with no claimedAt is left alone (can't judge its age)", async () => {
324
+ const reg = new AttachedQueueRegistry({ claimTtlMs: 1000 });
325
+ const store = new FakeStore();
326
+ store.add(inbound("a", "q", "2026-06-18T10:00:00Z", "in-flight")); // no claimedAt.
327
+ reg.register(specFor("laptop"), store);
328
+ const released = await reg.sweepExpired(new Date("2026-06-18T12:00:00Z"));
329
+ expect(released).toBe(0);
330
+ expect(store.notes.get("a")!.status).toBe("in-flight");
331
+ });
332
+
333
+ test("one channel's store error doesn't abort the sweep of the others", async () => {
334
+ const reg = new AttachedQueueRegistry({ claimTtlMs: 1000 });
335
+ const bad = new FakeStore();
336
+ bad.listInboundQueue = async () => {
337
+ throw new Error("vault down");
338
+ };
339
+ const good = new FakeStore();
340
+ good.add({ id: "stale", text: "s", sender: "operator", ts: "t", status: "in-flight", claimedAt: "2026-06-18T00:00:00Z" });
341
+ reg.register(specFor("bad"), bad);
342
+ reg.register(specFor("good"), good);
343
+ const released = await reg.sweepExpired(new Date("2026-06-18T12:00:00Z"));
344
+ expect(released).toBe(1); // good swept despite bad throwing.
345
+ expect(good.notes.get("stale")!.status).toBe("pending");
346
+ });
347
+ });
348
+
349
+ describe("AttachedQueueRegistry — restart safety (vault is the source of truth)", () => {
350
+ test("a claim survives a 'restart' (a fresh registry reading the same store)", async () => {
351
+ const store = new FakeStore();
352
+ store.add(inbound("a", "q1", "2026-06-18T10:00:00Z"));
353
+ store.add(inbound("b", "q2", "2026-06-18T10:05:00Z"));
354
+
355
+ // Registry instance #1 claims 'a'.
356
+ const reg1 = new AttachedQueueRegistry();
357
+ reg1.register(specFor("laptop"), store);
358
+ const claimed = await reg1.claimNext("laptop");
359
+ expect(claimed!.id).toBe("a");
360
+
361
+ // "Daemon restart": a BRAND-NEW registry over the SAME durable store. The claim
362
+ // (status:in-flight on note 'a') persisted — so the new registry's first claim is
363
+ // 'b' (a is still in-flight, not re-presented), and a handled message would not
364
+ // reappear either. The vault is the source of truth, not in-memory state.
365
+ const reg2 = new AttachedQueueRegistry();
366
+ reg2.register(specFor("laptop"), store);
367
+ const afterRestart = await reg2.claimNext("laptop");
368
+ expect(afterRestart!.id).toBe("b");
369
+ expect(store.notes.get("a")!.status).toBe("in-flight"); // claim survived.
370
+
371
+ // Replying (post-restart) marks 'b' handled; a re-read never re-presents it.
372
+ await reg2.reply("laptop", { inReplyTo: "b", text: "answer" });
373
+ expect(store.notes.get("b")!.status).toBe("handled");
374
+ expect(await reg2.pending("laptop")).toEqual({ count: 0, items: [] });
375
+ });
376
+ });