clawmini 0.0.8 → 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/{vDehDcuJ.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.CUGC2p-K.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.0arZe_Uf.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.Bq2JzCEj.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 +0 -1
  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 -118
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +126 -136
  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/{vDehDcuJ.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.CUGC2p-K.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.0arZe_Uf.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.Bq2JzCEj.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/D5iV40bG.js +0 -1
  328. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  329. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  331. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.ClM1bXLE.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/D5iV40bG.js +0 -1
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.ClM1bXLE.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
@@ -4,7 +4,13 @@ import path from 'node:path';
4
4
  import { TRPCError } from '@trpc/server';
5
5
  import { pathIsInsideDir } from '../../shared/utils/fs.js';
6
6
  import { on } from 'node:events';
7
- import { daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, DAEMON_EVENT_TYPING } from '../events.js';
7
+ import {
8
+ daemonEvents,
9
+ DAEMON_EVENT_CHAT_STREAM,
10
+ DAEMON_EVENT_TYPING,
11
+ type ChatStreamEnvelope,
12
+ type ChatStreamItem,
13
+ } from '../events.js';
8
14
  import {
9
15
  getSettingsPath,
10
16
  readChatSettings,
@@ -39,6 +45,7 @@ export const sendMessage = apiProcedure
39
45
  noWait: z.boolean().optional(),
40
46
  files: z.array(z.string()).optional(),
41
47
  adapter: z.string().optional(),
48
+ externalRef: z.string().optional(),
42
49
  }),
43
50
  })
44
51
  )
@@ -99,7 +106,16 @@ export const sendMessage = apiProcedure
99
106
  message = message ? `${message}\n\n${fileList}` : fileList;
100
107
  }
101
108
 
102
- await handleUserMessage(chatId, message, settings, undefined, noWait, sessionId, agentId);
109
+ await handleUserMessage(
110
+ chatId,
111
+ message,
112
+ settings,
113
+ undefined,
114
+ noWait,
115
+ sessionId,
116
+ agentId,
117
+ input.data.externalRef
118
+ );
103
119
 
104
120
  return { success: true };
105
121
  });
@@ -111,6 +127,11 @@ export const getMessages = apiProcedure
111
127
  return fetchMessages(chatId, input.limit);
112
128
  });
113
129
 
130
+ /**
131
+ * Interleaved chat stream: `ChatMessage` appends and turn lifecycle events
132
+ * arrive on a single subscription in emission order. Consumers that only
133
+ * care about messages can ignore items with `kind: 'turn'`.
134
+ */
114
135
  export const waitForMessages = apiProcedure
115
136
  .input(
116
137
  z.object({
@@ -121,20 +142,25 @@ export const waitForMessages = apiProcedure
121
142
  .subscription(async function* ({ input, signal }) {
122
143
  const chatId = input.chatId ?? (await getDefaultChatId());
123
144
 
124
- // 1. Check if there are already new messages
145
+ // 1. Catch up on any messages missed since `lastMessageId`. Turn events
146
+ // have no persisted backlog — they're live-only, like today.
125
147
  if (input.lastMessageId) {
126
148
  const messages = await fetchMessages(chatId);
127
149
  const lastIndex = messages.findIndex((m) => m.id === input.lastMessageId);
128
150
  if (lastIndex !== -1 && lastIndex < messages.length - 1) {
129
- yield messages.slice(lastIndex + 1);
151
+ const items: ChatStreamItem[] = messages
152
+ .slice(lastIndex + 1)
153
+ .map((message) => ({ kind: 'message', message }));
154
+ yield items;
130
155
  }
131
156
  }
132
157
 
133
- // 2. Listen for new messages
158
+ // 2. Live: messages and turn events on the same channel, in emission order.
134
159
  try {
135
- for await (const [event] of on(daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, { signal })) {
136
- if (event.chatId === chatId) {
137
- yield [event.message];
160
+ for await (const [envelope] of on(daemonEvents, DAEMON_EVENT_CHAT_STREAM, { signal })) {
161
+ const e = envelope as ChatStreamEnvelope;
162
+ if (e.chatId === chatId) {
163
+ yield [e.item];
138
164
  }
139
165
  }
140
166
  } catch (err) {
@@ -10,6 +10,7 @@ export interface TokenPayload {
10
10
  agentId: string;
11
11
  sessionId: string;
12
12
  subagentId?: string;
13
+ turnId?: string;
13
14
  timestamp: number;
14
15
  }
15
16
 
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- import { CronManager } from './cron.js';
3
+ import { CronManager, normalizeJob } from './cron.js';
4
4
  // @ts-expect-error - node-schedule types are missing
5
5
  import schedule from 'node-schedule';
6
6
  import { getInitialRouterState } from './message.js';
@@ -49,18 +49,76 @@ describe('CronManager', () => {
49
49
  }).toThrow("Invalid date format for 'at' schedule: invalid-date");
50
50
  });
51
51
 
52
- it('correctly schedules an interval "at" schedule', () => {
52
+ it('fires an overdue "at" job immediately instead of dropping it', () => {
53
+ const cronManager = new CronManager();
54
+ vi.mocked(schedule.scheduleJob as any).mockImplementation((rule: any, _cb: any) => {
55
+ return { cancel: vi.fn(), rule } as any;
56
+ });
57
+
58
+ const past = new Date(Date.now() - 60_000).toISOString();
59
+ cronManager.scheduleJob('chat-overdue', {
60
+ id: 'job-overdue',
61
+ createdAt: new Date().toISOString(),
62
+ message: 'hello',
63
+ schedule: { at: past },
64
+ });
65
+
66
+ expect(schedule.scheduleJob).toHaveBeenCalledTimes(1);
67
+ const [rule] = vi.mocked(schedule.scheduleJob as any).mock.calls[0]!;
68
+ expect(rule).toBeInstanceOf(Date);
69
+ expect((rule as Date).getTime()).toBeGreaterThan(Date.now() - 1_000);
70
+ });
71
+
72
+ it('correctly schedules an absolute "at" schedule', () => {
53
73
  const cronManager = new CronManager();
54
74
  expect(() => {
55
75
  cronManager.scheduleJob('chat2', {
56
76
  id: 'job2',
57
77
  createdAt: new Date().toISOString(),
58
78
  message: 'hello',
59
- schedule: { at: '2m' },
79
+ schedule: { at: new Date(Date.now() + 120_000).toISOString() },
60
80
  });
61
81
  }).not.toThrow();
62
82
  });
63
83
 
84
+ it('normalizeJob resolves interval "at" to an absolute ISO timestamp', () => {
85
+ const before = Date.now();
86
+ const normalized = normalizeJob({
87
+ id: 'job-norm',
88
+ createdAt: new Date().toISOString(),
89
+ message: 'hi',
90
+ schedule: { at: '2s' },
91
+ });
92
+ const after = Date.now();
93
+ expect('at' in normalized.schedule).toBe(true);
94
+ const at = (normalized.schedule as { at: string }).at;
95
+ const ms = new Date(at).getTime();
96
+ expect(ms).toBeGreaterThanOrEqual(before + 2000);
97
+ expect(ms).toBeLessThanOrEqual(after + 2000);
98
+ });
99
+
100
+ it('normalizeJob preserves an already-absolute "at" value', () => {
101
+ const iso = new Date(Date.now() + 60_000).toISOString();
102
+ const normalized = normalizeJob({
103
+ id: 'job-abs',
104
+ createdAt: new Date().toISOString(),
105
+ message: 'hi',
106
+ schedule: { at: iso },
107
+ });
108
+ expect((normalized.schedule as { at: string }).at).toBe(iso);
109
+ });
110
+
111
+ it('normalizeJob throws on unparseable "at" value', () => {
112
+ expect(() =>
113
+ normalizeJob({
114
+ id: 'job-bad',
115
+ createdAt: new Date().toISOString(),
116
+ message: 'hi',
117
+ schedule: { at: 'not-a-date' },
118
+ })
119
+ ).toThrow("Invalid date format for 'at' schedule: not-a-date");
120
+ });
121
+
64
122
  it('passes job.session.id to getInitialRouterState when session type is existing', async () => {
65
123
  const cronManager = new CronManager();
66
124
  let scheduledCallback: any = null;
@@ -73,7 +131,7 @@ describe('CronManager', () => {
73
131
  id: 'job3',
74
132
  createdAt: new Date().toISOString(),
75
133
  message: 'test existing session',
76
- schedule: { at: '1m' },
134
+ schedule: { at: new Date(Date.now() + 60_000).toISOString() },
77
135
  session: { type: 'existing', id: 'my-old-session-id' },
78
136
  });
79
137
 
@@ -9,6 +9,34 @@ import fs from 'node:fs/promises';
9
9
  import { getSettingsPath } from '../shared/workspace.js';
10
10
  import { applyEnvOverrides } from '../shared/utils/env.js';
11
11
 
12
+ // Resolves relative `at` intervals (e.g. "2s", "5m") to absolute ISO timestamps
13
+ // so a job's target time is stable across daemon restarts. Throws on unparseable
14
+ // `at` values so invalid input is rejected at creation rather than silently
15
+ // shifted by `scheduleJob`. `cron` and `every` schedules pass through unchanged.
16
+ export function normalizeJob(job: CronJob): CronJob {
17
+ if (!('at' in job.schedule)) return job;
18
+
19
+ const atStr = job.schedule.at;
20
+ const match = atStr.match(/^(\d+)\s*(m|min|minutes?|h|hours?|d|days?|s|sec|seconds?)$/i);
21
+ let absolute: Date;
22
+ if (match) {
23
+ const val = parseInt(match[1]!, 10);
24
+ const unit = match[2]!.toLowerCase();
25
+ let ms = 0;
26
+ if (unit.startsWith('s')) ms = val * 1000;
27
+ else if (unit.startsWith('m')) ms = val * 60 * 1000;
28
+ else if (unit.startsWith('h')) ms = val * 60 * 60 * 1000;
29
+ else if (unit.startsWith('d')) ms = val * 24 * 60 * 60 * 1000;
30
+ absolute = new Date(Date.now() + ms);
31
+ } else {
32
+ absolute = new Date(atStr);
33
+ if (isNaN(absolute.getTime())) {
34
+ throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
35
+ }
36
+ }
37
+ return { ...job, schedule: { at: absolute.toISOString() } };
38
+ }
39
+
12
40
  export class CronManager {
13
41
  private jobs = new Map<string, schedule.Job>();
14
42
 
@@ -62,22 +90,19 @@ export class CronManager {
62
90
  rule = everyStr;
63
91
  }
64
92
  } else if ('at' in job.schedule) {
65
- const atStr = (job.schedule as { at: string }).at;
66
- const match = atStr.match(/^(\d+)\s*(m|min|minutes?|h|hours?|d|days?|s|sec|seconds?)$/i);
67
- if (match) {
68
- const val = parseInt(match[1]!, 10);
69
- const unit = match[2]!.toLowerCase();
70
- let ms = 0;
71
- if (unit.startsWith('s')) ms = val * 1000;
72
- else if (unit.startsWith('m')) ms = val * 60 * 1000;
73
- else if (unit.startsWith('h')) ms = val * 60 * 60 * 1000;
74
- else if (unit.startsWith('d')) ms = val * 24 * 60 * 60 * 1000;
75
- rule = new Date(Date.now() + ms);
76
- } else {
77
- rule = new Date(atStr);
78
- if (isNaN(rule.getTime())) {
79
- throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
80
- }
93
+ const atStr = job.schedule.at;
94
+ rule = new Date(atStr);
95
+ if (isNaN(rule.getTime())) {
96
+ throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
97
+ }
98
+ // If the target time has already passed (e.g. daemon was down past it),
99
+ // node-schedule returns null and the job silently never fires. Push it
100
+ // just into the future so it executes on the next tick and cleans up.
101
+ if (rule.getTime() <= Date.now()) {
102
+ console.warn(
103
+ `One-off job ${job.id} for chat ${chatId} is overdue (target ${atStr}); firing immediately.`
104
+ );
105
+ rule = new Date(Date.now() + 100);
81
106
  }
82
107
  isOneOff = true;
83
108
  } else {
@@ -144,6 +169,7 @@ export class CronManager {
144
169
  routerState.nextSessionId = job.nextSessionId;
145
170
  if (job.action !== undefined) routerState.action = job.action;
146
171
  if (job.jobs !== undefined) routerState.jobs = job.jobs;
172
+ routerState.jobId = job.id;
147
173
 
148
174
  const routers = chatSettings.routers ?? globalSettings?.routers ?? [];
149
175
  const resolvedRouters = resolveRouters(routers, false);
@@ -5,11 +5,76 @@ export const daemonEvents = new EventEmitter();
5
5
 
6
6
  export const DAEMON_EVENT_MESSAGE_APPENDED = 'message-appended';
7
7
  export const DAEMON_EVENT_TYPING = 'typing';
8
+ export const DAEMON_EVENT_TURN_STARTED = 'turn-started';
9
+ export const DAEMON_EVENT_TURN_ENDED = 'turn-ended';
10
+ /**
11
+ * Unified event carrying both `ChatMessage` appends and turn lifecycle
12
+ * events so a single `waitForMessages` subscription can interleave them
13
+ * in emission order (no merge logic or messageQueue on the consumer side).
14
+ */
15
+ export const DAEMON_EVENT_CHAT_STREAM = 'chat-stream';
16
+
17
+ export interface TurnStartedEvent {
18
+ chatId: string;
19
+ turnId: string;
20
+ rootMessageId: string;
21
+ externalRef?: string;
22
+ }
23
+
24
+ export interface TurnEndedEvent {
25
+ chatId: string;
26
+ turnId: string;
27
+ outcome: 'ok' | 'error';
28
+ }
29
+
30
+ export type TurnLifecycleEvent =
31
+ | { type: 'started'; turnId: string; rootMessageId: string; externalRef?: string }
32
+ | { type: 'ended'; turnId: string; outcome: 'ok' | 'error' };
33
+
34
+ export type ChatStreamItem =
35
+ | { kind: 'message'; message: ChatMessage }
36
+ | { kind: 'turn'; event: TurnLifecycleEvent };
37
+
38
+ export interface ChatStreamEnvelope {
39
+ chatId: string;
40
+ item: ChatStreamItem;
41
+ }
8
42
 
9
43
  export function emitMessageAppended(chatId: string, message: ChatMessage) {
10
44
  daemonEvents.emit(DAEMON_EVENT_MESSAGE_APPENDED, { chatId, message });
45
+ const envelope: ChatStreamEnvelope = { chatId, item: { kind: 'message', message } };
46
+ daemonEvents.emit(DAEMON_EVENT_CHAT_STREAM, envelope);
11
47
  }
12
48
 
13
49
  export function emitTyping(chatId: string) {
14
50
  daemonEvents.emit(DAEMON_EVENT_TYPING, { chatId });
15
51
  }
52
+
53
+ export function emitTurnStarted(event: TurnStartedEvent) {
54
+ daemonEvents.emit(DAEMON_EVENT_TURN_STARTED, event);
55
+ const lifecycle: TurnLifecycleEvent = {
56
+ type: 'started',
57
+ turnId: event.turnId,
58
+ rootMessageId: event.rootMessageId,
59
+ ...(event.externalRef ? { externalRef: event.externalRef } : {}),
60
+ };
61
+ const envelope: ChatStreamEnvelope = {
62
+ chatId: event.chatId,
63
+ item: { kind: 'turn', event: lifecycle },
64
+ };
65
+ daemonEvents.emit(DAEMON_EVENT_CHAT_STREAM, envelope);
66
+ }
67
+
68
+ export function emitTurnEnded(event: TurnEndedEvent) {
69
+ daemonEvents.emit(DAEMON_EVENT_TURN_ENDED, event);
70
+ const lifecycle: TurnLifecycleEvent = {
71
+ type: 'ended',
72
+ turnId: event.turnId,
73
+ outcome: event.outcome,
74
+ };
75
+ const envelope: ChatStreamEnvelope = {
76
+ chatId: event.chatId,
77
+ item: { kind: 'turn', event: lifecycle },
78
+ };
79
+ daemonEvents.emit(DAEMON_EVENT_CHAT_STREAM, envelope);
80
+ }
@@ -20,6 +20,9 @@ import { SettingsSchema } from '../shared/config.js';
20
20
  import { validateToken, getApiContext } from './auth.js';
21
21
  import path from 'node:path';
22
22
  import { exportLiteToEnvironment } from '../shared/lite.js';
23
+ import { RequestStore } from './request-store.js';
24
+ import { drainPendingReplies } from './pending-replies.js';
25
+ import { getClawminiVersion } from '../shared/version.js';
23
26
 
24
27
  export async function initDaemon() {
25
28
  const socketPath = getSocketPath();
@@ -66,11 +69,13 @@ export async function initDaemon() {
66
69
  const command = envConfig?.[hookType];
67
70
  if (command) {
68
71
  console.log(`Executing '${hookType}' hook for environment '${envName}': ${command}`);
72
+ // No timeout: a SIGTERM mid-hook risks partial teardown (e.g.
73
+ // containers left running). The supervisor's per-service SIGKILL
74
+ // timer is the ultimate backstop for runaway hooks.
69
75
  execSync(command, {
70
76
  cwd: affectedDir,
71
77
  stdio: 'inherit',
72
78
  env: { ...process.env, ENV_DIR: envDir },
73
- timeout: hookType === 'down' ? 10000 : undefined,
74
79
  });
75
80
  }
76
81
  } catch (err) {
@@ -163,11 +168,29 @@ export async function initDaemon() {
163
168
  };
164
169
  await cleanOrphanedSubagents();
165
170
 
171
+ try {
172
+ const removed = await new RequestStore(getWorkspaceRoot()).cleanupCompleted();
173
+ if (removed > 0) {
174
+ console.log(`Cleaned up ${removed} completed policy request file(s).`);
175
+ }
176
+ } catch (err) {
177
+ console.warn('Failed to clean completed policy requests:', err);
178
+ }
179
+
166
180
  await runHooks('up');
167
181
 
168
182
  isReady = true;
169
183
  readyPromiseResolve!();
170
184
 
185
+ // Drain any replies queued by the previous daemon instance (e.g. /restart
186
+ // or /upgrade). This must run after the daemon is `isReady` so the tRPC
187
+ // subscription that the adapter forwarder reconnects to can deliver them.
188
+ try {
189
+ await drainPendingReplies(getClawminiVersion());
190
+ } catch (err) {
191
+ console.warn('Failed to drain pending replies:', err);
192
+ }
193
+
171
194
  // Initialize cron jobs
172
195
  cronManager.init().catch((err) => {
173
196
  console.error('Failed to initialize cron manager:', err);
@@ -19,6 +19,7 @@ vi.mock('../shared/workspace.js', () => ({
19
19
 
20
20
  readChatSettings: vi.fn().mockResolvedValue(null),
21
21
  writeChatSettings: vi.fn().mockResolvedValue(undefined),
22
+ updateChatSettings: vi.fn().mockResolvedValue(undefined),
22
23
  readAgentSessionSettings: vi.fn().mockResolvedValue(null),
23
24
  writeAgentSessionSettings: vi.fn().mockResolvedValue(undefined),
24
25
  getAgent: vi.fn().mockResolvedValue(null),
@@ -36,6 +36,7 @@ vi.mock('./cron.js', () => ({
36
36
  scheduleJob: vi.fn(),
37
37
  unscheduleJob: vi.fn(),
38
38
  },
39
+ normalizeJob: (job: unknown) => job,
39
40
  }));
40
41
 
41
42
  describe('Jobs and Session Handling in handleUserMessage', () => {
@@ -1,11 +1,15 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { executeRouterPipeline, resolveRouters } from './routers.js';
2
3
  import type { RouterState } from './routers/types.js';
3
4
  import { type ChatSettings, type Settings } from '../shared/config.js';
4
- import { readChatSettings, writeChatSettings } from '../shared/workspace.js';
5
- import { cronManager } from './cron.js';
5
+ import { readChatSettings, updateChatSettings, writeChatSettings } from '../shared/workspace.js';
6
+ import { cronManager, normalizeJob } from './cron.js';
6
7
  import type { Message } from './agent/types.js';
7
8
  import { createAgentSession } from './agent/agent-session.js';
8
9
  import { createChatLogger } from './agent/chat-logger.js';
10
+ import { taskScheduler } from './agent/task-scheduler.js';
11
+ import { emitTurnStarted } from './events.js';
12
+ import { registerTurn, markParentExited } from './agent/turn-registry.js';
9
13
 
10
14
  export { calculateDelay } from './agent/agent-runner.js';
11
15
 
@@ -24,9 +28,12 @@ export async function executeDirectMessage(
24
28
  | 'subagent_update'
25
29
  | 'router'
26
30
  | 'other',
27
- displayRole?: 'user' | 'agent'
31
+ displayRole?: 'user' | 'agent',
32
+ parentTurnId?: string
28
33
  ) {
29
- const logger = createChatLogger(chatId, subagentId);
34
+ const turnId = parentTurnId ?? randomUUID();
35
+ const emitLifecycle = !parentTurnId;
36
+ const logger = createChatLogger(chatId, subagentId, state.sessionId, turnId);
30
37
 
31
38
  let msgId: string;
32
39
  if (systemEvent) {
@@ -35,6 +42,7 @@ export async function executeDirectMessage(
35
42
  event: systemEvent,
36
43
  messageId: state.messageId,
37
44
  ...(displayRole ? { displayRole } : {}),
45
+ ...(state.jobId ? { jobId: state.jobId } : {}),
38
46
  });
39
47
  msgId = sysMsg.id;
40
48
  } else {
@@ -59,36 +67,57 @@ export async function executeDirectMessage(
59
67
  cwd,
60
68
  settings,
61
69
  logger,
70
+ turnId,
62
71
  });
63
72
  let finalMessage: Message = {
64
73
  id: state.messageId,
65
74
  content: state.message,
66
75
  env: state.env ?? {},
76
+ turnId,
67
77
  };
68
78
 
69
79
  // Process actions
70
80
  if (state.action === 'stop') {
71
81
  agentSession.stop();
82
+ if (!subagentId) {
83
+ await stopActiveSubagents(chatId, cwd);
84
+ }
72
85
  return;
73
86
  }
74
87
  if (state.action === 'interrupt') {
75
88
  finalMessage = agentSession.interrupt(finalMessage);
76
89
  }
77
90
 
91
+ if (emitLifecycle) {
92
+ registerTurn(chatId, turnId);
93
+ emitTurnStarted({
94
+ chatId,
95
+ turnId,
96
+ rootMessageId: msgId,
97
+ ...(state.externalRef ? { externalRef: state.externalRef } : {}),
98
+ });
99
+ }
100
+
78
101
  // Process message
79
102
  const taskPromise = agentSession.handleMessage(finalMessage);
80
103
 
81
- if (!noWait) {
104
+ const settleTurn = async () => {
82
105
  try {
83
106
  await taskPromise;
107
+ if (emitLifecycle) markParentExited(turnId, 'ok');
84
108
  } catch (err) {
109
+ if (emitLifecycle) markParentExited(turnId, 'error');
85
110
  if (!(err instanceof Error && err.name === 'AbortError')) {
86
111
  throw err;
87
112
  }
88
113
  }
114
+ };
115
+
116
+ if (!noWait) {
117
+ await settleTurn();
89
118
  } else {
90
- taskPromise.catch((err) => {
91
- if (err.name !== 'AbortError') {
119
+ settleTurn().catch((err) => {
120
+ if (err?.name !== 'AbortError') {
92
121
  console.error('Task execution error:', err);
93
122
  }
94
123
  });
@@ -100,7 +129,8 @@ export async function getInitialRouterState(
100
129
  message: string,
101
130
  chatSettings: Partial<ChatSettings>,
102
131
  overrideAgentId?: string,
103
- overrideSessionId?: string
132
+ overrideSessionId?: string,
133
+ externalRef?: string
104
134
  ): Promise<RouterState> {
105
135
  const agentId = overrideAgentId ?? chatSettings.defaultAgent ?? 'default';
106
136
  const sessionId = overrideSessionId ?? chatSettings.sessions?.[agentId] ?? 'default';
@@ -113,6 +143,7 @@ export async function getInitialRouterState(
113
143
  agentId,
114
144
  sessionId,
115
145
  env: {},
146
+ ...(externalRef ? { externalRef } : {}),
116
147
  };
117
148
  }
118
149
 
@@ -123,7 +154,8 @@ export async function handleUserMessage(
123
154
  cwd: string = process.cwd(),
124
155
  noWait: boolean = false,
125
156
  sessionId?: string,
126
- overrideAgentId?: string
157
+ overrideAgentId?: string,
158
+ externalRef?: string
127
159
  ): Promise<void> {
128
160
  const chatSettings = (await readChatSettings(chatId, cwd)) ?? {};
129
161
 
@@ -137,7 +169,8 @@ export async function handleUserMessage(
137
169
  message,
138
170
  chatSettings,
139
171
  overrideAgentId,
140
- sessionId
172
+ sessionId,
173
+ externalRef
141
174
  );
142
175
 
143
176
  const routers = chatSettings.routers ?? settings?.routers ?? [];
@@ -146,7 +179,15 @@ export async function handleUserMessage(
146
179
 
147
180
  await applyRouterStateUpdates(chatId, cwd, finalState, chatSettings, initialState.agentId);
148
181
 
149
- await executeDirectMessage(chatId, finalState, settings, cwd, noWait, message);
182
+ await executeDirectMessage(
183
+ chatId,
184
+ finalState,
185
+ settings,
186
+ cwd,
187
+ noWait,
188
+ message,
189
+ finalState.subagentId
190
+ );
150
191
  }
151
192
 
152
193
  export async function applyRouterStateUpdates(
@@ -185,12 +226,13 @@ export async function applyRouterStateUpdates(
185
226
  }
186
227
 
187
228
  if (finalState.jobs.add?.length) {
188
- const addMap = new Map(finalState.jobs.add.map((job) => [job.id, job]));
189
- for (const job of finalState.jobs.add) {
229
+ const normalized = finalState.jobs.add.map(normalizeJob);
230
+ const addMap = new Map(normalized.map((job) => [job.id, job]));
231
+ for (const job of normalized) {
190
232
  cronManager.scheduleJob(chatId, job);
191
233
  }
192
234
  chatSettings.jobs = chatSettings.jobs.filter((job) => !addMap.has(job.id));
193
- chatSettings.jobs.push(...finalState.jobs.add);
235
+ chatSettings.jobs.push(...normalized);
194
236
  settingsChanged = true;
195
237
  }
196
238
  }
@@ -207,3 +249,25 @@ export async function applyRouterStateUpdates(
207
249
  finalState.agentId = currentAgentId;
208
250
  }
209
251
  }
252
+
253
+ async function stopActiveSubagents(chatId: string, cwd: string): Promise<void> {
254
+ const sessionsToAbort: string[] = [];
255
+ await updateChatSettings(
256
+ chatId,
257
+ (settings) => {
258
+ if (settings.subagents) {
259
+ for (const sub of Object.values(settings.subagents)) {
260
+ if (sub.status === 'active') {
261
+ if (sub.sessionId) sessionsToAbort.push(sub.sessionId);
262
+ sub.status = 'failed';
263
+ }
264
+ }
265
+ }
266
+ return settings;
267
+ },
268
+ cwd
269
+ );
270
+ for (const sessionId of sessionsToAbort) {
271
+ taskScheduler.abortTasks(sessionId);
272
+ }
273
+ }