clawmini 0.0.7 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +42 -13
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -176
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  328. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  329. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  331. package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { startDaemonToDiscordForwarder } from './forwarder.js';
3
3
  import { readDiscordState, updateDiscordState } from './state.js';
4
+ import { recordInbound, _resetInboundCacheForTests } from './inbound-cache.js';
4
5
 
5
6
  vi.mock('./state.js', () => ({
6
7
  readDiscordState: vi.fn(),
@@ -69,7 +70,26 @@ describe('Daemon to Discord Forwarder', () => {
69
70
  },
70
71
  waitForMessages: {
71
72
  subscribe: vi.fn().mockImplementation((input, options) => {
72
- subscribeCallbacks = options;
73
+ // The subscription now yields `ChatStreamItem` envelopes. Tests
74
+ // continue to hand us raw message arrays; wrap them here so each
75
+ // individual test site stays readable. Pre-shaped envelopes
76
+ // (objects with a `kind` field) pass through untouched so tests
77
+ // can also send `{kind:'turn'}` items.
78
+ subscribeCallbacks = {
79
+ onData: (raw: unknown) => {
80
+ if (!Array.isArray(raw)) return options.onData(raw);
81
+ options.onData(
82
+ raw.map((item: unknown) =>
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ item && typeof item === 'object' && 'kind' in (item as any)
85
+ ? item
86
+ : { kind: 'message', message: item }
87
+ )
88
+ );
89
+ },
90
+ onError: options.onError,
91
+ onComplete: options.onComplete,
92
+ };
73
93
  return { unsubscribe: vi.fn() };
74
94
  }),
75
95
  },
@@ -130,7 +150,12 @@ describe('Daemon to Discord Forwarder', () => {
130
150
 
131
151
  expect(mockClient.users.fetch).toHaveBeenCalledWith('user-123');
132
152
  expect(mockUser.createDM).toHaveBeenCalled();
133
- expect(mockDm.send).toHaveBeenCalledWith({ content: 'Agent response' });
153
+ expect(mockDm.send).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ content: 'Agent response',
156
+ allowedMentions: { parse: [] },
157
+ })
158
+ );
134
159
  expect(mockUpdateDiscordState).toHaveBeenCalledWith({
135
160
  lastSyncedMessageIds: { default: 'msg-1' },
136
161
  });
@@ -255,8 +280,20 @@ describe('Daemon to Discord Forwarder', () => {
255
280
 
256
281
  await vi.waitFor(() => expect(mockDm.send).toHaveBeenCalledTimes(2));
257
282
 
258
- expect(mockDm.send).toHaveBeenNthCalledWith(1, { content: 'a'.repeat(2000) });
259
- expect(mockDm.send).toHaveBeenNthCalledWith(2, { content: 'a'.repeat(500) });
283
+ expect(mockDm.send).toHaveBeenNthCalledWith(
284
+ 1,
285
+ expect.objectContaining({
286
+ content: 'a'.repeat(2000),
287
+ allowedMentions: { parse: [] },
288
+ })
289
+ );
290
+ expect(mockDm.send).toHaveBeenNthCalledWith(
291
+ 2,
292
+ expect.objectContaining({
293
+ content: 'a'.repeat(500),
294
+ allowedMentions: { parse: [] },
295
+ })
296
+ );
260
297
  expect(mockUpdateDiscordState).toHaveBeenCalledWith({
261
298
  lastSyncedMessageIds: { default: 'msg-1' },
262
299
  });
@@ -293,10 +330,13 @@ describe('Daemon to Discord Forwarder', () => {
293
330
 
294
331
  await vi.waitFor(() => expect(mockDm.send).toHaveBeenCalled());
295
332
 
296
- expect(mockDm.send).toHaveBeenCalledWith({
297
- content: 'Here is your file',
298
- files: ['/path/to/my/file.txt'],
299
- });
333
+ expect(mockDm.send).toHaveBeenCalledWith(
334
+ expect.objectContaining({
335
+ content: 'Here is your file',
336
+ files: ['/path/to/my/file.txt'],
337
+ allowedMentions: { parse: [] },
338
+ })
339
+ );
300
340
 
301
341
  controller.abort();
302
342
  await forwarderPromise;
@@ -330,9 +370,12 @@ describe('Daemon to Discord Forwarder', () => {
330
370
 
331
371
  await vi.waitFor(() => expect(mockDm.send).toHaveBeenCalled());
332
372
 
333
- expect(mockDm.send).toHaveBeenCalledWith({
334
- files: ['/path/to/my/file.txt'],
335
- });
373
+ expect(mockDm.send).toHaveBeenCalledWith(
374
+ expect.objectContaining({
375
+ files: ['/path/to/my/file.txt'],
376
+ allowedMentions: { parse: [] },
377
+ })
378
+ );
336
379
 
337
380
  controller.abort();
338
381
  await forwarderPromise;
@@ -367,11 +410,21 @@ describe('Daemon to Discord Forwarder', () => {
367
410
 
368
411
  await vi.waitFor(() => expect(mockDm.send).toHaveBeenCalledTimes(2));
369
412
 
370
- expect(mockDm.send).toHaveBeenNthCalledWith(1, { content: 'a'.repeat(2000) });
371
- expect(mockDm.send).toHaveBeenNthCalledWith(2, {
372
- content: 'a'.repeat(500),
373
- files: ['/path/to/my/file.txt'],
374
- });
413
+ expect(mockDm.send).toHaveBeenNthCalledWith(
414
+ 1,
415
+ expect.objectContaining({
416
+ content: 'a'.repeat(2000),
417
+ allowedMentions: { parse: [] },
418
+ })
419
+ );
420
+ expect(mockDm.send).toHaveBeenNthCalledWith(
421
+ 2,
422
+ expect.objectContaining({
423
+ content: 'a'.repeat(500),
424
+ files: ['/path/to/my/file.txt'],
425
+ allowedMentions: { parse: [] },
426
+ })
427
+ );
375
428
 
376
429
  controller.abort();
377
430
  await forwarderPromise;
@@ -430,7 +483,12 @@ describe('Daemon to Discord Forwarder', () => {
430
483
  await vi.advanceTimersByTimeAsync(30000);
431
484
 
432
485
  expect(mockTrpc.waitForMessages.subscribe).toHaveBeenCalledTimes(3);
433
- expect(mockDm.send).toHaveBeenCalledWith({ content: 'Finally up' });
486
+ expect(mockDm.send).toHaveBeenCalledWith(
487
+ expect.objectContaining({
488
+ content: 'Finally up',
489
+ allowedMentions: { parse: [] },
490
+ })
491
+ );
434
492
 
435
493
  controller.abort();
436
494
  await forwarderPromise;
@@ -576,10 +634,14 @@ describe('Daemon to Discord Forwarder', () => {
576
634
 
577
635
  await vi.waitFor(() => expect(mockDm.send).toHaveBeenCalledTimes(2));
578
636
 
579
- expect(mockDm.send).toHaveBeenNthCalledWith(2, {
580
- content:
581
- 'Action Required: Policy Request\n\nPlease approve this\n\nApprove: `/approve msg-1`\nReject: `/reject msg-1 <optional_rationale>`',
582
- });
637
+ expect(mockDm.send).toHaveBeenNthCalledWith(
638
+ 2,
639
+ expect.objectContaining({
640
+ content:
641
+ 'Action Required: Policy Request\n\nPlease approve this\n\nApprove: `/approve msg-1`\nReject: `/reject msg-1 <optional_rationale>`',
642
+ allowedMentions: { parse: [] },
643
+ })
644
+ );
583
645
 
584
646
  // Should still update state to avoid infinite loop
585
647
  expect(mockUpdateDiscordState).toHaveBeenCalledWith({
@@ -683,4 +745,420 @@ describe('Daemon to Discord Forwarder', () => {
683
745
  controller.abort();
684
746
  await forwarderPromise;
685
747
  });
748
+
749
+ describe('turn-log threading', () => {
750
+ let mockChannel: {
751
+ isTextBased: () => boolean;
752
+ isDMBased: () => boolean;
753
+ isThread: () => boolean;
754
+ send: import('vitest').Mock;
755
+ messages: { fetch: import('vitest').Mock };
756
+ };
757
+ let mockUserMessage: {
758
+ startThread: import('vitest').Mock;
759
+ };
760
+ let mockThread: {
761
+ id: string;
762
+ send: import('vitest').Mock;
763
+ messages: { fetch: import('vitest').Mock };
764
+ };
765
+ let mockLogMessage: { id: string; edit: import('vitest').Mock };
766
+
767
+ beforeEach(() => {
768
+ _resetInboundCacheForTests();
769
+
770
+ mockLogMessage = {
771
+ id: 'log-msg-1',
772
+ edit: vi.fn().mockResolvedValue({}),
773
+ };
774
+ mockThread = {
775
+ id: 'thread-1',
776
+ send: vi.fn().mockResolvedValue({ id: 'log-msg-1' }),
777
+ messages: {
778
+ fetch: vi.fn().mockResolvedValue(mockLogMessage),
779
+ },
780
+ };
781
+ mockUserMessage = {
782
+ startThread: vi.fn().mockResolvedValue(mockThread),
783
+ };
784
+ mockChannel = {
785
+ isTextBased: () => true,
786
+ isDMBased: () => false,
787
+ isThread: () => false,
788
+ send: vi.fn().mockResolvedValue({}),
789
+ messages: {
790
+ fetch: vi.fn().mockResolvedValue(mockUserMessage),
791
+ },
792
+ };
793
+ mockClient.channels.fetch = vi.fn().mockResolvedValue(mockChannel);
794
+
795
+ vi.mocked(readDiscordState).mockResolvedValue({
796
+ lastSyncedMessageIds: { 'mapped-chat': 'msg-0' },
797
+ channelChatMap: { 'channel-123': { chatId: 'mapped-chat' } },
798
+ });
799
+ });
800
+
801
+ it('opens a thread on the user message and posts the activity log entry', async () => {
802
+ vi.useFakeTimers();
803
+ const controller = new AbortController();
804
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
805
+
806
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
807
+ chatId: 'mapped-chat',
808
+ signal: controller.signal,
809
+ });
810
+
811
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
812
+
813
+ subscribeCallbacks.onData([
814
+ {
815
+ kind: 'turn',
816
+ event: {
817
+ type: 'started',
818
+ turnId: 'turn-1',
819
+ rootMessageId: 'msg-1',
820
+ externalRef: 'inbound-1',
821
+ },
822
+ },
823
+ {
824
+ kind: 'message',
825
+ message: {
826
+ id: 'msg-2',
827
+ role: 'tool',
828
+ name: 'Read',
829
+ payload: { file_path: '/foo/bar.txt' },
830
+ timestamp: '',
831
+ turnId: 'turn-1',
832
+ },
833
+ },
834
+ ]);
835
+
836
+ await vi.runOnlyPendingTimersAsync();
837
+ await vi.advanceTimersByTimeAsync(1500);
838
+ await vi.runOnlyPendingTimersAsync();
839
+
840
+ expect(mockUserMessage.startThread).toHaveBeenCalledWith(
841
+ expect.objectContaining({ name: 'Activity log' })
842
+ );
843
+ // Initial flush posts the "Started processing…" entry; coalesced flush
844
+ // adds the tool entry. We assert the tool entry made it into a posted
845
+ // message.
846
+ const sentTexts = mockThread.send.mock.calls.map((c) => c[0]?.content as string);
847
+ const editedTexts = mockLogMessage.edit.mock.calls.map((c) => c[0]?.content as string);
848
+ const allTexts = [...sentTexts, ...editedTexts];
849
+ expect(allTexts.some((t) => t.includes('Started processing'))).toBe(true);
850
+ expect(allTexts.some((t) => t.includes('/foo/bar.txt'))).toBe(true);
851
+
852
+ controller.abort();
853
+ vi.useRealTimers();
854
+ await forwarderPromise;
855
+ });
856
+
857
+ it('does not open a thread when the inbound is missing (no cached anchor)', async () => {
858
+ const controller = new AbortController();
859
+
860
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
861
+ chatId: 'mapped-chat',
862
+ signal: controller.signal,
863
+ });
864
+
865
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
866
+
867
+ subscribeCallbacks.onData([
868
+ {
869
+ kind: 'turn',
870
+ event: {
871
+ type: 'started',
872
+ turnId: 'turn-1',
873
+ rootMessageId: 'msg-1',
874
+ externalRef: 'unknown-inbound',
875
+ },
876
+ },
877
+ {
878
+ kind: 'message',
879
+ message: {
880
+ id: 'msg-2',
881
+ role: 'tool',
882
+ name: 'Read',
883
+ payload: { file_path: '/foo/bar.txt' },
884
+ timestamp: '',
885
+ turnId: 'turn-1',
886
+ },
887
+ },
888
+ {
889
+ kind: 'turn',
890
+ event: { type: 'ended', turnId: 'turn-1', outcome: 'ok' },
891
+ },
892
+ ]);
893
+
894
+ await vi.waitFor(() =>
895
+ expect(mockUpdateDiscordState).toHaveBeenCalledWith({
896
+ lastSyncedMessageIds: { 'mapped-chat': 'msg-2' },
897
+ })
898
+ );
899
+
900
+ expect(mockUserMessage.startThread).not.toHaveBeenCalled();
901
+ expect(mockThread.send).not.toHaveBeenCalled();
902
+
903
+ controller.abort();
904
+ await forwarderPromise;
905
+ });
906
+
907
+ it('drops thread-log activity when visibility.threads is false', async () => {
908
+ vi.useFakeTimers();
909
+ const controller = new AbortController();
910
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
911
+
912
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
913
+ chatId: 'mapped-chat',
914
+ signal: controller.signal,
915
+ discordConfig: {
916
+ botToken: 't',
917
+ authorizedUserId: 'user-123',
918
+ chatId: 'mapped-chat',
919
+ maxAttachmentSizeMB: 25,
920
+ requireMention: false,
921
+ visibility: { threads: false },
922
+ },
923
+ });
924
+
925
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
926
+
927
+ subscribeCallbacks.onData([
928
+ {
929
+ kind: 'turn',
930
+ event: {
931
+ type: 'started',
932
+ turnId: 'turn-1',
933
+ rootMessageId: 'msg-1',
934
+ externalRef: 'inbound-1',
935
+ },
936
+ },
937
+ {
938
+ kind: 'message',
939
+ message: {
940
+ id: 'msg-2',
941
+ role: 'tool',
942
+ name: 'Read',
943
+ payload: { file_path: '/foo/bar.txt' },
944
+ timestamp: '',
945
+ turnId: 'turn-1',
946
+ },
947
+ },
948
+ ]);
949
+
950
+ await vi.advanceTimersByTimeAsync(1500);
951
+ await vi.runOnlyPendingTimersAsync();
952
+
953
+ expect(mockUserMessage.startThread).not.toHaveBeenCalled();
954
+ expect(mockThread.send).not.toHaveBeenCalled();
955
+
956
+ controller.abort();
957
+ vi.useRealTimers();
958
+ await forwarderPromise;
959
+ });
960
+
961
+ it('drops thread-log activity when the channel has threadsDisabled', async () => {
962
+ vi.useFakeTimers();
963
+ const controller = new AbortController();
964
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
965
+
966
+ vi.mocked(readDiscordState).mockResolvedValue({
967
+ lastSyncedMessageIds: { 'mapped-chat': 'msg-0' },
968
+ channelChatMap: {
969
+ 'channel-123': { chatId: 'mapped-chat', threadsDisabled: true },
970
+ },
971
+ });
972
+
973
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
974
+ chatId: 'mapped-chat',
975
+ signal: controller.signal,
976
+ });
977
+
978
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
979
+
980
+ subscribeCallbacks.onData([
981
+ {
982
+ kind: 'turn',
983
+ event: {
984
+ type: 'started',
985
+ turnId: 'turn-1',
986
+ rootMessageId: 'msg-1',
987
+ externalRef: 'inbound-1',
988
+ },
989
+ },
990
+ {
991
+ kind: 'message',
992
+ message: {
993
+ id: 'msg-2',
994
+ role: 'tool',
995
+ name: 'Read',
996
+ payload: { file_path: '/foo/bar.txt' },
997
+ timestamp: '',
998
+ turnId: 'turn-1',
999
+ },
1000
+ },
1001
+ ]);
1002
+
1003
+ await vi.advanceTimersByTimeAsync(1500);
1004
+ await vi.runOnlyPendingTimersAsync();
1005
+
1006
+ expect(mockUserMessage.startThread).not.toHaveBeenCalled();
1007
+ expect(mockThread.send).not.toHaveBeenCalled();
1008
+
1009
+ controller.abort();
1010
+ vi.useRealTimers();
1011
+ await forwarderPromise;
1012
+ });
1013
+
1014
+ it('reuses an existing thread when the user message already has one', async () => {
1015
+ vi.useFakeTimers();
1016
+ const controller = new AbortController();
1017
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
1018
+
1019
+ mockUserMessage = {
1020
+ startThread: vi.fn(),
1021
+ hasThread: true,
1022
+ thread: mockThread,
1023
+ } as unknown as typeof mockUserMessage;
1024
+ mockChannel.messages.fetch = vi.fn().mockResolvedValue(mockUserMessage);
1025
+
1026
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
1027
+ chatId: 'mapped-chat',
1028
+ signal: controller.signal,
1029
+ });
1030
+
1031
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
1032
+
1033
+ subscribeCallbacks.onData([
1034
+ {
1035
+ kind: 'turn',
1036
+ event: {
1037
+ type: 'started',
1038
+ turnId: 'turn-1',
1039
+ rootMessageId: 'msg-1',
1040
+ externalRef: 'inbound-1',
1041
+ },
1042
+ },
1043
+ ]);
1044
+
1045
+ await vi.advanceTimersByTimeAsync(1500);
1046
+ await vi.runOnlyPendingTimersAsync();
1047
+
1048
+ expect(mockUserMessage.startThread).not.toHaveBeenCalled();
1049
+ expect(mockThread.send).toHaveBeenCalled();
1050
+
1051
+ controller.abort();
1052
+ vi.useRealTimers();
1053
+ await forwarderPromise;
1054
+ });
1055
+
1056
+ it('recovers when startThread races with another caller (160004)', async () => {
1057
+ vi.useFakeTimers();
1058
+ const controller = new AbortController();
1059
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
1060
+
1061
+ const raceErr: Error & { code?: number } = new Error(
1062
+ 'A thread has already been created for this message'
1063
+ );
1064
+ raceErr.code = 160004;
1065
+ mockUserMessage = {
1066
+ startThread: vi.fn().mockRejectedValue(raceErr),
1067
+ hasThread: false,
1068
+ thread: null,
1069
+ } as unknown as typeof mockUserMessage;
1070
+ mockChannel.messages.fetch = vi
1071
+ .fn()
1072
+ // Initial fetch: no thread yet.
1073
+ .mockResolvedValueOnce(mockUserMessage)
1074
+ // Refetch after 160004: thread now exists.
1075
+ .mockResolvedValueOnce({
1076
+ startThread: vi.fn(),
1077
+ hasThread: true,
1078
+ thread: mockThread,
1079
+ });
1080
+
1081
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
1082
+ chatId: 'mapped-chat',
1083
+ signal: controller.signal,
1084
+ });
1085
+
1086
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
1087
+
1088
+ subscribeCallbacks.onData([
1089
+ {
1090
+ kind: 'turn',
1091
+ event: {
1092
+ type: 'started',
1093
+ turnId: 'turn-1',
1094
+ rootMessageId: 'msg-1',
1095
+ externalRef: 'inbound-1',
1096
+ },
1097
+ },
1098
+ ]);
1099
+
1100
+ await vi.advanceTimersByTimeAsync(1500);
1101
+ await vi.runOnlyPendingTimersAsync();
1102
+
1103
+ expect(mockUserMessage.startThread).toHaveBeenCalled();
1104
+ expect(mockChannel.messages.fetch).toHaveBeenCalledTimes(2);
1105
+ expect(mockThread.send).toHaveBeenCalled();
1106
+
1107
+ controller.abort();
1108
+ vi.useRealTimers();
1109
+ await forwarderPromise;
1110
+ });
1111
+
1112
+ it('routes the final agent reply top-level, not into the thread', async () => {
1113
+ vi.useFakeTimers();
1114
+ const controller = new AbortController();
1115
+ recordInbound({ messageId: 'inbound-1', channelId: 'channel-123' });
1116
+
1117
+ const forwarderPromise = startDaemonToDiscordForwarder(mockClient, mockTrpc, 'user-123', {
1118
+ chatId: 'mapped-chat',
1119
+ signal: controller.signal,
1120
+ });
1121
+
1122
+ await vi.waitFor(() => expect(subscribeCallbacks).toBeTruthy());
1123
+
1124
+ subscribeCallbacks.onData([
1125
+ {
1126
+ kind: 'turn',
1127
+ event: {
1128
+ type: 'started',
1129
+ turnId: 'turn-1',
1130
+ rootMessageId: 'msg-1',
1131
+ externalRef: 'inbound-1',
1132
+ },
1133
+ },
1134
+ {
1135
+ kind: 'message',
1136
+ message: {
1137
+ id: 'msg-final',
1138
+ role: 'agent',
1139
+ content: 'Final reply',
1140
+ timestamp: '',
1141
+ turnId: 'turn-1',
1142
+ },
1143
+ },
1144
+ ]);
1145
+
1146
+ await vi.advanceTimersByTimeAsync(1500);
1147
+ await vi.runOnlyPendingTimersAsync();
1148
+
1149
+ expect(mockChannel.send).toHaveBeenCalledWith(
1150
+ expect.objectContaining({
1151
+ content: 'Final reply',
1152
+ allowedMentions: { parse: [] },
1153
+ })
1154
+ );
1155
+ // The thread itself doesn't carry the agent reply.
1156
+ const threadBody = mockThread.send.mock.calls.map((c) => c[0]?.content as string).join('\n');
1157
+ expect(threadBody).not.toContain('Final reply');
1158
+
1159
+ controller.abort();
1160
+ vi.useRealTimers();
1161
+ await forwarderPromise;
1162
+ });
1163
+ });
686
1164
  });