clawmini 0.0.3 → 0.0.5

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 (333) hide show
  1. package/README.md +19 -0
  2. package/dist/adapter-discord/index.d.mts.map +1 -1
  3. package/dist/adapter-discord/index.mjs +398 -193
  4. package/dist/adapter-discord/index.mjs.map +1 -1
  5. package/dist/adapter-google-chat/index.d.mts +5 -0
  6. package/dist/adapter-google-chat/index.d.mts.map +1 -0
  7. package/dist/adapter-google-chat/index.mjs +1077 -0
  8. package/dist/adapter-google-chat/index.mjs.map +1 -0
  9. package/dist/cli/index.mjs +107 -14
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/lite.mjs +175 -16
  12. package/dist/cli/lite.mjs.map +1 -1
  13. package/dist/cli/propose-policy.d.mts +1 -0
  14. package/dist/cli/propose-policy.mjs +7159 -0
  15. package/dist/cli/propose-policy.mjs.map +1 -0
  16. package/dist/daemon/index.d.mts.map +1 -1
  17. package/dist/daemon/index.mjs +1427 -513
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/{lite-oSYSvaOr.mjs → lite-CBxOT1y5.mjs} +101 -24
  20. package/dist/lite-CBxOT1y5.mjs.map +1 -0
  21. package/dist/routing-D8rTxtaV.mjs +245 -0
  22. package/dist/routing-D8rTxtaV.mjs.map +1 -0
  23. package/dist/web/_app/immutable/assets/0.C-4eziNy.css +1 -0
  24. package/dist/web/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
  25. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +1 -0
  26. package/dist/web/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
  27. package/{web/.svelte-kit/output/client/_app/immutable/chunks/8YNcRyEk.js → dist/web/_app/immutable/chunks/C20lZMGz.js} +1 -1
  28. package/dist/web/_app/immutable/chunks/C9lbZ-kT.js +1 -0
  29. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +2 -0
  30. package/dist/web/_app/immutable/chunks/CME08kGM.js +1 -0
  31. package/dist/web/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +1 -0
  33. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +2 -0
  34. package/dist/web/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
  36. package/dist/web/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
  37. package/dist/web/_app/immutable/chunks/DsIToJCP.js +1 -0
  38. package/dist/web/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
  39. package/{web/.svelte-kit/output/client/_app/immutable/entry/app.DO5eYwVz.js → dist/web/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
  40. package/dist/web/_app/immutable/entry/start.DuxJo6av.js +1 -0
  41. package/dist/web/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
  42. package/dist/web/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
  43. package/dist/web/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
  44. package/dist/web/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
  45. package/dist/web/_app/immutable/nodes/4.DLarELN4.js +60 -0
  46. package/dist/web/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
  47. package/dist/web/_app/version.json +1 -1
  48. package/dist/web/index.html +12 -12
  49. package/dist/{workspace-DjoNjhW0.mjs → workspace-BJmJBfKi.mjs} +103 -11
  50. package/dist/workspace-BJmJBfKi.mjs.map +1 -0
  51. package/docs/14_google_chat_adapter/development_log.md +40 -0
  52. package/docs/14_google_chat_adapter/notes.md +28 -0
  53. package/docs/14_google_chat_adapter/prd.md +35 -0
  54. package/docs/14_google_chat_adapter/questions.md +9 -0
  55. package/docs/14_google_chat_adapter/tickets.md +117 -0
  56. package/docs/15_sandbox_policies/tickets.md +33 -0
  57. package/docs/16_session_timeout/development_log.md +20 -0
  58. package/docs/16_session_timeout/notes.md +44 -0
  59. package/docs/16_session_timeout/prd.md +106 -0
  60. package/docs/16_session_timeout/questions.md +10 -0
  61. package/docs/16_session_timeout/tickets.md +64 -0
  62. package/docs/17_auto_approve_policy/development_log.md +29 -0
  63. package/docs/17_auto_approve_policy/notes.md +25 -0
  64. package/docs/17_auto_approve_policy/prd.md +34 -0
  65. package/docs/17_auto_approve_policy/questions.md +10 -0
  66. package/docs/17_auto_approve_policy/tickets.md +11 -0
  67. package/docs/18_clawmini_skills/development_log.md +36 -0
  68. package/docs/18_clawmini_skills/notes.md +8 -0
  69. package/docs/18_clawmini_skills/prd.md +45 -0
  70. package/docs/18_clawmini_skills/questions.md +10 -0
  71. package/docs/18_clawmini_skills/tickets.md +55 -0
  72. package/docs/19_subagents/development_log.md +69 -0
  73. package/docs/19_subagents/notes.md +18 -0
  74. package/docs/19_subagents/prd.md +156 -0
  75. package/docs/19_subagents/questions.md +13 -0
  76. package/docs/19_subagents/tickets.md +113 -0
  77. package/docs/20_chat_logs_cleanup/development_log.md +50 -0
  78. package/docs/20_chat_logs_cleanup/notes.md +43 -0
  79. package/docs/20_chat_logs_cleanup/prd.md +232 -0
  80. package/docs/20_chat_logs_cleanup/questions.md +2 -0
  81. package/docs/20_chat_logs_cleanup/tickets.md +98 -0
  82. package/docs/20_webui_markdown/development_log.md +36 -0
  83. package/docs/20_webui_markdown/notes.md +23 -0
  84. package/docs/20_webui_markdown/prd.md +49 -0
  85. package/docs/20_webui_markdown/questions.md +10 -0
  86. package/docs/20_webui_markdown/tickets.md +55 -0
  87. package/docs/21_adapter_filtering/development_log.md +29 -0
  88. package/docs/21_adapter_filtering/notes.md +25 -0
  89. package/docs/21_adapter_filtering/prd.md +44 -0
  90. package/docs/21_adapter_filtering/questions.md +12 -0
  91. package/docs/21_adapter_filtering/tickets.md +38 -0
  92. package/docs/21_built_in_routers/development_log.md +17 -0
  93. package/docs/21_built_in_routers/notes.md +27 -0
  94. package/docs/21_built_in_routers/prd.md +34 -0
  95. package/docs/21_built_in_routers/questions.md +4 -0
  96. package/docs/21_built_in_routers/tickets.md +25 -0
  97. package/docs/21_fancy_policies/development_log.md +38 -0
  98. package/docs/21_fancy_policies/notes.md +27 -0
  99. package/docs/21_fancy_policies/prd.md +58 -0
  100. package/docs/21_fancy_policies/questions.md +6 -0
  101. package/docs/21_fancy_policies/tickets.md +48 -0
  102. package/docs/22_adapter_multi_chat/development_log.md +76 -0
  103. package/docs/22_adapter_multi_chat/notes.md +42 -0
  104. package/docs/22_adapter_multi_chat/prd.md +76 -0
  105. package/docs/22_adapter_multi_chat/questions.md +16 -0
  106. package/docs/22_adapter_multi_chat/tickets.md +164 -0
  107. package/docs/23_custom_token_env/development_log.md +31 -0
  108. package/docs/23_custom_token_env/notes.md +16 -0
  109. package/docs/23_custom_token_env/prd.md +42 -0
  110. package/docs/23_custom_token_env/questions.md +8 -0
  111. package/docs/23_custom_token_env/tickets.md +54 -0
  112. package/docs/guides/discord_adapter_setup.md +15 -2
  113. package/docs/guides/google_chat_adapter_setup.md +145 -0
  114. package/napkin.md +5 -0
  115. package/package.json +7 -2
  116. package/src/adapter-discord/config.test.ts +27 -8
  117. package/src/adapter-discord/config.ts +6 -8
  118. package/src/adapter-discord/forwarder.test.ts +307 -114
  119. package/src/adapter-discord/forwarder.ts +260 -75
  120. package/src/adapter-discord/index.test.ts +278 -0
  121. package/src/adapter-discord/index.ts +160 -30
  122. package/src/adapter-discord/interactions.test.ts +96 -0
  123. package/src/adapter-discord/interactions.ts +156 -0
  124. package/src/adapter-discord/state.test.ts +9 -8
  125. package/src/adapter-discord/state.ts +51 -8
  126. package/src/adapter-google-chat/auth.test.ts +87 -0
  127. package/src/adapter-google-chat/auth.ts +132 -0
  128. package/src/adapter-google-chat/cards.ts +71 -0
  129. package/src/adapter-google-chat/client.test.ts +561 -0
  130. package/src/adapter-google-chat/client.ts +430 -0
  131. package/src/adapter-google-chat/config.test.ts +187 -0
  132. package/src/adapter-google-chat/config.ts +82 -0
  133. package/src/adapter-google-chat/cron.test.ts +143 -0
  134. package/src/adapter-google-chat/cron.ts +81 -0
  135. package/src/adapter-google-chat/forwarder.test.ts +537 -0
  136. package/src/adapter-google-chat/forwarder.ts +349 -0
  137. package/src/adapter-google-chat/index.test.ts +62 -0
  138. package/src/adapter-google-chat/index.ts +61 -0
  139. package/src/adapter-google-chat/state.test.ts +96 -0
  140. package/src/adapter-google-chat/state.ts +85 -0
  141. package/src/adapter-google-chat/subscriptions.ts +124 -0
  142. package/src/adapter-google-chat/upload.ts +88 -0
  143. package/src/adapter-google-chat/utils.test.ts +111 -0
  144. package/src/adapter-google-chat/utils.ts +133 -0
  145. package/src/cli/commands/init.ts +0 -7
  146. package/src/cli/commands/messages.ts +18 -3
  147. package/src/cli/commands/policies.ts +70 -0
  148. package/src/cli/commands/skills.ts +71 -0
  149. package/src/cli/commands/web-api/chats.ts +5 -1
  150. package/src/cli/e2e/basic.test.ts +1 -1
  151. package/src/cli/e2e/cron.test.ts +1 -1
  152. package/src/cli/e2e/daemon.test.ts +132 -4
  153. package/src/cli/e2e/export-lite-func.test.ts +54 -31
  154. package/src/cli/e2e/fallbacks.test.ts +8 -6
  155. package/src/cli/e2e/init.test.ts +7 -0
  156. package/src/cli/e2e/messages.test.ts +90 -55
  157. package/src/cli/e2e/propose-policy.test.ts +203 -0
  158. package/src/cli/e2e/requests.test.ts +15 -0
  159. package/src/cli/e2e/session-timeout.test.ts +192 -0
  160. package/src/cli/e2e/skills.test.ts +55 -0
  161. package/src/cli/e2e/slash-new.test.ts +93 -0
  162. package/src/cli/e2e/subagents.test.ts +106 -0
  163. package/src/cli/index.ts +4 -0
  164. package/src/cli/lite.ts +51 -11
  165. package/src/cli/propose-policy.ts +91 -0
  166. package/src/cli/subagent-commands.ts +215 -0
  167. package/src/daemon/agent/agent-context.ts +89 -0
  168. package/src/daemon/agent/agent-extractors.ts +68 -0
  169. package/src/daemon/agent/agent-runner.ts +153 -0
  170. package/src/daemon/agent/agent-session.ts +261 -0
  171. package/src/daemon/agent/chat-logger.test.ts +158 -0
  172. package/src/daemon/agent/chat-logger.ts +188 -0
  173. package/src/daemon/agent/task-scheduler.test.ts +202 -0
  174. package/src/daemon/agent/task-scheduler.ts +276 -0
  175. package/src/daemon/agent/types.ts +84 -0
  176. package/src/daemon/agent/utils.ts +7 -0
  177. package/src/daemon/api/agent-router.ts +166 -18
  178. package/src/daemon/api/index.test.ts +50 -18
  179. package/src/daemon/api/policy-request.test.ts +39 -2
  180. package/src/daemon/api/subagent-router.test.ts +108 -0
  181. package/src/daemon/api/subagent-router.ts +296 -0
  182. package/src/daemon/api/subagent-utils.test.ts +56 -0
  183. package/src/daemon/api/subagent-utils.ts +130 -0
  184. package/src/daemon/api/user-router.ts +30 -13
  185. package/src/daemon/auth.ts +1 -0
  186. package/src/daemon/chats.ts +6 -0
  187. package/src/daemon/cron.test.ts +66 -1
  188. package/src/daemon/cron.ts +35 -8
  189. package/src/daemon/index.ts +23 -0
  190. package/src/daemon/message-agent.test.ts +11 -25
  191. package/src/daemon/message-extraction.test.ts +10 -27
  192. package/src/daemon/message-fallbacks.test.ts +13 -35
  193. package/src/daemon/message-interruption.test.ts +70 -53
  194. package/src/daemon/message-jobs.test.ts +138 -0
  195. package/src/daemon/message-queue.test.ts +30 -43
  196. package/src/daemon/message-router.test.ts +12 -11
  197. package/src/daemon/message-session.test.ts +41 -28
  198. package/src/daemon/message-typing.test.ts +19 -6
  199. package/src/daemon/message.ts +103 -515
  200. package/src/daemon/policy-request-service.ts +8 -3
  201. package/src/daemon/policy-utils.ts +19 -1
  202. package/src/daemon/queue.ts +16 -0
  203. package/src/daemon/request-store.test.ts +4 -0
  204. package/src/daemon/routers/session-timeout.test.ts +122 -0
  205. package/src/daemon/routers/session-timeout.ts +71 -0
  206. package/src/daemon/routers/slash-new.ts +3 -1
  207. package/src/daemon/routers/slash-policies.test.ts +26 -13
  208. package/src/daemon/routers/slash-policies.ts +39 -29
  209. package/src/daemon/routers/types.ts +8 -0
  210. package/src/daemon/routers.ts +64 -2
  211. package/src/daemon/utils/spawn.ts +6 -8
  212. package/src/shared/adapters/commands.test.ts +155 -0
  213. package/src/shared/adapters/commands.ts +125 -0
  214. package/src/shared/adapters/filtering.test.ts +111 -0
  215. package/src/shared/adapters/filtering.ts +57 -0
  216. package/src/shared/adapters/routing.test.ts +144 -0
  217. package/src/shared/adapters/routing.ts +109 -0
  218. package/src/shared/agent-utils.ts +10 -0
  219. package/src/shared/chats.test.ts +145 -3
  220. package/src/shared/chats.ts +215 -18
  221. package/src/shared/config.ts +67 -15
  222. package/src/shared/lite.ts +22 -18
  223. package/src/shared/policies.ts +7 -0
  224. package/src/shared/workspace.test.ts +45 -1
  225. package/src/shared/workspace.ts +119 -6
  226. package/templates/debug/settings.json +5 -2
  227. package/templates/environments/cladding/env.json +2 -2
  228. package/templates/gemini/.gemini/hooks/check-subagents.mjs +23 -0
  229. package/templates/gemini/.gemini/hooks/clawmini-logging.sh +17 -0
  230. package/templates/gemini/.gemini/hooks/insert-pending.sh +9 -0
  231. package/templates/gemini/.gemini/settings.json +50 -0
  232. package/templates/gemini/settings.json +22 -8
  233. package/templates/gemini-claw/.gemini/base-system.md +100 -0
  234. package/templates/gemini-claw/.gemini/hooks/check-subagents.mjs +23 -0
  235. package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +1 -1
  236. package/templates/gemini-claw/.gemini/settings.json +13 -0
  237. package/templates/gemini-claw/.gemini/subagent-system.md +7 -0
  238. package/templates/gemini-claw/.gemini/system.md +3 -99
  239. package/templates/gemini-claw/settings.json +27 -22
  240. package/templates/skills/clawmini-requests/SKILL.md +92 -0
  241. package/templates/skills/clawmini-subagents/SKILL.md +79 -0
  242. package/templates/skills/skill-creator/SKILL.md +60 -0
  243. package/tsdown.config.ts +10 -1
  244. package/web/.svelte-kit/generated/server/internal.js +2 -1
  245. package/web/.svelte-kit/non-ambient.d.ts +2 -0
  246. package/web/.svelte-kit/output/client/.vite/manifest.json +141 -138
  247. package/web/.svelte-kit/output/client/_app/immutable/assets/0.C-4eziNy.css +1 -0
  248. package/web/.svelte-kit/output/client/_app/immutable/assets/4.Cc_xwLNl.css +1 -0
  249. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +1 -0
  250. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Dc-UOHw9.js → BmRlVmv6.js} +1 -1
  251. package/{dist/web/_app/immutable/chunks/8YNcRyEk.js → web/.svelte-kit/output/client/_app/immutable/chunks/C20lZMGz.js} +1 -1
  252. package/web/.svelte-kit/output/client/_app/immutable/chunks/C9lbZ-kT.js +1 -0
  253. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +2 -0
  254. package/web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js +1 -0
  255. package/web/.svelte-kit/output/client/_app/immutable/chunks/{BPy8HLo7.js → Ck-be5J2.js} +1 -1
  256. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +1 -0
  257. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +2 -0
  258. package/web/.svelte-kit/output/client/_app/immutable/chunks/{B8yYFADm.js → DhD271EB.js} +1 -1
  259. package/web/.svelte-kit/output/client/_app/immutable/chunks/{DcrmIfTj.js → DpuLqk8d.js} +1 -1
  260. package/web/.svelte-kit/output/client/_app/immutable/chunks/{ZkLyk0mE.js → Drm9vgeP.js} +1 -1
  261. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +1 -0
  262. package/web/.svelte-kit/output/client/_app/immutable/chunks/{CyNaE55B.js → Zeh-C-mx.js} +1 -1
  263. package/{dist/web/_app/immutable/entry/app.DO5eYwVz.js → web/.svelte-kit/output/client/_app/immutable/entry/app.BgB5VkRU.js} +2 -2
  264. package/web/.svelte-kit/output/client/_app/immutable/entry/start.DuxJo6av.js +1 -0
  265. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.C9oFZP9h.js +1 -0
  266. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.BON2Wk6k.js +1 -0
  267. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.CK3CLC0f.js → 2.BnwnD1Ki.js} +1 -1
  268. package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.ncP0xLO6.js → 3.CIs4tjjw.js} +1 -1
  269. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.DLarELN4.js +60 -0
  270. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BpJUN6QH.js → 5.CE_QKy_3.js} +1 -1
  271. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  272. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -3
  273. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.C-4eziNy.css +1 -0
  274. package/web/.svelte-kit/output/server/_app/immutable/assets/_page.Cc_xwLNl.css +1 -0
  275. package/web/.svelte-kit/output/server/chunks/app-state.svelte.js +5 -0
  276. package/web/.svelte-kit/output/server/chunks/bot.js +4 -4
  277. package/web/.svelte-kit/output/server/chunks/client.js +2 -1
  278. package/web/.svelte-kit/output/server/chunks/exports.js +0 -1
  279. package/web/.svelte-kit/output/server/chunks/internal.js +2 -1
  280. package/web/.svelte-kit/output/server/chunks/root.js +482 -392
  281. package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +57 -7
  282. package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +234 -9
  283. package/web/.svelte-kit/output/server/index.js +82 -10
  284. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  285. package/web/.svelte-kit/output/server/manifest.js +1 -1
  286. package/web/.svelte-kit/output/server/nodes/0.js +2 -2
  287. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  288. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  289. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  290. package/web/.svelte-kit/output/server/nodes/4.js +2 -2
  291. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  292. package/web/.svelte-kit/types/src/routes/$types.d.ts +1 -2
  293. package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +1 -2
  294. package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +1 -2
  295. package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +1 -2
  296. package/web/package.json +8 -0
  297. package/web/src/lib/app-state.svelte.ts +5 -1
  298. package/web/src/lib/components/app/markdown-renderer.svelte +56 -0
  299. package/web/src/lib/components/app/markdown-renderer.svelte.spec.ts +44 -0
  300. package/web/src/lib/components/app/message-content.svelte +16 -0
  301. package/web/src/lib/types.ts +67 -3
  302. package/web/src/routes/+layout.svelte +31 -1
  303. package/web/src/routes/chats/[id]/+page.svelte +167 -18
  304. package/web/src/routes/chats/[id]/page.svelte.spec.ts +58 -7
  305. package/dist/lite-oSYSvaOr.mjs.map +0 -1
  306. package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +0 -1
  307. package/dist/web/_app/immutable/chunks/B5abRDXp.js +0 -1
  308. package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
  309. package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +0 -2
  310. package/dist/web/_app/immutable/chunks/C3k55nDF.js +0 -1
  311. package/dist/web/_app/immutable/chunks/CpaGRn9L.js +0 -1
  312. package/dist/web/_app/immutable/chunks/DG5RZBw-.js +0 -2
  313. package/dist/web/_app/immutable/chunks/DQoygso7.js +0 -1
  314. package/dist/web/_app/immutable/entry/start.D48mVn1m.js +0 -1
  315. package/dist/web/_app/immutable/nodes/0.B-0CcADM.js +0 -1
  316. package/dist/web/_app/immutable/nodes/1.FixKgvRO.js +0 -1
  317. package/dist/web/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
  318. package/dist/workspace-DjoNjhW0.mjs.map +0 -1
  319. package/src/daemon/message-verbosity.test.ts +0 -127
  320. package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +0 -1
  321. package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +0 -1
  322. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +0 -1
  323. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +0 -2
  324. package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +0 -1
  325. package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +0 -1
  326. package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +0 -2
  327. package/web/.svelte-kit/output/client/_app/immutable/chunks/DQoygso7.js +0 -1
  328. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D48mVn1m.js +0 -1
  329. package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B-0CcADM.js +0 -1
  330. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.FixKgvRO.js +0 -1
  331. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CQYJEgv8.js +0 -1
  332. package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +0 -1
  333. /package/templates/{gemini-claw/.gemini/skills → skills}/clawmini-jobs/SKILL.md +0 -0
@@ -0,0 +1,84 @@
1
+ import type {
2
+ ChatMessage,
3
+ CommandLogMessage,
4
+ UserMessage,
5
+ SystemMessage,
6
+ AgentReplyMessage,
7
+ ToolMessage,
8
+ PolicyRequestMessage,
9
+ SubagentStatusMessage,
10
+ } from '../chats.js';
11
+
12
+ export interface Logger {
13
+ append<T extends ChatMessage>(msg: T): Promise<T>;
14
+ getMessages(limit?: number): Promise<ChatMessage[]>;
15
+ findLastMessage(predicate: (msg: ChatMessage) => boolean): Promise<ChatMessage | null>;
16
+ logUserMessage(msg: string): Promise<UserMessage>;
17
+ logSystemEvent(options: {
18
+ content: string;
19
+ level?: 'default' | 'debug' | 'verbose';
20
+ }): Promise<CommandLogMessage>;
21
+ logAutomaticReply(options: { messageId: string; content: string }): Promise<SystemMessage>;
22
+ logCommandRetry(options: {
23
+ messageId: string;
24
+ content: string;
25
+ cwd: string;
26
+ }): Promise<CommandLogMessage>;
27
+ logCommandResult(options: ExecutionResponse): Promise<CommandLogMessage>;
28
+ logSystemMessage(options: {
29
+ content: string;
30
+ event: SystemMessage['event'];
31
+ messageId?: string;
32
+ displayRole?: 'user' | 'agent';
33
+ }): Promise<SystemMessage>;
34
+ logSubagentStatus(options: {
35
+ subagentId: string;
36
+ status: 'completed' | 'failed';
37
+ }): Promise<SubagentStatusMessage>;
38
+ logAgentReply(options: { content: string; files?: string[] }): Promise<AgentReplyMessage>;
39
+ logToolMessage(options: {
40
+ content: string;
41
+ messageId: string;
42
+ name: string;
43
+ payload: unknown;
44
+ }): Promise<ToolMessage>;
45
+ logPolicyRequestMessage(options: {
46
+ content: string;
47
+ messageId: string;
48
+ requestId: string;
49
+ commandName: string;
50
+ args: string[];
51
+ status: 'pending' | 'approved' | 'rejected';
52
+ }): Promise<PolicyRequestMessage>;
53
+ }
54
+
55
+ export interface Message {
56
+ id: string;
57
+ content: string;
58
+ env: Record<string, string>;
59
+ }
60
+
61
+ export interface ExecutionResponse {
62
+ messageId: string;
63
+ content: string;
64
+ command: string;
65
+ cwd: string;
66
+ result: RunCommandResult;
67
+ extractedSessionId?: string | undefined;
68
+ }
69
+
70
+ export type RunCommandResult = {
71
+ stdout: string;
72
+ stderr: string;
73
+ exitCode: number;
74
+ };
75
+
76
+ export type RunCommandFn = (args: {
77
+ command: string;
78
+ cwd: string;
79
+ env: Record<string, string>;
80
+ stdin?: string | undefined;
81
+ signal?: AbortSignal | undefined;
82
+ }) => Promise<RunCommandResult>;
83
+
84
+ export type MaybePromise<T> = T | Promise<T>;
@@ -0,0 +1,7 @@
1
+ export function formatPendingMessages(payloads: string[]): string {
2
+ return payloads.map((text) => `<message>\n${text}\n</message>`).join('\n\n');
3
+ }
4
+
5
+ export function isNewSession(env: Record<string, string>): boolean {
6
+ return env['SESSION_ID'] === undefined;
7
+ }
@@ -2,15 +2,21 @@ import { z } from 'zod';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { TRPCError } from '@trpc/server';
5
- import { appendMessage, type CommandLogMessage } from '../chats.js';
6
- import { executeSafe, generateRequestPreview } from '../policy-utils.js';
5
+ import {
6
+ appendMessage,
7
+ type CommandLogMessage,
8
+ type AgentReplyMessage,
9
+ type ToolMessage,
10
+ type PolicyRequestMessage,
11
+ } from '../chats.js';
12
+ import { executeSafe, generateRequestPreview, executeRequest } from '../policy-utils.js';
7
13
  import { getWorkspaceRoot, readPolicies, getClawminiDir } from '../../shared/workspace.js';
8
14
  import { PolicyRequestService } from '../policy-request-service.js';
9
15
  import { RequestStore } from '../request-store.js';
10
16
  import { CronJobSchema } from '../../shared/config.js';
11
17
  import { apiProcedure, router } from './trpc.js';
12
- import { getMessageQueue } from '../queue.js';
13
- import { formatPendingMessages } from '../message.js';
18
+ import { taskScheduler } from '../agent/task-scheduler.js';
19
+ import { formatPendingMessages } from '../agent/utils.js';
14
20
  import {
15
21
  resolveAgentDir,
16
22
  validateLogFile,
@@ -48,14 +54,52 @@ export const logMessage = apiProcedure
48
54
  const logMsg: CommandLogMessage = {
49
55
  id,
50
56
  messageId: id,
51
- role: 'log',
52
- source: 'router',
57
+ role: 'command',
53
58
  content: messageStr,
59
+ stdout: '',
54
60
  stderr: '',
55
61
  timestamp,
56
62
  command: `clawmini-lite log${filesArgStr}`,
57
63
  cwd: process.cwd(),
58
64
  exitCode: 0,
65
+ ...(ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}),
66
+ ...(filePaths.length > 0 ? { files: filePaths } : {}),
67
+ };
68
+
69
+ await appendMessage(chatId, logMsg);
70
+ return { success: true };
71
+ });
72
+
73
+ export const logReplyMessage = apiProcedure
74
+ .input(
75
+ z.object({
76
+ message: z.string(),
77
+ files: z.array(z.string()).optional(),
78
+ })
79
+ )
80
+ .mutation(async ({ input, ctx }) => {
81
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
82
+ const chatId = ctx.tokenPayload.chatId;
83
+ const timestamp = new Date().toISOString();
84
+ const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
85
+
86
+ const filePaths: string[] = [];
87
+ if (input.files && input.files.length > 0) {
88
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
89
+ const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
90
+
91
+ for (const file of input.files) {
92
+ const validPath = await validateLogFile(file, agentDir, workspaceRoot);
93
+ filePaths.push(validPath);
94
+ }
95
+ }
96
+
97
+ const logMsg: AgentReplyMessage = {
98
+ id,
99
+ role: 'agent',
100
+ content: input.message,
101
+ timestamp,
102
+ ...(ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}),
59
103
  ...(filePaths.length > 0 ? { files: filePaths } : {}),
60
104
  };
61
105
 
@@ -63,6 +107,47 @@ export const logMessage = apiProcedure
63
107
  return { success: true };
64
108
  });
65
109
 
110
+ export const logToolMessage = apiProcedure
111
+ .input(
112
+ z.object({
113
+ name: z.string(),
114
+ payload: z.any().optional(),
115
+ })
116
+ )
117
+ .mutation(async ({ input, ctx }) => {
118
+ if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
119
+ const chatId = ctx.tokenPayload.chatId;
120
+ const timestamp = new Date().toISOString();
121
+ const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
122
+ const messageId = randomUUID();
123
+
124
+ const payloadObj = input.payload !== undefined ? input.payload : {};
125
+ let contentStr: string;
126
+ if (typeof payloadObj === 'string') {
127
+ contentStr = payloadObj;
128
+ } else {
129
+ try {
130
+ contentStr = JSON.stringify(payloadObj, null, 2);
131
+ } catch {
132
+ contentStr = String(payloadObj);
133
+ }
134
+ }
135
+
136
+ const logMsg: ToolMessage = {
137
+ id,
138
+ messageId,
139
+ role: 'tool',
140
+ name: input.name,
141
+ payload: payloadObj,
142
+ content: contentStr,
143
+ timestamp,
144
+ ...(ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}),
145
+ };
146
+
147
+ await appendMessage(chatId, logMsg);
148
+ return { success: true };
149
+ });
150
+
66
151
  export const agentListCronJobs = apiProcedure.query(async ({ ctx }) => {
67
152
  if (!ctx.tokenPayload) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing token' });
68
153
  const chatId = ctx.tokenPayload.chatId;
@@ -134,28 +219,70 @@ export const createPolicyRequest = apiProcedure
134
219
  const chatId = ctx.tokenPayload.chatId;
135
220
  const agentId = ctx.tokenPayload.agentId;
136
221
 
222
+ const config = await readPolicies();
223
+ const policy = config?.policies?.[input.commandName];
224
+
225
+ if (!policy) {
226
+ throw new TRPCError({
227
+ code: 'NOT_FOUND',
228
+ message: `Policy not found: ${input.commandName}`,
229
+ });
230
+ }
231
+
232
+ const isAutoApprove = !!policy.autoApprove;
233
+
137
234
  const request = await service.createRequest(
138
235
  input.commandName,
139
236
  input.args,
140
237
  input.fileMappings,
141
238
  chatId,
142
- agentId
239
+ agentId,
240
+ isAutoApprove,
241
+ ctx.tokenPayload.subagentId
143
242
  );
144
243
 
244
+ if (isAutoApprove) {
245
+ const { stdout, stderr, exitCode, commandStr } = await executeRequest(
246
+ request,
247
+ policy,
248
+ getWorkspaceRoot()
249
+ );
250
+
251
+ request.executionResult = { stdout, stderr, exitCode };
252
+ await store.save(request);
253
+
254
+ const logMsg: PolicyRequestMessage = {
255
+ id: randomUUID(),
256
+ // TODO: we should store the message ID in the CLAW_API_TOKEN, and extract it here
257
+ messageId: randomUUID(),
258
+ role: 'policy',
259
+ requestId: request.id,
260
+ commandName: input.commandName,
261
+ args: input.args,
262
+ status: 'approved',
263
+ content: `[Auto-approved] Policy ${input.commandName} was executed.\n\nCommand: ${commandStr}\nExit Code: ${exitCode}\n\nStdout:\n${stdout}\n\nStderr:\n${stderr}`,
264
+ timestamp: new Date().toISOString(),
265
+ ...(ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}),
266
+ };
267
+
268
+ await appendMessage(chatId, logMsg);
269
+ return request;
270
+ }
271
+
145
272
  const previewContent = await generateRequestPreview(request);
146
273
 
147
- const logMsg = {
274
+ const logMsg: PolicyRequestMessage = {
148
275
  id: randomUUID(),
149
276
  // TODO: we should store the message ID in the CLAW_API_TOKEN, and extract it here
150
277
  messageId: randomUUID(),
151
- role: 'log' as const,
152
- source: 'router' as const,
278
+ role: 'policy',
279
+ requestId: request.id,
280
+ commandName: input.commandName,
281
+ args: input.args,
282
+ status: 'pending',
153
283
  content: previewContent,
154
- stderr: '',
155
284
  timestamp: new Date().toISOString(),
156
- command: 'policy-request',
157
- cwd: process.cwd(),
158
- exitCode: 0,
285
+ displayRole: 'agent',
159
286
  };
160
287
 
161
288
  await appendMessage(chatId, logMsg);
@@ -165,19 +292,33 @@ export const createPolicyRequest = apiProcedure
165
292
  import { ping } from './user-router.js';
166
293
 
167
294
  export const fetchPendingMessages = apiProcedure.mutation(async ({ ctx }) => {
168
- const cwd = process.cwd();
169
- const queue = getMessageQueue(cwd);
295
+ if (!ctx.tokenPayload?.agentId) {
296
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing agent ID' });
297
+ }
170
298
  const targetSessionId = ctx.tokenPayload?.sessionId || 'default';
171
299
 
172
- const extracted = queue.extractPending((p) => p.sessionId === targetSessionId);
300
+ const extracted = taskScheduler.extractPending(targetSessionId);
173
301
  if (extracted.length === 0) {
174
302
  return { messages: '' };
175
303
  }
176
- return { messages: formatPendingMessages(extracted.map((p) => p.text)) };
304
+
305
+ return { messages: formatPendingMessages(extracted) };
177
306
  });
178
307
 
308
+ import {
309
+ subagentSpawn,
310
+ subagentSend,
311
+ subagentWait,
312
+ subagentStop,
313
+ subagentDelete,
314
+ subagentList,
315
+ subagentTail,
316
+ } from './subagent-router.js';
317
+
179
318
  export const agentRouter = router({
180
319
  logMessage,
320
+ logReplyMessage,
321
+ logToolMessage,
181
322
  listCronJobs: agentListCronJobs,
182
323
  addCronJob: agentAddCronJob,
183
324
  deleteCronJob: agentDeleteCronJob,
@@ -186,6 +327,13 @@ export const agentRouter = router({
186
327
  createPolicyRequest,
187
328
  fetchPendingMessages,
188
329
  ping,
330
+ subagentSpawn,
331
+ subagentSend,
332
+ subagentWait,
333
+ subagentStop,
334
+ subagentDelete,
335
+ subagentList,
336
+ subagentTail,
189
337
  });
190
338
 
191
339
  export type AgentRouter = typeof agentRouter;
@@ -6,7 +6,7 @@ import * as workspace from '../../shared/workspace.js';
6
6
  import * as chats from '../../shared/chats.js';
7
7
  import type { CronJob } from '../../shared/config.js';
8
8
  import * as message from '../message.js';
9
- import { getMessageQueue } from '../queue.js';
9
+ import { taskScheduler } from '../agent/task-scheduler.js';
10
10
  import * as fs from 'node:fs/promises';
11
11
  import path from 'node:path';
12
12
 
@@ -46,6 +46,7 @@ vi.mock('../../shared/workspace.js', async (importOriginal) => {
46
46
  const actual = await importOriginal<typeof import('../../shared/workspace.js')>();
47
47
  return {
48
48
  ...actual,
49
+ readSettings: vi.fn().mockResolvedValue(null),
49
50
  readChatSettings: vi.fn(),
50
51
  writeChatSettings: vi.fn(),
51
52
  getSettingsPath: vi.fn().mockReturnValue('/mock/settings.json'),
@@ -169,7 +170,6 @@ describe('Daemon TRPC Router', () => {
169
170
  {},
170
171
  undefined,
171
172
  false,
172
- expect.any(Function),
173
173
  undefined,
174
174
  undefined
175
175
  );
@@ -300,7 +300,7 @@ describe('Daemon TRPC Router', () => {
300
300
  expect(chats.appendMessage).toHaveBeenCalledWith(
301
301
  'default-chat',
302
302
  expect.objectContaining({
303
- role: 'log',
303
+ role: 'command',
304
304
  content: 'Test log',
305
305
  }),
306
306
  expect.any(String)
@@ -325,7 +325,7 @@ describe('Daemon TRPC Router', () => {
325
325
  expect(chats.appendMessage).toHaveBeenCalledWith(
326
326
  'default-chat',
327
327
  expect.objectContaining({
328
- role: 'log',
328
+ role: 'command',
329
329
  content: 'Test log with file',
330
330
  files: [path.normalize('attachments/discord/image.png')],
331
331
  }),
@@ -393,10 +393,9 @@ describe('Daemon TRPC Router', () => {
393
393
  });
394
394
 
395
395
  describe('fetchPendingMessages', () => {
396
- let queue: ReturnType<typeof getMessageQueue>;
397
396
  beforeEach(() => {
398
- queue = getMessageQueue(process.cwd());
399
- queue.clear();
397
+ taskScheduler.abortTasks('s1');
398
+ taskScheduler.abortTasks('s2');
400
399
  });
401
400
 
402
401
  it('should extract pending messages from queue matching the session and format them', async () => {
@@ -406,21 +405,54 @@ describe('Daemon TRPC Router', () => {
406
405
  });
407
406
 
408
407
  // The first task will start and block, leaving the others in pending
409
- queue.enqueue(
410
- async () => {
408
+ taskScheduler.schedule({
409
+ id: 't1',
410
+ rootChatId: 'c1',
411
+ dirPath: 'd1',
412
+ sessionId: 's1',
413
+ execute: async () => {
411
414
  await firstTaskPromise;
412
415
  },
413
- { text: 'Task 1', sessionId: 's1' }
414
- );
416
+ });
415
417
 
416
418
  // These will stay in pending
417
- const p2 = queue.enqueue(async () => {}, { text: 'Task 2', sessionId: 's1' });
418
- const p3 = queue.enqueue(async () => {}, { text: 'Task 3', sessionId: 's1' });
419
- const p4 = queue.enqueue(async () => {}, { text: 'Task 4', sessionId: 's2' });
419
+ const p2 = taskScheduler.schedule({
420
+ id: 't2',
421
+ rootChatId: 'c1', // force block
422
+ dirPath: 'd1',
423
+ sessionId: 's1',
424
+ text: 'Task 2',
425
+ execute: async () => {},
426
+ });
427
+ const p3 = taskScheduler.schedule({
428
+ id: 't3',
429
+ rootChatId: 'c1', // force block
430
+ dirPath: 'd1',
431
+ sessionId: 's1',
432
+ text: 'Task 3',
433
+ execute: async () => {},
434
+ });
435
+ const p3_5 = taskScheduler.schedule({
436
+ id: 't3_5',
437
+ rootChatId: 'c2', // force block
438
+ dirPath: 'd1',
439
+ sessionId: 's2',
440
+ text: 'Task 3.5',
441
+ execute: async () => {},
442
+ });
443
+ const p4 = taskScheduler.schedule({
444
+ id: 't4',
445
+ rootChatId: 'c2',
446
+ dirPath: 'd1',
447
+ sessionId: 's2',
448
+ text: 'Task 4',
449
+ execute: async () => {},
450
+ });
420
451
 
421
452
  // We expect them to throw AbortError when extracted
422
453
  p2.catch(() => {});
423
454
  p3.catch(() => {});
455
+ p3_5.catch(() => {});
424
456
  p4.catch(() => {});
425
457
 
426
458
  const caller = agentRouter.createCaller({
@@ -431,15 +463,15 @@ describe('Daemon TRPC Router', () => {
431
463
  expect(result.messages).toBe(
432
464
  '<message>\nTask 2\n</message>\n\n<message>\nTask 3\n</message>'
433
465
  );
434
- expect(queue.extractPending((p) => p.sessionId === 's2')).toEqual([
435
- { text: 'Task 4', sessionId: 's2' },
436
- ]);
466
+ expect(taskScheduler.extractPending('s2')).toEqual(['Task 4']);
437
467
 
438
468
  resolveFirstTask!(); // cleanup
439
469
  });
440
470
 
441
471
  it('should return empty string if no pending messages', async () => {
442
- const caller = agentRouter.createCaller({});
472
+ const caller = agentRouter.createCaller({
473
+ tokenPayload: { agentId: 'test', sessionId: 's1' } as any,
474
+ });
443
475
  const result = await caller.fetchPendingMessages();
444
476
  expect(result.messages).toBe('');
445
477
  });
@@ -11,6 +11,18 @@ vi.mock('../../shared/chats.js', () => ({
11
11
  vi.mock('../../shared/workspace.js', () => ({
12
12
  getWorkspaceRoot: vi.fn().mockReturnValue('/mock/workspace'),
13
13
  getClawminiDir: vi.fn().mockReturnValue('/mock/.clawmini'),
14
+ readPolicies: vi.fn().mockResolvedValue({
15
+ policies: {
16
+ 'test-cmd': {
17
+ command: 'echo',
18
+ autoApprove: false,
19
+ },
20
+ 'auto-cmd': {
21
+ command: 'echo',
22
+ autoApprove: true,
23
+ },
24
+ },
25
+ }),
14
26
  }));
15
27
 
16
28
  vi.mock('../policy-request-service.js', () => {
@@ -46,11 +58,13 @@ vi.mock('node:fs/promises', async (importOriginal) => {
46
58
  default: {
47
59
  ...actual,
48
60
  readFile: mockReadFile,
61
+ writeFile: vi.fn(),
49
62
  mkdir: vi.fn(),
50
63
  readdir: vi.fn().mockResolvedValue([]),
51
64
  realpath: vi.fn().mockImplementation((p) => Promise.resolve(p)),
52
65
  },
53
66
  readFile: mockReadFile,
67
+ writeFile: vi.fn(),
54
68
  mkdir: vi.fn(),
55
69
  readdir: vi.fn().mockResolvedValue([]),
56
70
  realpath: vi.fn().mockImplementation((p) => Promise.resolve(p)),
@@ -95,8 +109,7 @@ describe('createPolicyRequest preview message', () => {
95
109
  const logMsg = callArgs[1] as any;
96
110
 
97
111
  expect(chatId).toBe('default-chat');
98
- expect(logMsg.role).toBe('log');
99
- expect(logMsg.command).toBe('policy-request');
112
+ expect(logMsg.role).toBe('policy');
100
113
 
101
114
  // Assert preview content format
102
115
  const content = logMsg.content;
@@ -110,4 +123,28 @@ describe('createPolicyRequest preview message', () => {
110
123
  expect(content).toContain('File [file2]:\n' + 'A'.repeat(500) + '\n... (truncated)');
111
124
  expect(content).toContain('Use /approve req-123 or /reject req-123 [reason]');
112
125
  });
126
+
127
+ it('should create an auto-approved request and execute it immediately', async () => {
128
+ const caller = appRouter.createCaller({
129
+ isApiServer: true,
130
+ tokenPayload: { agentId: 'default', chatId: 'default-chat' },
131
+ } as any);
132
+
133
+ const result = await caller.createPolicyRequest({
134
+ commandName: 'auto-cmd',
135
+ args: ['hello'],
136
+ fileMappings: {},
137
+ });
138
+
139
+ expect(result.id).toBe('REQ-123');
140
+ expect(result.executionResult).toBeDefined();
141
+
142
+ expect(chats.appendMessage).toHaveBeenCalledTimes(1);
143
+ const callArgs = vi.mocked(chats.appendMessage).mock.calls[0]!;
144
+ const logMsg = callArgs[1] as any;
145
+
146
+ expect(logMsg.content).toContain('[Auto-approved] Policy auto-cmd was executed.');
147
+ expect(logMsg.role).toBe('policy');
148
+ expect(logMsg.status).toBe('approved');
149
+ });
113
150
  });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { agentRouter } from './agent-router.js';
3
+ import { daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED } from '../events.js';
4
+ import * as workspace from '../../shared/workspace.js';
5
+
6
+ vi.mock('../../shared/workspace.js', () => ({
7
+ readChatSettings: vi.fn(),
8
+ updateChatSettings: vi.fn(),
9
+ getWorkspaceRoot: vi.fn().mockReturnValue('/mock/root'),
10
+ }));
11
+
12
+ vi.mock('../agent/chat-logger.js', () => ({
13
+ createChatLogger: vi.fn(() => ({
14
+ findLastMessage: vi.fn().mockResolvedValue({ role: 'log', content: 'Mock output' }),
15
+ })),
16
+ }));
17
+
18
+ describe('subagentWait', () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ afterEach(() => {
24
+ daemonEvents.removeAllListeners();
25
+ });
26
+
27
+ it('should not miss events if subagent completes immediately after checkSubagentStatus starts', async () => {
28
+ // We simulate checkSubagentStatus taking some time (e.g. reading from disk).
29
+ // While it is awaiting, an event is emitted indicating completion.
30
+ // The wait procedure should still catch the completion.
31
+
32
+ const subagentId = 'sub-1';
33
+ const chatId = 'chat-1';
34
+
35
+ let firstCall = true;
36
+
37
+ vi.mocked(workspace.readChatSettings).mockImplementation(async (_id: string) => {
38
+ if (firstCall) {
39
+ firstCall = false;
40
+ // Emit the event while the first check is "in flight"
41
+ // This simulates the race condition where the status is "active" at the exact moment
42
+ // of reading, but changes immediately after before the event listener would be bound in the buggy code.
43
+ setTimeout(() => {
44
+ daemonEvents.emit(DAEMON_EVENT_MESSAGE_APPENDED, {
45
+ chatId,
46
+ message: { role: 'subagent_status', status: 'completed', subagentId },
47
+ });
48
+ }, 10);
49
+
50
+ await new Promise((r) => setTimeout(r, 50));
51
+ return {
52
+ subagents: {
53
+ [subagentId]: { status: 'active', id: subagentId, createdAt: '2023-01-01' },
54
+ },
55
+ };
56
+ } else {
57
+ return {
58
+ subagents: {
59
+ [subagentId]: { status: 'completed', id: subagentId, createdAt: '2023-01-01' },
60
+ },
61
+ };
62
+ }
63
+ });
64
+
65
+ const ctx = {
66
+ tokenPayload: { chatId, agentId: 'agent', sessionId: 'session' },
67
+ };
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const caller = agentRouter.createCaller(ctx as any);
70
+
71
+ const resultPromise = caller.subagentWait({ subagentId });
72
+
73
+ // We don't want the test to hang if the bug is present, so we use Promise.race
74
+ const timeoutPromise = new Promise((_, reject) =>
75
+ setTimeout(() => reject(new Error('Timeout - event missed')), 500)
76
+ );
77
+
78
+ const result = await Promise.race([resultPromise, timeoutPromise]);
79
+ expect(result).toEqual({ status: 'completed', output: 'Mock output' });
80
+ });
81
+
82
+ it('should clean up the event listener if it returns early due to initial status check', async () => {
83
+ const subagentId = 'sub-2';
84
+ const chatId = 'chat-2';
85
+
86
+ // Mock an immediate completed status
87
+ vi.mocked(workspace.readChatSettings).mockResolvedValue({
88
+ subagents: {
89
+ [subagentId]: { status: 'completed', id: subagentId, createdAt: '2023-01-01' },
90
+ },
91
+ });
92
+
93
+ const ctx = {
94
+ tokenPayload: { chatId, agentId: 'agent', sessionId: 'session' },
95
+ };
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ const caller = agentRouter.createCaller(ctx as any);
98
+
99
+ const initialListeners = daemonEvents.listenerCount(DAEMON_EVENT_MESSAGE_APPENDED);
100
+
101
+ const result = await caller.subagentWait({ subagentId });
102
+
103
+ expect(result).toEqual({ status: 'completed', output: 'Mock output' });
104
+
105
+ const finalListeners = daemonEvents.listenerCount(DAEMON_EVENT_MESSAGE_APPENDED);
106
+ expect(finalListeners).toBe(initialListeners); // Should not leave any lingering listeners
107
+ });
108
+ });