@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,456 @@
1
+ /**
2
+ * Integration tests for the v2 host core.
3
+ * Tests routing, session creation, message writing, and delivery
4
+ * without spawning actual containers.
5
+ */
6
+ import { openDb } from './db/connection.js';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10
+
11
+ import {
12
+ initTestDb,
13
+ closeDb,
14
+ runMigrations,
15
+ createAgentGroup,
16
+ createMessagingGroup,
17
+ createMessagingGroupAgent,
18
+ } from './db/index.js';
19
+ import {
20
+ resolveSession,
21
+ writeSessionMessage,
22
+ initSessionFolder,
23
+ sessionDir,
24
+ inboundDbPath,
25
+ outboundDbPath,
26
+ } from './session-manager.js';
27
+ import { getSession, findSession } from './db/sessions.js';
28
+ import type { InboundEvent } from './channels/adapter.js';
29
+
30
+ // Mock container runner to prevent actual Docker spawning
31
+ vi.mock('./container-runner.js', () => ({
32
+ wakeContainer: vi.fn().mockResolvedValue(undefined),
33
+ isContainerRunning: vi.fn().mockReturnValue(false),
34
+ getActiveContainerCount: vi.fn().mockReturnValue(0),
35
+ killContainer: vi.fn(),
36
+ }));
37
+
38
+ // Override DATA_DIR for tests
39
+ vi.mock('./config.js', async () => {
40
+ const actual = await vi.importActual('./config.js');
41
+ return { ...actual, DATA_DIR: '/tmp/paraclaw-test-host' };
42
+ });
43
+
44
+ function now() {
45
+ return new Date().toISOString();
46
+ }
47
+
48
+ const TEST_DIR = '/tmp/paraclaw-test-host';
49
+
50
+ beforeEach(() => {
51
+ // Clean test directory
52
+ if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
53
+ fs.mkdirSync(TEST_DIR, { recursive: true });
54
+
55
+ const db = initTestDb();
56
+ runMigrations(db);
57
+ });
58
+
59
+ afterEach(() => {
60
+ closeDb();
61
+ if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true });
62
+ });
63
+
64
+ describe('session manager', () => {
65
+ beforeEach(() => {
66
+ createAgentGroup({
67
+ id: 'ag-1',
68
+ name: 'Test Agent',
69
+ folder: 'test-agent',
70
+ agent_provider: null,
71
+ created_at: now(),
72
+ });
73
+ createMessagingGroup({
74
+ id: 'mg-1',
75
+ channel_type: 'discord',
76
+ platform_id: 'chan-123',
77
+ name: 'General',
78
+ is_group: 1,
79
+ unknown_sender_policy: 'strict',
80
+ created_at: now(),
81
+ });
82
+ });
83
+
84
+ it('should create session folder and both DBs', () => {
85
+ initSessionFolder('ag-1', 'sess-test');
86
+ const dir = sessionDir('ag-1', 'sess-test');
87
+ expect(fs.existsSync(dir)).toBe(true);
88
+ expect(fs.existsSync(path.join(dir, 'outbox'))).toBe(true);
89
+
90
+ // Verify inbound.db
91
+ const inPath = inboundDbPath('ag-1', 'sess-test');
92
+ expect(fs.existsSync(inPath)).toBe(true);
93
+ const inDb = openDb(inPath);
94
+ const inTables = inDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>;
95
+ expect(inTables.map((t) => t.name)).toContain('messages_in');
96
+ expect(inTables.map((t) => t.name)).toContain('delivered');
97
+ inDb.close();
98
+
99
+ // Verify outbound.db
100
+ const outPath = outboundDbPath('ag-1', 'sess-test');
101
+ expect(fs.existsSync(outPath)).toBe(true);
102
+ const outDb = openDb(outPath);
103
+ const outTables = outDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{
104
+ name: string;
105
+ }>;
106
+ expect(outTables.map((t) => t.name)).toContain('messages_out');
107
+ expect(outTables.map((t) => t.name)).toContain('processing_ack');
108
+ outDb.close();
109
+ });
110
+
111
+ it('should resolve to existing session (shared mode)', () => {
112
+ const { session: s1, created: c1 } = resolveSession('ag-1', 'mg-1', null, 'shared');
113
+ expect(c1).toBe(true);
114
+
115
+ const { session: s2, created: c2 } = resolveSession('ag-1', 'mg-1', null, 'shared');
116
+ expect(c2).toBe(false);
117
+ expect(s2.id).toBe(s1.id);
118
+ });
119
+
120
+ it('should create separate sessions per thread (per-thread mode)', () => {
121
+ const { session: s1 } = resolveSession('ag-1', 'mg-1', 'thread-1', 'per-thread');
122
+ const { session: s2 } = resolveSession('ag-1', 'mg-1', 'thread-2', 'per-thread');
123
+ expect(s1.id).not.toBe(s2.id);
124
+ });
125
+
126
+ it('should reuse session for same thread', () => {
127
+ const { session: s1 } = resolveSession('ag-1', 'mg-1', 'thread-1', 'per-thread');
128
+ const { session: s2, created } = resolveSession('ag-1', 'mg-1', 'thread-1', 'per-thread');
129
+ expect(created).toBe(false);
130
+ expect(s2.id).toBe(s1.id);
131
+ });
132
+
133
+ it('should write message to inbound DB', () => {
134
+ const { session } = resolveSession('ag-1', 'mg-1', null, 'shared');
135
+
136
+ writeSessionMessage('ag-1', session.id, {
137
+ id: 'msg-1',
138
+ kind: 'chat',
139
+ timestamp: now(),
140
+ platformId: 'chan-123',
141
+ channelType: 'discord',
142
+ threadId: null,
143
+ content: JSON.stringify({ sender: 'User', text: 'Hello' }),
144
+ });
145
+
146
+ // Read from the inbound DB
147
+ const dbPath = inboundDbPath('ag-1', session.id);
148
+ const db = openDb(dbPath);
149
+ const rows = db.prepare('SELECT * FROM messages_in').all() as Array<{
150
+ id: string;
151
+ kind: string;
152
+ status: string;
153
+ content: string;
154
+ }>;
155
+ db.close();
156
+
157
+ expect(rows).toHaveLength(1);
158
+ expect(rows[0].id).toBe('msg-1');
159
+ expect(rows[0].status).toBe('pending');
160
+ expect(JSON.parse(rows[0].content).text).toBe('Hello');
161
+ });
162
+
163
+ it('should update last_active on message write', () => {
164
+ const { session } = resolveSession('ag-1', 'mg-1', null, 'shared');
165
+ expect(getSession(session.id)!.last_active).toBeNull();
166
+
167
+ writeSessionMessage('ag-1', session.id, {
168
+ id: 'msg-1',
169
+ kind: 'chat',
170
+ timestamp: now(),
171
+ content: JSON.stringify({ text: 'hi' }),
172
+ });
173
+
174
+ expect(getSession(session.id)!.last_active).not.toBeNull();
175
+ });
176
+ });
177
+
178
+ describe('router', () => {
179
+ beforeEach(() => {
180
+ createAgentGroup({
181
+ id: 'ag-1',
182
+ name: 'Test Agent',
183
+ folder: 'test-agent',
184
+ agent_provider: null,
185
+ created_at: now(),
186
+ });
187
+ // Use 'public' policy so the router tests exercise routing, not the
188
+ // access gate. Dedicated access-gate tests live with the access module.
189
+ createMessagingGroup({
190
+ id: 'mg-1',
191
+ channel_type: 'discord',
192
+ platform_id: 'chan-123',
193
+ name: 'General',
194
+ is_group: 1,
195
+ unknown_sender_policy: 'public',
196
+ created_at: now(),
197
+ });
198
+ createMessagingGroupAgent({
199
+ id: 'mga-1',
200
+ messaging_group_id: 'mg-1',
201
+ agent_group_id: 'ag-1',
202
+ engage_mode: 'pattern',
203
+ engage_pattern: '.',
204
+ sender_scope: 'all',
205
+ ignored_message_policy: 'drop',
206
+ session_mode: 'shared',
207
+ priority: 0,
208
+ created_at: now(),
209
+ });
210
+ });
211
+
212
+ it('should route a message end-to-end', async () => {
213
+ const { routeInbound } = await import('./router.js');
214
+ const { wakeContainer } = await import('./container-runner.js');
215
+
216
+ const event: InboundEvent = {
217
+ channelType: 'discord',
218
+ platformId: 'chan-123',
219
+ threadId: null,
220
+ message: {
221
+ id: 'msg-in-1',
222
+ kind: 'chat',
223
+ content: JSON.stringify({ sender: 'User', text: 'Hello agent!' }),
224
+ timestamp: now(),
225
+ },
226
+ };
227
+
228
+ await routeInbound(event);
229
+
230
+ // Verify session was created
231
+ const session = findSession('mg-1', null);
232
+ expect(session).toBeDefined();
233
+
234
+ // Verify message was written to inbound DB
235
+ const dbPath = inboundDbPath('ag-1', session!.id);
236
+ const db = openDb(dbPath);
237
+ const rows = db.prepare('SELECT * FROM messages_in').all() as Array<{ id: string; content: string }>;
238
+ db.close();
239
+
240
+ expect(rows).toHaveLength(1);
241
+ expect(JSON.parse(rows[0].content).text).toBe('Hello agent!');
242
+
243
+ // Verify container was woken
244
+ expect(wakeContainer).toHaveBeenCalled();
245
+ });
246
+
247
+ it('auto-creates messaging group only when the bot is addressed (mention/DM)', async () => {
248
+ // The router's no-mg branch is escalation-gated: plain chatter on an
249
+ // unknown channel stays silent (no DB writes) so a bot that sits in
250
+ // many unwired channels doesn't bloat messaging_groups. Only explicit
251
+ // mentions and DMs trigger auto-create.
252
+ const { routeInbound } = await import('./router.js');
253
+ const { getMessagingGroupByPlatform } = await import('./db/messaging-groups.js');
254
+
255
+ // Plain message on unknown channel — should NOT auto-create.
256
+ await routeInbound({
257
+ channelType: 'slack',
258
+ platformId: 'C-PLAIN',
259
+ threadId: null,
260
+ message: {
261
+ id: 'msg-plain',
262
+ kind: 'chat',
263
+ content: JSON.stringify({ sender: 'User', text: 'Hi' }),
264
+ timestamp: now(),
265
+ },
266
+ });
267
+ expect(getMessagingGroupByPlatform('slack', 'C-PLAIN')).toBeUndefined();
268
+
269
+ // Mention on unknown channel — SHOULD auto-create (next step: channel-registration flow).
270
+ await routeInbound({
271
+ channelType: 'slack',
272
+ platformId: 'C-MENTIONED',
273
+ threadId: null,
274
+ message: {
275
+ id: 'msg-mentioned',
276
+ kind: 'chat',
277
+ content: JSON.stringify({ sender: 'User', text: '@bot hi' }),
278
+ timestamp: now(),
279
+ isMention: true,
280
+ },
281
+ });
282
+ expect(getMessagingGroupByPlatform('slack', 'C-MENTIONED')).toBeDefined();
283
+ });
284
+
285
+ it('should route multiple messages to the same session', async () => {
286
+ const { routeInbound } = await import('./router.js');
287
+
288
+ await routeInbound({
289
+ channelType: 'discord',
290
+ platformId: 'chan-123',
291
+ threadId: null,
292
+ message: { id: 'msg-a', kind: 'chat', content: JSON.stringify({ sender: 'A', text: 'First' }), timestamp: now() },
293
+ });
294
+
295
+ await routeInbound({
296
+ channelType: 'discord',
297
+ platformId: 'chan-123',
298
+ threadId: null,
299
+ message: {
300
+ id: 'msg-b',
301
+ kind: 'chat',
302
+ content: JSON.stringify({ sender: 'B', text: 'Second' }),
303
+ timestamp: now(),
304
+ },
305
+ });
306
+
307
+ // Both should be in the same session
308
+ const session = findSession('mg-1', null);
309
+ const dbPath = inboundDbPath('ag-1', session!.id);
310
+ const db = openDb(dbPath);
311
+ const rows = db.prepare('SELECT * FROM messages_in ORDER BY timestamp').all();
312
+ db.close();
313
+
314
+ expect(rows).toHaveLength(2);
315
+ });
316
+
317
+ it('fans out to every matching agent, each in its own session', async () => {
318
+ const { routeInbound } = await import('./router.js');
319
+ const { wakeContainer } = await import('./container-runner.js');
320
+ (wakeContainer as unknown as ReturnType<typeof vi.fn>).mockClear();
321
+
322
+ // Wire a second agent to the same messaging group.
323
+ createAgentGroup({
324
+ id: 'ag-2',
325
+ name: 'Secondary Agent',
326
+ folder: 'secondary-agent',
327
+ agent_provider: null,
328
+ created_at: now(),
329
+ });
330
+ createMessagingGroupAgent({
331
+ id: 'mga-2',
332
+ messaging_group_id: 'mg-1',
333
+ agent_group_id: 'ag-2',
334
+ engage_mode: 'pattern',
335
+ engage_pattern: '.',
336
+ sender_scope: 'all',
337
+ ignored_message_policy: 'drop',
338
+ session_mode: 'shared',
339
+ priority: 0,
340
+ created_at: now(),
341
+ });
342
+
343
+ await routeInbound({
344
+ channelType: 'discord',
345
+ platformId: 'chan-123',
346
+ threadId: null,
347
+ message: { id: 'msg-fan', kind: 'chat', content: JSON.stringify({ text: 'hello all' }), timestamp: now() },
348
+ });
349
+
350
+ // Both agents should now have their own session and be woken.
351
+ expect(wakeContainer).toHaveBeenCalledTimes(2);
352
+
353
+ const { getSessionsByAgentGroup } = await import('./db/sessions.js');
354
+ expect(getSessionsByAgentGroup('ag-1')).toHaveLength(1);
355
+ expect(getSessionsByAgentGroup('ag-2')).toHaveLength(1);
356
+ });
357
+
358
+ it('accumulates without waking when engage fails + ignored_message_policy=accumulate', async () => {
359
+ const { routeInbound } = await import('./router.js');
360
+ const { wakeContainer } = await import('./container-runner.js');
361
+ (wakeContainer as unknown as ReturnType<typeof vi.fn>).mockClear();
362
+
363
+ // Replace the seed row with a mention-only wiring whose accumulate
364
+ // policy should store context even when the message doesn't mention us.
365
+ const { updateMessagingGroupAgent } = await import('./db/messaging-groups.js');
366
+ updateMessagingGroupAgent('mga-1', {
367
+ engage_mode: 'mention',
368
+ ignored_message_policy: 'accumulate',
369
+ });
370
+
371
+ await routeInbound({
372
+ channelType: 'discord',
373
+ platformId: 'chan-123',
374
+ threadId: null,
375
+ message: {
376
+ id: 'msg-nomatch',
377
+ kind: 'chat',
378
+ content: JSON.stringify({ text: 'no mention here' }),
379
+ timestamp: now(),
380
+ },
381
+ });
382
+
383
+ expect(wakeContainer).not.toHaveBeenCalled();
384
+
385
+ const session = findSession('mg-1', null);
386
+ expect(session).toBeDefined();
387
+ const db = openDb(inboundDbPath('ag-1', session!.id));
388
+ const rows = db.prepare('SELECT id, trigger FROM messages_in').all() as Array<{
389
+ id: string;
390
+ trigger: number;
391
+ }>;
392
+ db.close();
393
+ expect(rows).toHaveLength(1);
394
+ expect(rows[0].trigger).toBe(0);
395
+ });
396
+
397
+ it('drops silently when engage fails + ignored_message_policy=drop', async () => {
398
+ const { routeInbound } = await import('./router.js');
399
+ const { wakeContainer } = await import('./container-runner.js');
400
+ (wakeContainer as unknown as ReturnType<typeof vi.fn>).mockClear();
401
+
402
+ const { updateMessagingGroupAgent } = await import('./db/messaging-groups.js');
403
+ updateMessagingGroupAgent('mga-1', { engage_mode: 'mention' }); // drop is the default
404
+
405
+ await routeInbound({
406
+ channelType: 'discord',
407
+ platformId: 'chan-123',
408
+ threadId: null,
409
+ message: { id: 'msg-drop', kind: 'chat', content: JSON.stringify({ text: 'ignored' }), timestamp: now() },
410
+ });
411
+
412
+ expect(wakeContainer).not.toHaveBeenCalled();
413
+ // No session should have been created for this agent.
414
+ expect(findSession('mg-1', null)).toBeUndefined();
415
+ });
416
+ });
417
+
418
+ describe('delivery', () => {
419
+ it('should detect undelivered messages in outbound DB', () => {
420
+ createAgentGroup({
421
+ id: 'ag-1',
422
+ name: 'Agent',
423
+ folder: 'agent',
424
+ agent_provider: null,
425
+ created_at: now(),
426
+ });
427
+ createMessagingGroup({
428
+ id: 'mg-test',
429
+ channel_type: 'discord',
430
+ platform_id: 'chan-test',
431
+ name: 'Test',
432
+ is_group: 0,
433
+ unknown_sender_policy: 'strict',
434
+ created_at: now(),
435
+ });
436
+
437
+ const { session } = resolveSession('ag-1', 'mg-test', null, 'shared');
438
+
439
+ // Write a response to the outbound DB (simulating what the agent-runner does)
440
+ const dbPath = outboundDbPath('ag-1', session.id);
441
+ const db = openDb(dbPath);
442
+ db.prepare(
443
+ `INSERT INTO messages_out (id, timestamp, kind, platform_id, channel_type, content)
444
+ VALUES ('out-1', datetime('now'), 'chat', 'chan-123', 'discord', ?)`,
445
+ ).run(JSON.stringify({ text: 'Agent response' }));
446
+
447
+ const undelivered = db.prepare('SELECT * FROM messages_out').all() as Array<{
448
+ id: string;
449
+ content: string;
450
+ }>;
451
+ db.close();
452
+
453
+ expect(undelivered).toHaveLength(1);
454
+ expect(JSON.parse(undelivered[0].content).text).toBe('Agent response');
455
+ });
456
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Unit tests for the stuck-container decision logic introduced by
3
+ * ACTION-ITEMS item 9. Lives on the pure helper `decideStuckAction` so we
4
+ * don't have to mock the filesystem or the container runner.
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ import { ABSOLUTE_CEILING_MS, CLAIM_STUCK_MS, decideStuckAction } from './host-sweep.js';
9
+
10
+ const BASE = Date.parse('2026-04-20T12:00:00.000Z');
11
+
12
+ function claim(id: string, offsetMs: number) {
13
+ return { message_id: id, status_changed: new Date(BASE - offsetMs).toISOString() };
14
+ }
15
+
16
+ describe('decideStuckAction', () => {
17
+ it('returns ok when heartbeat is fresh and no claims', () => {
18
+ expect(
19
+ decideStuckAction({
20
+ now: BASE,
21
+ heartbeatMtimeMs: BASE - 5_000,
22
+ containerState: null,
23
+ claims: [],
24
+ }),
25
+ ).toEqual({ action: 'ok' });
26
+ });
27
+
28
+ it('returns kill-ceiling when heartbeat older than 30 min', () => {
29
+ const heartbeatMtimeMs = BASE - ABSOLUTE_CEILING_MS - 1_000;
30
+ const res = decideStuckAction({
31
+ now: BASE,
32
+ heartbeatMtimeMs,
33
+ containerState: null,
34
+ claims: [],
35
+ });
36
+ expect(res.action).toBe('kill-ceiling');
37
+ if (res.action !== 'kill-ceiling') return;
38
+ expect(res.ceilingMs).toBe(ABSOLUTE_CEILING_MS);
39
+ expect(res.heartbeatAgeMs).toBeGreaterThan(ABSOLUTE_CEILING_MS);
40
+ });
41
+
42
+ it('skips the ceiling check when no heartbeat file exists (fresh container not yet ticked)', () => {
43
+ // A freshly-spawned container hasn't produced any SDK events yet, so no
44
+ // heartbeat. Prior behavior treated this as infinitely stale and killed
45
+ // every container within seconds of spawn. With no claims either, we
46
+ // should conclude everything is fine.
47
+ const res = decideStuckAction({
48
+ now: BASE,
49
+ heartbeatMtimeMs: 0,
50
+ containerState: null,
51
+ claims: [],
52
+ });
53
+ expect(res.action).toBe('ok');
54
+ });
55
+
56
+ it('kills on claim-stuck when heartbeat is absent AND a claim has aged past tolerance', () => {
57
+ // Hanging fresh container: spawned, picked up a message (claim recorded
58
+ // in processing_ack), but never wrote a heartbeat. Falls through the
59
+ // skipped ceiling check into claim-stuck — which correctly fires.
60
+ const claimedAgeMs = CLAIM_STUCK_MS + 5_000;
61
+ const res = decideStuckAction({
62
+ now: BASE,
63
+ heartbeatMtimeMs: 0,
64
+ containerState: null,
65
+ claims: [claim('msg-1', claimedAgeMs)],
66
+ });
67
+ expect(res.action).toBe('kill-claim');
68
+ });
69
+
70
+ it('extends the ceiling when Bash has a declared timeout longer than 30 min', () => {
71
+ const twoHrMs = 2 * 60 * 60 * 1000;
72
+ const res = decideStuckAction({
73
+ now: BASE,
74
+ // 45 min — over the default ceiling, but under the Bash timeout
75
+ heartbeatMtimeMs: BASE - 45 * 60 * 1000,
76
+ containerState: {
77
+ current_tool: 'Bash',
78
+ tool_declared_timeout_ms: twoHrMs,
79
+ tool_started_at: new Date(BASE - 45 * 60 * 1000).toISOString(),
80
+ },
81
+ claims: [],
82
+ });
83
+ expect(res.action).toBe('ok');
84
+ });
85
+
86
+ it('returns kill-claim when a claim is past 60s and heartbeat has not moved', () => {
87
+ const claimedAgeMs = CLAIM_STUCK_MS + 10_000;
88
+ const res = decideStuckAction({
89
+ now: BASE,
90
+ heartbeatMtimeMs: BASE - claimedAgeMs - 5_000, // older than the claim
91
+ containerState: null,
92
+ claims: [claim('msg-1', claimedAgeMs)],
93
+ });
94
+ expect(res.action).toBe('kill-claim');
95
+ if (res.action !== 'kill-claim') return;
96
+ expect(res.messageId).toBe('msg-1');
97
+ expect(res.toleranceMs).toBe(CLAIM_STUCK_MS);
98
+ });
99
+
100
+ it('does not kill when heartbeat has been touched since the claim', () => {
101
+ const claimedAgeMs = CLAIM_STUCK_MS + 10_000;
102
+ const res = decideStuckAction({
103
+ now: BASE,
104
+ heartbeatMtimeMs: BASE - 2_000, // fresh, updated after the claim
105
+ containerState: null,
106
+ claims: [claim('msg-1', claimedAgeMs)],
107
+ });
108
+ expect(res.action).toBe('ok');
109
+ });
110
+
111
+ it('does not kill when claim age is below tolerance', () => {
112
+ const res = decideStuckAction({
113
+ now: BASE,
114
+ heartbeatMtimeMs: BASE - CLAIM_STUCK_MS - 10_000, // old, but claim is recent
115
+ containerState: null,
116
+ claims: [claim('msg-1', 5_000)],
117
+ });
118
+ expect(res.action).toBe('ok');
119
+ });
120
+
121
+ it('widens per-claim tolerance for a running Bash with long timeout', () => {
122
+ const tenMinMs = 10 * 60 * 1000;
123
+ const res = decideStuckAction({
124
+ now: BASE,
125
+ // 5 min since claim, over the 60s default but under the declared Bash timeout
126
+ heartbeatMtimeMs: BASE - 5 * 60 * 1000 - 5_000,
127
+ containerState: {
128
+ current_tool: 'Bash',
129
+ tool_declared_timeout_ms: tenMinMs,
130
+ tool_started_at: new Date(BASE - 5 * 60 * 1000).toISOString(),
131
+ },
132
+ claims: [claim('msg-1', 5 * 60 * 1000)],
133
+ });
134
+ expect(res.action).toBe('ok');
135
+ });
136
+
137
+ it('ignores claims with unparseable timestamps', () => {
138
+ const res = decideStuckAction({
139
+ now: BASE,
140
+ heartbeatMtimeMs: BASE - 5_000,
141
+ containerState: null,
142
+ claims: [{ message_id: 'x', status_changed: 'not-a-date' }],
143
+ });
144
+ expect(res.action).toBe('ok');
145
+ });
146
+ });