@openparachute/agent 0.1.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 (501) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.json +5 -0
  3. package/.claude/skills/add-atomic-chat-tool/SKILL.md +243 -0
  4. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +229 -0
  5. package/.claude/skills/add-codex/SKILL.md +161 -0
  6. package/.claude/skills/add-dashboard/SKILL.md +138 -0
  7. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +495 -0
  8. package/.claude/skills/add-emacs/SKILL.md +296 -0
  9. package/.claude/skills/add-gcal-tool/SKILL.md +210 -0
  10. package/.claude/skills/add-gchat/REMOVE.md +6 -0
  11. package/.claude/skills/add-gchat/SKILL.md +92 -0
  12. package/.claude/skills/add-gchat/VERIFY.md +3 -0
  13. package/.claude/skills/add-github/REMOVE.md +6 -0
  14. package/.claude/skills/add-github/SKILL.md +148 -0
  15. package/.claude/skills/add-github/VERIFY.md +3 -0
  16. package/.claude/skills/add-gmail-tool/SKILL.md +229 -0
  17. package/.claude/skills/add-imessage/REMOVE.md +6 -0
  18. package/.claude/skills/add-imessage/SKILL.md +113 -0
  19. package/.claude/skills/add-imessage/VERIFY.md +3 -0
  20. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +110 -0
  21. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +75 -0
  22. package/.claude/skills/add-linear/REMOVE.md +6 -0
  23. package/.claude/skills/add-linear/SKILL.md +168 -0
  24. package/.claude/skills/add-linear/VERIFY.md +3 -0
  25. package/.claude/skills/add-macos-statusbar/SKILL.md +133 -0
  26. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +147 -0
  27. package/.claude/skills/add-matrix/REMOVE.md +6 -0
  28. package/.claude/skills/add-matrix/SKILL.md +148 -0
  29. package/.claude/skills/add-matrix/VERIFY.md +3 -0
  30. package/.claude/skills/add-ollama-provider/SKILL.md +179 -0
  31. package/.claude/skills/add-ollama-tool/SKILL.md +193 -0
  32. package/.claude/skills/add-opencode/SKILL.md +229 -0
  33. package/.claude/skills/add-parallel/SKILL.md +290 -0
  34. package/.claude/skills/add-resend/REMOVE.md +6 -0
  35. package/.claude/skills/add-resend/SKILL.md +93 -0
  36. package/.claude/skills/add-resend/VERIFY.md +3 -0
  37. package/.claude/skills/add-signal/REMOVE.md +13 -0
  38. package/.claude/skills/add-signal/SKILL.md +318 -0
  39. package/.claude/skills/add-signal/VERIFY.md +5 -0
  40. package/.claude/skills/add-slack/REMOVE.md +6 -0
  41. package/.claude/skills/add-slack/SKILL.md +112 -0
  42. package/.claude/skills/add-slack/VERIFY.md +3 -0
  43. package/.claude/skills/add-teams/REMOVE.md +6 -0
  44. package/.claude/skills/add-teams/SKILL.md +207 -0
  45. package/.claude/skills/add-teams/VERIFY.md +3 -0
  46. package/.claude/skills/add-vercel/SKILL.md +147 -0
  47. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +103 -0
  48. package/.claude/skills/add-webex/REMOVE.md +6 -0
  49. package/.claude/skills/add-webex/SKILL.md +88 -0
  50. package/.claude/skills/add-webex/VERIFY.md +3 -0
  51. package/.claude/skills/add-wechat/REMOVE.md +49 -0
  52. package/.claude/skills/add-wechat/SKILL.md +170 -0
  53. package/.claude/skills/add-wechat/scripts/wire-dm.ts +172 -0
  54. package/.claude/skills/add-whatsapp/SKILL.md +264 -0
  55. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +6 -0
  56. package/.claude/skills/add-whatsapp-cloud/SKILL.md +95 -0
  57. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +3 -0
  58. package/.claude/skills/claw/SKILL.md +131 -0
  59. package/.claude/skills/claw/scripts/claw +374 -0
  60. package/.claude/skills/convert-to-apple-container/SKILL.md +212 -0
  61. package/.claude/skills/customize/SKILL.md +110 -0
  62. package/.claude/skills/debug/SKILL.md +349 -0
  63. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  64. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  65. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  66. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  67. package/.claude/skills/init-first-agent/SKILL.md +120 -0
  68. package/.claude/skills/init-onecli/SKILL.md +270 -0
  69. package/.claude/skills/manage-channels/SKILL.md +87 -0
  70. package/.claude/skills/manage-mounts/SKILL.md +47 -0
  71. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +100 -0
  72. package/.claude/skills/migrate-from-openclaw/SKILL.md +447 -0
  73. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +734 -0
  74. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +476 -0
  75. package/.claude/skills/migrate-nanoclaw/SKILL.md +484 -0
  76. package/.claude/skills/migrate-nanoclaw/diagnostics.md +51 -0
  77. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  78. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  79. package/.claude/skills/update-nanoclaw/SKILL.md +243 -0
  80. package/.claude/skills/update-nanoclaw/diagnostics.md +48 -0
  81. package/.claude/skills/update-skills/SKILL.md +130 -0
  82. package/.claude/skills/use-native-credential-proxy/SKILL.md +167 -0
  83. package/.claude/skills/x-integration/SKILL.md +417 -0
  84. package/.claude/skills/x-integration/agent.ts +243 -0
  85. package/.claude/skills/x-integration/host.ts +155 -0
  86. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  87. package/.claude/skills/x-integration/lib/config.ts +62 -0
  88. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  89. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  90. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  91. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  92. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  93. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  94. package/.github/CODEOWNERS +10 -0
  95. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  96. package/.github/workflows/bump-version.yml +35 -0
  97. package/.github/workflows/ci.yml +39 -0
  98. package/.github/workflows/label-pr.yml +40 -0
  99. package/.github/workflows/update-tokens.yml +43 -0
  100. package/.husky/pre-commit +1 -0
  101. package/.mcp.json +3 -0
  102. package/.nvmrc +1 -0
  103. package/.parachute/module.json +14 -0
  104. package/.prettierrc +4 -0
  105. package/CHANGELOG.md +215 -0
  106. package/CLAUDE.md +307 -0
  107. package/CODE_OF_CONDUCT.md +128 -0
  108. package/CONTRIBUTING.md +159 -0
  109. package/CONTRIBUTORS.md +26 -0
  110. package/LICENSE +21 -0
  111. package/README.md +190 -0
  112. package/README_ja.md +194 -0
  113. package/README_zh.md +194 -0
  114. package/assets/nanoclaw-favicon.png +0 -0
  115. package/assets/nanoclaw-icon.png +0 -0
  116. package/assets/nanoclaw-logo-dark.png +0 -0
  117. package/assets/nanoclaw-logo.png +0 -0
  118. package/assets/nanoclaw-profile.jpeg +0 -0
  119. package/assets/nanoclaw-sales.png +0 -0
  120. package/assets/social-preview.jpg +0 -0
  121. package/config-examples/mount-allowlist.json +25 -0
  122. package/container/.dockerignore +2 -0
  123. package/container/CLAUDE.md +21 -0
  124. package/container/Dockerfile +121 -0
  125. package/container/agent-runner/bun.lock +243 -0
  126. package/container/agent-runner/package.json +22 -0
  127. package/container/agent-runner/scripts/sdk-signal-probe.ts +169 -0
  128. package/container/agent-runner/src/config.ts +55 -0
  129. package/container/agent-runner/src/db/connection.ts +267 -0
  130. package/container/agent-runner/src/db/index.ts +20 -0
  131. package/container/agent-runner/src/db/messages-in.ts +138 -0
  132. package/container/agent-runner/src/db/messages-out.ts +143 -0
  133. package/container/agent-runner/src/db/session-routing.ts +30 -0
  134. package/container/agent-runner/src/db/session-state.test.ts +100 -0
  135. package/container/agent-runner/src/db/session-state.ts +79 -0
  136. package/container/agent-runner/src/destinations.ts +135 -0
  137. package/container/agent-runner/src/formatter.test.ts +167 -0
  138. package/container/agent-runner/src/formatter.ts +260 -0
  139. package/container/agent-runner/src/index.ts +110 -0
  140. package/container/agent-runner/src/integration.test.ts +121 -0
  141. package/container/agent-runner/src/mcp-tools/agents.instructions.md +26 -0
  142. package/container/agent-runner/src/mcp-tools/agents.ts +66 -0
  143. package/container/agent-runner/src/mcp-tools/core.instructions.md +27 -0
  144. package/container/agent-runner/src/mcp-tools/core.ts +262 -0
  145. package/container/agent-runner/src/mcp-tools/index.ts +22 -0
  146. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +22 -0
  147. package/container/agent-runner/src/mcp-tools/interactive.ts +169 -0
  148. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +40 -0
  149. package/container/agent-runner/src/mcp-tools/scheduling.ts +299 -0
  150. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +25 -0
  151. package/container/agent-runner/src/mcp-tools/self-mod.ts +120 -0
  152. package/container/agent-runner/src/mcp-tools/server.ts +54 -0
  153. package/container/agent-runner/src/mcp-tools/types.ts +6 -0
  154. package/container/agent-runner/src/poll-loop.test.ts +248 -0
  155. package/container/agent-runner/src/poll-loop.ts +437 -0
  156. package/container/agent-runner/src/providers/claude.ts +379 -0
  157. package/container/agent-runner/src/providers/factory.test.ts +19 -0
  158. package/container/agent-runner/src/providers/factory.ts +13 -0
  159. package/container/agent-runner/src/providers/index.ts +6 -0
  160. package/container/agent-runner/src/providers/mock.ts +77 -0
  161. package/container/agent-runner/src/providers/provider-registry.ts +33 -0
  162. package/container/agent-runner/src/providers/types.ts +82 -0
  163. package/container/agent-runner/src/scheduling/task-script.ts +121 -0
  164. package/container/agent-runner/src/timezone.test.ts +93 -0
  165. package/container/agent-runner/src/timezone.ts +107 -0
  166. package/container/agent-runner/tsconfig.json +14 -0
  167. package/container/build.sh +48 -0
  168. package/container/entrypoint.sh +16 -0
  169. package/container/skills/agent-browser/SKILL.md +159 -0
  170. package/container/skills/frontend-engineer/SKILL.md +157 -0
  171. package/container/skills/self-customize/SKILL.md +87 -0
  172. package/container/skills/slack-formatting/SKILL.md +94 -0
  173. package/container/skills/vercel-cli/SKILL.md +111 -0
  174. package/container/skills/welcome/SKILL.md +85 -0
  175. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  176. package/docs/BRANCH-FORK-MAINTENANCE.md +81 -0
  177. package/docs/README.md +25 -0
  178. package/docs/SDK_DEEP_DIVE.md +643 -0
  179. package/docs/SECURITY.md +162 -0
  180. package/docs/agent-runner-details.md +749 -0
  181. package/docs/api-details.md +365 -0
  182. package/docs/architecture-diagram.html +422 -0
  183. package/docs/architecture-diagram.md +215 -0
  184. package/docs/architecture.md +751 -0
  185. package/docs/audit/2026-04-30-channel-endpoint-audit.md +36 -0
  186. package/docs/build-and-runtime.md +80 -0
  187. package/docs/cross-mount-stress/README.md +112 -0
  188. package/docs/cross-mount-stress/container-writer-retry.mjs +55 -0
  189. package/docs/cross-mount-stress/container-writer-slow.mjs +42 -0
  190. package/docs/cross-mount-stress/container-writer.mjs +47 -0
  191. package/docs/cross-mount-stress/host-writer-retry.mjs +55 -0
  192. package/docs/cross-mount-stress/host-writer-slow.mjs +43 -0
  193. package/docs/cross-mount-stress/host-writer.mjs +47 -0
  194. package/docs/db-central.md +316 -0
  195. package/docs/db-session.md +183 -0
  196. package/docs/db.md +119 -0
  197. package/docs/design/2026-04-29-vault-management-ui.md +231 -0
  198. package/docs/design/2026-04-30-channel-wiring-rework.md +234 -0
  199. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +272 -0
  200. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +250 -0
  201. package/docs/docker-sandboxes.md +359 -0
  202. package/docs/isolation-model.md +88 -0
  203. package/docs/ollama.md +79 -0
  204. package/docs/parachute-integration.md +109 -0
  205. package/docs/post-night-rebirth-reflections.md +151 -0
  206. package/eslint.config.js +32 -0
  207. package/package.json +54 -0
  208. package/pnpm-workspace.yaml +8 -0
  209. package/repo-tokens/README.md +113 -0
  210. package/repo-tokens/action.yml +186 -0
  211. package/repo-tokens/badge.svg +23 -0
  212. package/repo-tokens/examples/green.svg +14 -0
  213. package/repo-tokens/examples/red.svg +14 -0
  214. package/repo-tokens/examples/yellow-green.svg +14 -0
  215. package/repo-tokens/examples/yellow.svg +14 -0
  216. package/scripts/chat.ts +101 -0
  217. package/scripts/cleanup-sessions.sh +150 -0
  218. package/scripts/init-cli-agent.ts +171 -0
  219. package/scripts/init-first-agent.ts +377 -0
  220. package/scripts/parachute.ts +158 -0
  221. package/scripts/run-migrations.ts +105 -0
  222. package/scripts/sanity-live-poll.ts +95 -0
  223. package/scripts/seed-discord.ts +79 -0
  224. package/scripts/test-v2-agent.ts +106 -0
  225. package/scripts/test-v2-channel-e2e.ts +265 -0
  226. package/scripts/test-v2-host.ts +184 -0
  227. package/src/channels/adapter.ts +214 -0
  228. package/src/channels/ask-question.ts +46 -0
  229. package/src/channels/channel-registry.test.ts +421 -0
  230. package/src/channels/channel-registry.ts +313 -0
  231. package/src/channels/chat-sdk-bridge.test.ts +84 -0
  232. package/src/channels/chat-sdk-bridge.ts +652 -0
  233. package/src/channels/cli.ts +276 -0
  234. package/src/channels/discord.ts +90 -0
  235. package/src/channels/index.ts +17 -0
  236. package/src/channels/telegram-markdown-sanitize.test.ts +78 -0
  237. package/src/channels/telegram-markdown-sanitize.ts +55 -0
  238. package/src/channels/telegram-pairing.test.ts +254 -0
  239. package/src/channels/telegram-pairing.ts +339 -0
  240. package/src/channels/telegram.ts +279 -0
  241. package/src/channels/trust-hint.test.ts +48 -0
  242. package/src/channels/trust-hint.ts +75 -0
  243. package/src/claude-md-compose.migrate.test.ts +64 -0
  244. package/src/claude-md-compose.ts +205 -0
  245. package/src/command-gate.ts +63 -0
  246. package/src/config.test.ts +93 -0
  247. package/src/config.ts +108 -0
  248. package/src/container-config.ts +167 -0
  249. package/src/container-runner.test.ts +32 -0
  250. package/src/container-runner.ts +576 -0
  251. package/src/container-runtime.test.ts +169 -0
  252. package/src/container-runtime.ts +92 -0
  253. package/src/db/_bun-sqlite-shim.ts +88 -0
  254. package/src/db/agent-activity.test.ts +155 -0
  255. package/src/db/agent-activity.ts +121 -0
  256. package/src/db/agent-groups.ts +77 -0
  257. package/src/db/connection.migrate.test.ts +143 -0
  258. package/src/db/connection.ts +224 -0
  259. package/src/db/db-v2.test.ts +440 -0
  260. package/src/db/dropped-messages.ts +44 -0
  261. package/src/db/index.ts +40 -0
  262. package/src/db/messaging-groups.ts +252 -0
  263. package/src/db/migrations/001-initial.ts +112 -0
  264. package/src/db/migrations/002-chat-sdk-state.ts +36 -0
  265. package/src/db/migrations/008-dropped-messages.ts +27 -0
  266. package/src/db/migrations/009-drop-pending-credentials.ts +13 -0
  267. package/src/db/migrations/010-engage-modes.ts +103 -0
  268. package/src/db/migrations/011-pending-sender-approvals.ts +40 -0
  269. package/src/db/migrations/012-channel-registration.ts +48 -0
  270. package/src/db/migrations/013-approval-render-metadata.ts +27 -0
  271. package/src/db/migrations/014-secrets.ts +44 -0
  272. package/src/db/migrations/015-secrets-drop-host-pattern.ts +18 -0
  273. package/src/db/migrations/016-secret-assignments.ts +30 -0
  274. package/src/db/migrations/017-agent-activity.ts +40 -0
  275. package/src/db/migrations/018-oauth-app-configs.ts +34 -0
  276. package/src/db/migrations/019-oauth-app-connections.ts +48 -0
  277. package/src/db/migrations/020-agent-app-connections.ts +28 -0
  278. package/src/db/migrations/021-pending-oauth-states.ts +35 -0
  279. package/src/db/migrations/022-app-connections-provider.ts +25 -0
  280. package/src/db/migrations/023-agent-group-secret-mode.test.ts +124 -0
  281. package/src/db/migrations/023-agent-group-secret-mode.ts +65 -0
  282. package/src/db/migrations/024-collapse-approvals.test.ts +249 -0
  283. package/src/db/migrations/024-collapse-approvals.ts +182 -0
  284. package/src/db/migrations/025-secret-mode-check.test.ts +155 -0
  285. package/src/db/migrations/025-secret-mode-check.ts +49 -0
  286. package/src/db/migrations/026-user-dms-bot-id.test.ts +116 -0
  287. package/src/db/migrations/026-user-dms-bot-id.ts +54 -0
  288. package/src/db/migrations/027-provider-credentials.ts +41 -0
  289. package/src/db/migrations/_test-helpers.ts +41 -0
  290. package/src/db/migrations/index.ts +127 -0
  291. package/src/db/migrations/module-agent-to-agent-destinations.ts +84 -0
  292. package/src/db/migrations/module-approvals-pending-approvals.ts +42 -0
  293. package/src/db/migrations/module-approvals-title-options.ts +40 -0
  294. package/src/db/schema.ts +258 -0
  295. package/src/db/session-db.test.ts +93 -0
  296. package/src/db/session-db.ts +325 -0
  297. package/src/db/sessions.ts +241 -0
  298. package/src/delivery.test.ts +148 -0
  299. package/src/delivery.ts +445 -0
  300. package/src/env.ts +74 -0
  301. package/src/group-folder.test.ts +35 -0
  302. package/src/group-folder.ts +44 -0
  303. package/src/group-init.ts +92 -0
  304. package/src/host-core.test.ts +456 -0
  305. package/src/host-sweep.test.ts +146 -0
  306. package/src/host-sweep.ts +287 -0
  307. package/src/index.ts +227 -0
  308. package/src/install-slug.ts +33 -0
  309. package/src/log.test.ts +81 -0
  310. package/src/log.ts +117 -0
  311. package/src/mcp/http.ts +72 -0
  312. package/src/mcp/server.ts +92 -0
  313. package/src/mcp/stdio.ts +51 -0
  314. package/src/mcp/tools/activity.ts +88 -0
  315. package/src/mcp/tools/agent-groups.ts +183 -0
  316. package/src/mcp/tools/approvals.ts +122 -0
  317. package/src/mcp/tools/channels.ts +199 -0
  318. package/src/mcp/tools/index.ts +27 -0
  319. package/src/mcp/tools/oauth.ts +48 -0
  320. package/src/mcp/tools/secrets.ts +169 -0
  321. package/src/mcp/tools/sessions.ts +135 -0
  322. package/src/mcp/types.ts +51 -0
  323. package/src/modules/agent-to-agent/agent-route.test.ts +46 -0
  324. package/src/modules/agent-to-agent/agent-route.ts +223 -0
  325. package/src/modules/agent-to-agent/create-agent.ts +127 -0
  326. package/src/modules/agent-to-agent/db/agent-destinations.ts +135 -0
  327. package/src/modules/agent-to-agent/index.ts +22 -0
  328. package/src/modules/agent-to-agent/write-destinations.ts +59 -0
  329. package/src/modules/approvals/agent.md +45 -0
  330. package/src/modules/approvals/index.ts +21 -0
  331. package/src/modules/approvals/picks.test.ts +291 -0
  332. package/src/modules/approvals/primitive.ts +279 -0
  333. package/src/modules/approvals/project.md +27 -0
  334. package/src/modules/approvals/response-handler.ts +87 -0
  335. package/src/modules/index.ts +24 -0
  336. package/src/modules/interactive/agent.md +21 -0
  337. package/src/modules/interactive/index.ts +69 -0
  338. package/src/modules/interactive/project.md +12 -0
  339. package/src/modules/mount-security/index.ts +448 -0
  340. package/src/modules/mount-security/migrate.test.ts +91 -0
  341. package/src/modules/permissions/access.ts +28 -0
  342. package/src/modules/permissions/channel-approval.test.ts +389 -0
  343. package/src/modules/permissions/channel-approval.ts +188 -0
  344. package/src/modules/permissions/db/agent-group-members.ts +44 -0
  345. package/src/modules/permissions/db/pending-channel-approvals.test.ts +86 -0
  346. package/src/modules/permissions/db/pending-channel-approvals.ts +66 -0
  347. package/src/modules/permissions/db/pending-sender-approvals.ts +60 -0
  348. package/src/modules/permissions/db/user-dms.ts +58 -0
  349. package/src/modules/permissions/db/user-roles.ts +85 -0
  350. package/src/modules/permissions/db/users.ts +38 -0
  351. package/src/modules/permissions/index.ts +421 -0
  352. package/src/modules/permissions/permissions.test.ts +358 -0
  353. package/src/modules/permissions/sender-approval.test.ts +470 -0
  354. package/src/modules/permissions/sender-approval.ts +165 -0
  355. package/src/modules/permissions/user-dm.ts +200 -0
  356. package/src/modules/provider-credentials/db.ts +121 -0
  357. package/src/modules/provider-credentials/index.ts +12 -0
  358. package/src/modules/provider-credentials/spawn.test.ts +206 -0
  359. package/src/modules/provider-credentials/spawn.ts +114 -0
  360. package/src/modules/scheduling/actions.ts +113 -0
  361. package/src/modules/scheduling/db.test.ts +282 -0
  362. package/src/modules/scheduling/db.ts +148 -0
  363. package/src/modules/scheduling/index.ts +34 -0
  364. package/src/modules/scheduling/recurrence.test.ts +98 -0
  365. package/src/modules/scheduling/recurrence.ts +54 -0
  366. package/src/modules/self-mod/agent.md +30 -0
  367. package/src/modules/self-mod/apply.ts +85 -0
  368. package/src/modules/self-mod/index.ts +30 -0
  369. package/src/modules/self-mod/project.md +39 -0
  370. package/src/modules/self-mod/request.ts +91 -0
  371. package/src/modules/typing/index.ts +165 -0
  372. package/src/oauth/agent-app-connections.ts +103 -0
  373. package/src/oauth/app-configs.test.ts +64 -0
  374. package/src/oauth/app-configs.ts +114 -0
  375. package/src/oauth/app-connections.test.ts +109 -0
  376. package/src/oauth/app-connections.ts +178 -0
  377. package/src/oauth/crypto.ts +56 -0
  378. package/src/oauth/flow.ts +104 -0
  379. package/src/oauth/providers/google.test.ts +38 -0
  380. package/src/oauth/providers/google.ts +46 -0
  381. package/src/oauth/providers/index.ts +48 -0
  382. package/src/oauth/state-store.test.ts +54 -0
  383. package/src/oauth/state-store.ts +93 -0
  384. package/src/parachute/README.md +27 -0
  385. package/src/parachute/create-agent.test.ts +83 -0
  386. package/src/parachute/create-agent.ts +122 -0
  387. package/src/parachute/group-status.test.ts +165 -0
  388. package/src/parachute/group-status.ts +136 -0
  389. package/src/parachute/types.ts +41 -0
  390. package/src/parachute/vault-mcp.test.ts +251 -0
  391. package/src/parachute/vault-mcp.ts +232 -0
  392. package/src/platform-id.test.ts +104 -0
  393. package/src/platform-id.ts +109 -0
  394. package/src/providers/index.ts +6 -0
  395. package/src/providers/provider-container-registry.ts +58 -0
  396. package/src/response-registry.ts +45 -0
  397. package/src/router.ts +530 -0
  398. package/src/secrets/crypto.test.ts +45 -0
  399. package/src/secrets/crypto.ts +55 -0
  400. package/src/secrets/index.ts +355 -0
  401. package/src/secrets/master-key.ts +70 -0
  402. package/src/secrets/secrets.test.ts +354 -0
  403. package/src/session-manager.migrate.test.ts +59 -0
  404. package/src/session-manager.ts +433 -0
  405. package/src/startup-bootstrap.test.ts +226 -0
  406. package/src/startup-bootstrap.ts +207 -0
  407. package/src/state-sqlite.ts +182 -0
  408. package/src/timezone.test.ts +64 -0
  409. package/src/timezone.ts +37 -0
  410. package/src/types.ts +230 -0
  411. package/src/web/auth.test.ts +335 -0
  412. package/src/web/auth.ts +214 -0
  413. package/src/web/discord-validate.test.ts +77 -0
  414. package/src/web/discord-validate.ts +88 -0
  415. package/src/web/hub-discovery.test.ts +98 -0
  416. package/src/web/hub-discovery.ts +69 -0
  417. package/src/web/routes/activity.ts +106 -0
  418. package/src/web/routes/agent-provider.test.ts +282 -0
  419. package/src/web/routes/agent-provider.ts +309 -0
  420. package/src/web/routes/approvals.ts +185 -0
  421. package/src/web/routes/apps.ts +434 -0
  422. package/src/web/routes/channels-mg-detail.test.ts +324 -0
  423. package/src/web/routes/channels-mga-detail.test.ts +425 -0
  424. package/src/web/routes/channels.ts +489 -0
  425. package/src/web/routes/oauth-providers.ts +42 -0
  426. package/src/web/routes/secrets.test.ts +175 -0
  427. package/src/web/routes/secrets.ts +282 -0
  428. package/src/web/routes/sessions.ts +123 -0
  429. package/src/web/routes/settings.test.ts +106 -0
  430. package/src/web/routes/settings.ts +247 -0
  431. package/src/web/routes/setup-status.ts +205 -0
  432. package/src/web/routes/vaults.test.ts +389 -0
  433. package/src/web/routes/vaults.ts +225 -0
  434. package/src/web/server-version.test.ts +16 -0
  435. package/src/web/server.ts +1003 -0
  436. package/src/web/services-manifest.test.ts +120 -0
  437. package/src/web/services-manifest.ts +61 -0
  438. package/src/web/static-serve.test.ts +255 -0
  439. package/src/web/static-serve.ts +104 -0
  440. package/src/web/telegram-validate.test.ts +116 -0
  441. package/src/web/telegram-validate.ts +107 -0
  442. package/src/web/vault-proxy.test.ts +214 -0
  443. package/src/web/vault-proxy.ts +120 -0
  444. package/src/web/wire-channel.ts +181 -0
  445. package/src/webhook-server.ts +134 -0
  446. package/tsconfig.json +21 -0
  447. package/vitest.config.ts +18 -0
  448. package/web/README.md +63 -0
  449. package/web/ui/index.html +13 -0
  450. package/web/ui/package.json +35 -0
  451. package/web/ui/pnpm-lock.yaml +2164 -0
  452. package/web/ui/scripts/verify-base.mjs +31 -0
  453. package/web/ui/src/App.tsx +88 -0
  454. package/web/ui/src/components/ActivityFeed.tsx +444 -0
  455. package/web/ui/src/components/AgentGroupPicker.tsx +263 -0
  456. package/web/ui/src/components/AgentProviderCards.tsx +220 -0
  457. package/web/ui/src/components/CredentialForm.tsx +214 -0
  458. package/web/ui/src/components/ScopeGrants.tsx +74 -0
  459. package/web/ui/src/components/StatusDot.tsx +43 -0
  460. package/web/ui/src/components/VaultPicker.tsx +127 -0
  461. package/web/ui/src/components/setup/AdapterInstallStep.tsx +178 -0
  462. package/web/ui/src/components/setup/AgentGroupStep.tsx +43 -0
  463. package/web/ui/src/components/setup/ChannelPickStep.tsx +74 -0
  464. package/web/ui/src/components/setup/DoneStep.tsx +49 -0
  465. package/web/ui/src/components/setup/PrereqStep.tsx +129 -0
  466. package/web/ui/src/components/setup/TestConnectionStep.tsx +108 -0
  467. package/web/ui/src/components/setup/TestMessageStep.tsx +104 -0
  468. package/web/ui/src/components/setup/WireChannelStep.tsx +166 -0
  469. package/web/ui/src/components/setup/types.ts +105 -0
  470. package/web/ui/src/lib/api.test.ts +410 -0
  471. package/web/ui/src/lib/api.ts +1210 -0
  472. package/web/ui/src/lib/auth.test.ts +139 -0
  473. package/web/ui/src/lib/auth.ts +348 -0
  474. package/web/ui/src/lib/channel-adapters.ts +136 -0
  475. package/web/ui/src/main.tsx +19 -0
  476. package/web/ui/src/routes/ApprovalsList.tsx +294 -0
  477. package/web/ui/src/routes/Apps.tsx +613 -0
  478. package/web/ui/src/routes/ChannelWireDetail.test.tsx +233 -0
  479. package/web/ui/src/routes/ChannelWireDetail.tsx +403 -0
  480. package/web/ui/src/routes/ChannelsList.tsx +158 -0
  481. package/web/ui/src/routes/GroupDetail.tsx +755 -0
  482. package/web/ui/src/routes/GroupList.tsx +187 -0
  483. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +233 -0
  484. package/web/ui/src/routes/MessagingGroupDetail.tsx +306 -0
  485. package/web/ui/src/routes/NewGroupWizard.tsx +390 -0
  486. package/web/ui/src/routes/OAuthCallback.tsx +56 -0
  487. package/web/ui/src/routes/SecretsList.tsx +921 -0
  488. package/web/ui/src/routes/SessionsList.tsx +220 -0
  489. package/web/ui/src/routes/SettingsAgentProvider.tsx +109 -0
  490. package/web/ui/src/routes/SettingsApprovals.tsx +234 -0
  491. package/web/ui/src/routes/SetupWizard.tsx +219 -0
  492. package/web/ui/src/routes/VaultDetail.test.tsx +361 -0
  493. package/web/ui/src/routes/VaultDetail.tsx +960 -0
  494. package/web/ui/src/routes/VaultsList.tsx +295 -0
  495. package/web/ui/src/routes/WireChannelPage.tsx +413 -0
  496. package/web/ui/src/styles.css +608 -0
  497. package/web/ui/src/test/setup.ts +23 -0
  498. package/web/ui/src/vite-env.d.ts +10 -0
  499. package/web/ui/tsconfig.json +20 -0
  500. package/web/ui/vite.config.ts +34 -0
  501. package/web/ui/vitest.config.ts +25 -0
@@ -0,0 +1,241 @@
1
+ import type { Approval, ApprovalBody, ApprovalKind, ApprovalStatus, Session } from '../types.js';
2
+ import { getDb, hasTable } from './connection.js';
3
+
4
+ // ── Sessions ──
5
+
6
+ export function createSession(session: Session): void {
7
+ getDb()
8
+ .prepare(
9
+ `INSERT INTO sessions (id, agent_group_id, messaging_group_id, thread_id, agent_provider, status, container_status, last_active, created_at)
10
+ VALUES (@id, @agent_group_id, @messaging_group_id, @thread_id, @agent_provider, @status, @container_status, @last_active, @created_at)`,
11
+ )
12
+ .run(session);
13
+ }
14
+
15
+ export function getSession(id: string): Session | undefined {
16
+ return getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
17
+ }
18
+
19
+ export function findSession(messagingGroupId: string, threadId: string | null): Session | undefined {
20
+ if (threadId) {
21
+ return getDb()
22
+ .prepare('SELECT * FROM sessions WHERE messaging_group_id = ? AND thread_id = ? AND status = ?')
23
+ .get(messagingGroupId, threadId, 'active') as Session | undefined;
24
+ }
25
+ return getDb()
26
+ .prepare('SELECT * FROM sessions WHERE messaging_group_id = ? AND thread_id IS NULL AND status = ?')
27
+ .get(messagingGroupId, 'active') as Session | undefined;
28
+ }
29
+
30
+ /**
31
+ * Session lookup scoped to a specific agent group. Needed when multiple
32
+ * agents are wired to the same messaging group + thread (fan-out) — the
33
+ * plain `findSession` would return whichever agent's session happened to
34
+ * be first and route to the wrong container.
35
+ */
36
+ export function findSessionForAgent(
37
+ agentGroupId: string,
38
+ messagingGroupId: string,
39
+ threadId: string | null,
40
+ ): Session | undefined {
41
+ if (threadId) {
42
+ return getDb()
43
+ .prepare(
44
+ "SELECT * FROM sessions WHERE agent_group_id = ? AND messaging_group_id = ? AND thread_id = ? AND status = 'active'",
45
+ )
46
+ .get(agentGroupId, messagingGroupId, threadId) as Session | undefined;
47
+ }
48
+ return getDb()
49
+ .prepare(
50
+ "SELECT * FROM sessions WHERE agent_group_id = ? AND messaging_group_id = ? AND thread_id IS NULL AND status = 'active'",
51
+ )
52
+ .get(agentGroupId, messagingGroupId) as Session | undefined;
53
+ }
54
+
55
+ /** Find an active session scoped to an agent group (ignoring messaging group). */
56
+ export function findSessionByAgentGroup(agentGroupId: string): Session | undefined {
57
+ return getDb()
58
+ .prepare("SELECT * FROM sessions WHERE agent_group_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1")
59
+ .get(agentGroupId) as Session | undefined;
60
+ }
61
+
62
+ export function getSessionsByAgentGroup(agentGroupId: string): Session[] {
63
+ return getDb().prepare('SELECT * FROM sessions WHERE agent_group_id = ?').all(agentGroupId) as Session[];
64
+ }
65
+
66
+ export function getActiveSessions(): Session[] {
67
+ return getDb().prepare("SELECT * FROM sessions WHERE status = 'active'").all() as Session[];
68
+ }
69
+
70
+ export function getRunningSessions(): Session[] {
71
+ return getDb().prepare("SELECT * FROM sessions WHERE container_status IN ('running', 'idle')").all() as Session[];
72
+ }
73
+
74
+ export function updateSession(
75
+ id: string,
76
+ updates: Partial<Pick<Session, 'status' | 'container_status' | 'last_active' | 'agent_provider'>>,
77
+ ): void {
78
+ const fields: string[] = [];
79
+ const values: Record<string, unknown> = { id };
80
+
81
+ for (const [key, value] of Object.entries(updates)) {
82
+ if (value !== undefined) {
83
+ fields.push(`${key} = @${key}`);
84
+ values[key] = value;
85
+ }
86
+ }
87
+ if (fields.length === 0) return;
88
+
89
+ getDb()
90
+ .prepare(`UPDATE sessions SET ${fields.join(', ')} WHERE id = @id`)
91
+ .run(values);
92
+ }
93
+
94
+ export function deleteSession(id: string): void {
95
+ getDb().prepare('DELETE FROM sessions WHERE id = ?').run(id);
96
+ }
97
+
98
+ // ── Approvals ──
99
+ // Unified table for both inline UX prompts (`kind='question'`) and
100
+ // admin-gating actions (`kind='install_packages'` etc.), collapsed in
101
+ // migration 024 (paraclaw#11). The kind discriminator is open-string —
102
+ // new module actions don't require a schema bump.
103
+
104
+ interface ApprovalRow {
105
+ id: string;
106
+ kind: ApprovalKind;
107
+ agent_group_id: string;
108
+ session_id: string | null;
109
+ body: string;
110
+ status: ApprovalStatus;
111
+ approver_user_id: string | null;
112
+ decided_at: string | null;
113
+ created_at: string;
114
+ expires_at: string | null;
115
+ }
116
+
117
+ export interface CreateApprovalInput {
118
+ id: string;
119
+ kind: ApprovalKind;
120
+ agent_group_id: string;
121
+ session_id?: string | null;
122
+ body: ApprovalBody;
123
+ status?: ApprovalStatus;
124
+ created_at: string;
125
+ expires_at?: string | null;
126
+ }
127
+
128
+ function rowToApproval(row: ApprovalRow): Approval {
129
+ return {
130
+ id: row.id,
131
+ kind: row.kind,
132
+ agent_group_id: row.agent_group_id,
133
+ session_id: row.session_id,
134
+ body: JSON.parse(row.body) as ApprovalBody,
135
+ status: row.status,
136
+ approver_user_id: row.approver_user_id,
137
+ decided_at: row.decided_at,
138
+ created_at: row.created_at,
139
+ expires_at: row.expires_at,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Insert an approval row. Idempotent: delivery retries with the same id must
145
+ * not fail on UNIQUE before the send step gets a chance to succeed. Returns
146
+ * true if a new row was inserted.
147
+ */
148
+ export function createApproval(input: CreateApprovalInput): boolean {
149
+ const result = getDb()
150
+ .prepare(
151
+ `INSERT OR IGNORE INTO approvals
152
+ (id, kind, agent_group_id, session_id, body, status, created_at, expires_at)
153
+ VALUES
154
+ (@id, @kind, @agent_group_id, @session_id, @body, @status, @created_at, @expires_at)`,
155
+ )
156
+ .run({
157
+ id: input.id,
158
+ kind: input.kind,
159
+ agent_group_id: input.agent_group_id,
160
+ session_id: input.session_id ?? null,
161
+ body: JSON.stringify(input.body),
162
+ status: input.status ?? 'pending',
163
+ created_at: input.created_at,
164
+ expires_at: input.expires_at ?? null,
165
+ });
166
+ return result.changes > 0;
167
+ }
168
+
169
+ export function getApproval(id: string): Approval | undefined {
170
+ const row = getDb().prepare('SELECT * FROM approvals WHERE id = ?').get(id) as ApprovalRow | undefined;
171
+ return row ? rowToApproval(row) : undefined;
172
+ }
173
+
174
+ export function deleteApproval(id: string): void {
175
+ getDb().prepare('DELETE FROM approvals WHERE id = ?').run(id);
176
+ }
177
+
178
+ /**
179
+ * List pending approvals, optionally filtered by `kind`. Pass an array to
180
+ * include only those kinds; pass `{ exclude }` to omit kinds (e.g. exclude
181
+ * `'question'` for the admin-gating UI surface).
182
+ */
183
+ export function listPendingApprovals(filter?: { kinds?: ApprovalKind[]; excludeKinds?: ApprovalKind[] }): Approval[] {
184
+ const clauses = ["status = 'pending'"];
185
+ const params: Record<string, unknown> = {};
186
+ if (filter?.kinds && filter.kinds.length > 0) {
187
+ const placeholders = filter.kinds.map((_, i) => `@kind_${i}`).join(', ');
188
+ clauses.push(`kind IN (${placeholders})`);
189
+ filter.kinds.forEach((k, i) => {
190
+ params[`kind_${i}`] = k;
191
+ });
192
+ }
193
+ if (filter?.excludeKinds && filter.excludeKinds.length > 0) {
194
+ const placeholders = filter.excludeKinds.map((_, i) => `@xkind_${i}`).join(', ');
195
+ clauses.push(`kind NOT IN (${placeholders})`);
196
+ filter.excludeKinds.forEach((k, i) => {
197
+ params[`xkind_${i}`] = k;
198
+ });
199
+ }
200
+ const rows = getDb()
201
+ .prepare(`SELECT * FROM approvals WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC`)
202
+ .all(params) as ApprovalRow[];
203
+ return rows.map(rowToApproval);
204
+ }
205
+
206
+ /**
207
+ * Resolve ask_question render metadata (title + normalized options) for any
208
+ * card, regardless of whether it lives in `approvals` (kind question or
209
+ * action) or one of the permissions-module side tables.
210
+ */
211
+ export function getAskQuestionRender(
212
+ id: string,
213
+ ): { title: string; options: import('../channels/ask-question.js').NormalizedOption[] } | undefined {
214
+ if (hasTable(getDb(), 'approvals')) {
215
+ const a = getApproval(id);
216
+ if (a) {
217
+ const body = a.body as { title?: string; options?: import('../channels/ask-question.js').NormalizedOption[] };
218
+ if (body.title && Array.isArray(body.options)) {
219
+ return { title: body.title, options: body.options };
220
+ }
221
+ }
222
+ }
223
+
224
+ // Channel-registration + unknown-sender approvals persist title/options_json
225
+ // in their own tables — just SELECT and return.
226
+ if (hasTable(getDb(), 'pending_channel_approvals')) {
227
+ const c = getDb()
228
+ .prepare('SELECT title, options_json FROM pending_channel_approvals WHERE messaging_group_id = ?')
229
+ .get(id) as { title: string; options_json: string } | undefined;
230
+ if (c?.title) return { title: c.title, options: JSON.parse(c.options_json) };
231
+ }
232
+
233
+ if (hasTable(getDb(), 'pending_sender_approvals')) {
234
+ const s = getDb().prepare('SELECT title, options_json FROM pending_sender_approvals WHERE id = ?').get(id) as
235
+ | { title: string; options_json: string }
236
+ | undefined;
237
+ if (s?.title) return { title: s.title, options: JSON.parse(s.options_json) };
238
+ }
239
+
240
+ return undefined;
241
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Delivery race tests.
3
+ *
4
+ * The active poll (1s, running sessions) and the sweep poll (60s, all
5
+ * active sessions) both call deliverSessionMessages. A running session
6
+ * sits in both result sets, so the two timer chains can race on the same
7
+ * outbound row — read-undelivered → call channel API → markDelivered. The
8
+ * INSERT OR IGNORE in markDelivered makes the DB write idempotent, but
9
+ * the channel API has already fired twice → user sees the message twice.
10
+ */
11
+ import { openDb } from './db/connection.js';
12
+ import fs from 'fs';
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
14
+
15
+ vi.mock('./container-runner.js', () => ({
16
+ wakeContainer: vi.fn().mockResolvedValue(undefined),
17
+ isContainerRunning: vi.fn().mockReturnValue(false),
18
+ killContainer: vi.fn(),
19
+ buildAgentGroupImage: vi.fn().mockResolvedValue(undefined),
20
+ }));
21
+
22
+ vi.mock('./config.js', async () => {
23
+ const actual = await vi.importActual<typeof import('./config.js')>('./config.js');
24
+ return { ...actual, DATA_DIR: '/tmp/paraclaw-test-delivery' };
25
+ });
26
+
27
+ const TEST_DIR = '/tmp/paraclaw-test-delivery';
28
+
29
+ import { initTestDb, closeDb, runMigrations, createAgentGroup, createMessagingGroup } from './db/index.js';
30
+ import { resolveSession, outboundDbPath } from './session-manager.js';
31
+ import { deliverSessionMessages, setDeliveryAdapter } from './delivery.js';
32
+
33
+ function now(): string {
34
+ return new Date().toISOString();
35
+ }
36
+
37
+ function seedAgentAndChannel(): void {
38
+ createAgentGroup({
39
+ id: 'ag-1',
40
+ name: 'Test Agent',
41
+ folder: 'test-agent',
42
+ agent_provider: null,
43
+ created_at: now(),
44
+ });
45
+ createMessagingGroup({
46
+ id: 'mg-1',
47
+ channel_type: 'telegram',
48
+ platform_id: 'telegram:123',
49
+ name: 'Test Chat',
50
+ is_group: 0,
51
+ unknown_sender_policy: 'public',
52
+ created_at: now(),
53
+ });
54
+ }
55
+
56
+ function insertOutbound(agentGroupId: string, sessionId: string, msgId: string): void {
57
+ const db = openDb(outboundDbPath(agentGroupId, sessionId));
58
+ db.prepare(
59
+ `INSERT INTO messages_out (id, timestamp, kind, platform_id, channel_type, content)
60
+ VALUES (?, datetime('now'), 'chat', 'telegram:123', 'telegram', ?)`,
61
+ ).run(msgId, JSON.stringify({ text: 'hello' }));
62
+ db.close();
63
+ }
64
+
65
+ beforeEach(() => {
66
+ if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
67
+ fs.mkdirSync(TEST_DIR, { recursive: true });
68
+ const db = initTestDb();
69
+ runMigrations(db);
70
+ });
71
+
72
+ afterEach(() => {
73
+ closeDb();
74
+ if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
75
+ });
76
+
77
+ describe('deliverSessionMessages — concurrent invocations', () => {
78
+ it('delivers a message exactly once when active and sweep polls overlap', async () => {
79
+ seedAgentAndChannel();
80
+ const { session } = resolveSession('ag-1', 'mg-1', null, 'shared');
81
+ insertOutbound('ag-1', session.id, 'out-1');
82
+
83
+ const calls: string[] = [];
84
+ setDeliveryAdapter({
85
+ async deliver(_channelType, _platformId, _threadId, _kind, content) {
86
+ calls.push(content);
87
+ // Hold long enough that the second concurrent caller can race the
88
+ // read-undelivered → markDelivered window.
89
+ await new Promise((r) => setTimeout(r, 100));
90
+ return 'plat-msg-1';
91
+ },
92
+ });
93
+
94
+ // Two concurrent calls — simulating active (1s) and sweep (60s) polls
95
+ // hitting the same running session at the same moment.
96
+ await Promise.all([deliverSessionMessages(session), deliverSessionMessages(session)]);
97
+
98
+ expect(calls).toHaveLength(1);
99
+ });
100
+
101
+ it('still delivers on a subsequent call after the first finishes', async () => {
102
+ seedAgentAndChannel();
103
+ const { session } = resolveSession('ag-1', 'mg-1', null, 'shared');
104
+ insertOutbound('ag-1', session.id, 'out-first');
105
+
106
+ const calls: string[] = [];
107
+ setDeliveryAdapter({
108
+ async deliver(_channelType, _platformId, _threadId, _kind, content) {
109
+ calls.push(content);
110
+ return 'plat-msg-id';
111
+ },
112
+ });
113
+
114
+ await deliverSessionMessages(session);
115
+ expect(calls).toHaveLength(1);
116
+
117
+ // Insert a second outbound message and deliver again — the lock from
118
+ // the first call must have been released.
119
+ insertOutbound('ag-1', session.id, 'out-second');
120
+ await deliverSessionMessages(session);
121
+ expect(calls).toHaveLength(2);
122
+ });
123
+
124
+ it('does not re-deliver when retried after a successful send (cleanup-after-send safety)', async () => {
125
+ // If something post-send throws (e.g. outbox cleanup), the message has
126
+ // still landed on the user's screen — the catch path must not trigger
127
+ // a re-send. We simulate by having the adapter succeed on the first
128
+ // call and recording how many times it's invoked across two attempts.
129
+ seedAgentAndChannel();
130
+ const { session } = resolveSession('ag-1', 'mg-1', null, 'shared');
131
+ insertOutbound('ag-1', session.id, 'out-once');
132
+
133
+ let callCount = 0;
134
+ setDeliveryAdapter({
135
+ async deliver() {
136
+ callCount++;
137
+ return 'plat-msg-id';
138
+ },
139
+ });
140
+
141
+ await deliverSessionMessages(session);
142
+ // Re-invoke — should be idempotent because the message is now in the
143
+ // delivered table; the channel adapter must not be called again.
144
+ await deliverSessionMessages(session);
145
+
146
+ expect(callCount).toBe(1);
147
+ });
148
+ });