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
@@ -1,20 +1,21 @@
1
- import { C as writeAgentSessionSettings, D as pathIsInsideDir, O as CronJobSchema, S as updateChatSettings, T as writeChatSettings, _ as readEnvironment, b as resolveAgentWorkDir, c as getClawminiDir, d as getSocketPath, f as getWorkspaceRoot, g as readChatSettings, h as readAgentSessionSettings, k as SettingsSchema, l as getEnvironmentPath, m as listAgents, o as getActiveEnvironmentInfo, s as getAgent, u as getSettingsPath, v as readPolicies, y as readSettings } from "../workspace-BJmJBfKi.mjs";
2
- import { c as createChat, f as getDefaultChatId, h as listChats, n as exportLiteToEnvironment, p as getMessages$1, s as appendMessage$1, u as findLastMessage } from "../lite-CBxOT1y5.mjs";
1
+ import { A as updateChatSettings, C as readSettings, E as resolveAgentWorkDir, F as pathIsInsideDir, I as CronJobSchema, L as SettingsSchema, N as writeChatSettings, O as substituteLayeredEnvDir, S as readPoliciesForPath, b as readChatSettings, c as getAgent, d as getEnvironmentPath, f as getEnvironmentSearchDirs, g as getWorkspaceRoot, h as getSocketPath, j as writeAgentSessionSettings, k as updateAgentOverlay, l as getAgentOverlay, m as getSettingsPath, s as getActiveEnvironmentInfo, u as getClawminiDir, v as listAgents, x as readEnvironment, y as readAgentSessionSettings } from "../workspace-oWmVh5mi.mjs";
2
+ import { A as getMessages$1, D as findLastMessage, M as listChats, T as createChat, a as DAEMON_EVENT_CHAT_STREAM, b as exportLiteToEnvironment, c as daemonEvents, d as emitTyping, i as appendMessage, k as getDefaultChatId, l as emitTurnEnded, m as detectInstall, o as DAEMON_EVENT_MESSAGE_APPENDED, p as sendControlRequest, r as drainPendingReplies, s as DAEMON_EVENT_TYPING, t as isAcceptableVersion, u as emitTurnStarted } from "../supervisor-actions-CiW56eLi.mjs";
3
3
  import fs, { constants } from "node:fs";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { execSync, spawn } from "node:child_process";
6
- import fs$1 from "node:fs/promises";
7
+ import crypto$1, { randomBytes, randomUUID } from "node:crypto";
8
+ import fsPromises from "node:fs/promises";
7
9
  import { z } from "zod";
8
10
  import http from "node:http";
9
11
  import net from "node:net";
12
+ import { on } from "node:events";
10
13
  import { createHTTPHandler } from "@trpc/server/adapters/standalone";
11
14
  import { TRPCError, initTRPC } from "@trpc/server";
12
15
  import schedule from "node-schedule";
13
- import crypto$1, { randomBytes, randomUUID } from "node:crypto";
14
- import fs$2 from "fs/promises";
16
+ import fs$1 from "fs/promises";
15
17
  import path$1 from "path";
16
18
  import { randomInt } from "crypto";
17
- import { EventEmitter, on } from "node:events";
18
19
 
19
20
  //#region src/daemon/api/trpc.ts
20
21
  const t = initTRPC.context().create();
@@ -67,10 +68,10 @@ async function slashCommand(state) {
67
68
  if (!pathIsInsideDir(path.resolve(commandsDir, commandName), commandsDir)) continue;
68
69
  let content;
69
70
  try {
70
- content = await fs$1.readFile(targetPathMd, "utf8");
71
+ content = await fsPromises.readFile(targetPathMd, "utf8");
71
72
  } catch {
72
73
  try {
73
- content = await fs$1.readFile(targetPathTxt, "utf8");
74
+ content = await fsPromises.readFile(targetPathTxt, "utf8");
74
75
  } catch {
75
76
  continue;
76
77
  }
@@ -124,7 +125,9 @@ const PolicyRequestSchema = z.object({
124
125
  createdAt: z.number(),
125
126
  rejectionReason: z.string().optional(),
126
127
  chatId: z.string(),
127
- agentId: z.string()
128
+ agentId: z.string(),
129
+ subagentId: z.string().optional(),
130
+ cwd: z.string().optional()
128
131
  });
129
132
  function isENOENT(err) {
130
133
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
@@ -135,7 +138,7 @@ var RequestStore = class {
135
138
  this.baseDir = path$1.join(getClawminiDir(startDir), "tmp", "requests");
136
139
  }
137
140
  async init() {
138
- await fs$2.mkdir(this.baseDir, { recursive: true });
141
+ await fs$1.mkdir(this.baseDir, { recursive: true });
139
142
  }
140
143
  getFilePath(id) {
141
144
  return path$1.join(this.baseDir, `${id}.json`);
@@ -145,13 +148,40 @@ var RequestStore = class {
145
148
  const normalizedId = normalizePolicyId(request.id);
146
149
  request.id = normalizedId;
147
150
  const filePath = this.getFilePath(normalizedId);
148
- await fs$2.writeFile(filePath, JSON.stringify(request, null, 2), "utf8");
151
+ await fs$1.writeFile(filePath, JSON.stringify(request, null, 2), "utf8");
152
+ }
153
+ async delete(id) {
154
+ const normalizedId = normalizePolicyId(id);
155
+ const filePath = this.getFilePath(normalizedId);
156
+ try {
157
+ await fs$1.unlink(filePath);
158
+ } catch (err) {
159
+ if (!isENOENT(err)) throw err;
160
+ }
161
+ }
162
+ async cleanupCompleted() {
163
+ let removed = 0;
164
+ try {
165
+ const files = await fs$1.readdir(this.baseDir);
166
+ for (const file of files) {
167
+ if (!file.endsWith(".json")) continue;
168
+ const id = path$1.basename(file, ".json");
169
+ const req = await this.load(id);
170
+ if (req && req.state !== "Pending") {
171
+ await this.delete(id);
172
+ removed++;
173
+ }
174
+ }
175
+ } catch (err) {
176
+ if (!isENOENT(err)) throw err;
177
+ }
178
+ return removed;
149
179
  }
150
180
  async load(id) {
151
181
  const normalizedId = normalizePolicyId(id);
152
182
  const filePath = this.getFilePath(normalizedId);
153
183
  try {
154
- const data = await fs$2.readFile(filePath, "utf8");
184
+ const data = await fs$1.readFile(filePath, "utf8");
155
185
  return PolicyRequestSchema.parse(JSON.parse(data));
156
186
  } catch (err) {
157
187
  if (isENOENT(err)) return null;
@@ -164,7 +194,7 @@ var RequestStore = class {
164
194
  await this.init();
165
195
  const requests = [];
166
196
  try {
167
- const files = await fs$2.readdir(this.baseDir);
197
+ const files = await fs$1.readdir(this.baseDir);
168
198
  for (const file of files) {
169
199
  if (!file.endsWith(".json")) continue;
170
200
  const id = path$1.basename(file, ".json");
@@ -190,10 +220,56 @@ function normalizePolicyId(id) {
190
220
  //#endregion
191
221
  //#region src/daemon/policy-utils.ts
192
222
  const MAX_SNAPSHOT_SIZE = 5 * 1024 * 1024;
223
+ const MAX_INLINE_OUTPUT_LENGTH = 500;
224
+ /**
225
+ * Strips the sandbox `baseDir` from `sandboxCwd` and resolves the remainder
226
+ * against `hostTargetDir` (the host dir that mirrors baseDir inside the
227
+ * sandbox). Pure translation — no security validation; callers must validate
228
+ * the result with `assertPathInsideDir`.
229
+ */
230
+ function translateSandboxPath(sandboxCwd, baseDir, hostTargetDir) {
231
+ let relativePath = sandboxCwd;
232
+ if (sandboxCwd.startsWith(baseDir)) relativePath = sandboxCwd.slice(baseDir.length);
233
+ if (relativePath.startsWith("/") || relativePath.startsWith("\\")) relativePath = relativePath.slice(1);
234
+ return path.resolve(hostTargetDir, relativePath);
235
+ }
236
+ /**
237
+ * Throws if `cwd` (after symlink resolution) is not inside `boundaryDir`.
238
+ *
239
+ * Security note (TOCTOU): There is an inherent race between validating the
240
+ * resolved path here and the moment `spawn` uses it as cwd. A symlink created
241
+ * on the host filesystem in that window could redirect execution outside
242
+ * boundaryDir. We accept this because the sandboxed agent cannot modify the
243
+ * host filesystem — only a local user or process with host-level access could
244
+ * exploit the gap, and that is outside our threat model.
245
+ */
246
+ function assertPathInsideDir(cwd, boundaryDir) {
247
+ if (!pathIsInsideDir(tryRealpath(cwd), tryRealpath(boundaryDir), { allowSameDir: true })) throw new Error(`Security Error: Path resolves outside the allowed directory: ${cwd}`);
248
+ }
249
+ function tryRealpath(p) {
250
+ const resolved = path.resolve(p);
251
+ try {
252
+ return fs.realpathSync(resolved);
253
+ } catch (err) {
254
+ if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
255
+ }
256
+ const parent = path.dirname(resolved);
257
+ if (parent === resolved) return resolved;
258
+ return path.join(tryRealpath(parent), path.basename(resolved));
259
+ }
260
+ async function resolveRequestCwd(requestCwd, agentId, workspaceRoot) {
261
+ if (!requestCwd) return workspaceRoot;
262
+ const agentDir = await resolveAgentDir(agentId, workspaceRoot);
263
+ const envInfo = await getActiveEnvironmentInfo(agentDir, workspaceRoot);
264
+ const envConfig = envInfo ? await readEnvironment(envInfo.name, workspaceRoot) : null;
265
+ const hostCwd = envInfo && envConfig?.baseDir ? translateSandboxPath(requestCwd, envConfig.baseDir, envInfo.targetPath) : path.resolve(agentDir, requestCwd);
266
+ assertPathInsideDir(hostCwd, agentDir);
267
+ return hostCwd;
268
+ }
193
269
  async function createSnapshot(requestedPath, agentDir, snapshotDir) {
194
270
  let realAgentDir;
195
271
  try {
196
- realAgentDir = await fs$1.realpath(agentDir);
272
+ realAgentDir = await fsPromises.realpath(agentDir);
197
273
  } catch (err) {
198
274
  throw new Error(`Agent directory not found or cannot be resolved: ${agentDir}`, { cause: err });
199
275
  }
@@ -201,7 +277,7 @@ async function createSnapshot(requestedPath, agentDir, snapshotDir) {
201
277
  if (!pathIsInsideDir(resolvedRequestedPath, realAgentDir, { allowSameDir: true })) throw new Error(`Security Error: Path resolves outside the allowed agent directory: ${resolvedRequestedPath}`);
202
278
  let stat;
203
279
  try {
204
- stat = await fs$1.lstat(resolvedRequestedPath);
280
+ stat = await fsPromises.lstat(resolvedRequestedPath);
205
281
  } catch (err) {
206
282
  throw new Error(`File not found or cannot be accessed: ${requestedPath}`, { cause: err });
207
283
  }
@@ -210,13 +286,13 @@ async function createSnapshot(requestedPath, agentDir, snapshotDir) {
210
286
  if (stat.size > MAX_SNAPSHOT_SIZE) throw new Error(`File exceeds maximum snapshot size of 5MB: ${requestedPath}`);
211
287
  const ext = path.extname(resolvedRequestedPath);
212
288
  const base = path.basename(resolvedRequestedPath, ext);
213
- await fs$1.mkdir(snapshotDir, { recursive: true });
289
+ await fsPromises.mkdir(snapshotDir, { recursive: true });
214
290
  let snapshotPath;
215
291
  while (true) {
216
292
  const snapshotFileName = `${base}_${randomBytes(8).toString("hex")}${ext}`;
217
293
  snapshotPath = path.join(snapshotDir, snapshotFileName);
218
294
  try {
219
- await fs$1.copyFile(resolvedRequestedPath, snapshotPath, constants.COPYFILE_EXCL);
295
+ await fsPromises.copyFile(resolvedRequestedPath, snapshotPath, constants.COPYFILE_EXCL);
220
296
  break;
221
297
  } catch (err) {
222
298
  if (err instanceof Error && "code" in err && err.code === "EEXIST") continue;
@@ -276,6 +352,27 @@ async function executeRequest(request, policy, cwd) {
276
352
  commandStr: `${policy.command} ${interpolatedArgs.join(" ")}`
277
353
  };
278
354
  }
355
+ /**
356
+ * Saves large stdout/stderr to files in the agent's tmp/ directory and returns
357
+ * placeholder strings pointing to those files. Small outputs are returned as-is.
358
+ */
359
+ async function truncateLargeOutput(stdout, stderr, requestId, agentId) {
360
+ const agentDir = await resolveAgentDir(agentId, getWorkspaceRoot());
361
+ const tmpDir = path.join(agentDir, "tmp");
362
+ if (stdout.length >= MAX_INLINE_OUTPUT_LENGTH || stderr.length >= MAX_INLINE_OUTPUT_LENGTH) await fsPromises.mkdir(tmpDir, { recursive: true });
363
+ if (stdout.length >= MAX_INLINE_OUTPUT_LENGTH) {
364
+ await fsPromises.writeFile(path.join(tmpDir, `stdout-${requestId}.txt`), stdout, "utf-8");
365
+ stdout = `stdout is ${stdout.length} characters, saved to ./tmp/stdout-${requestId}.txt\n`;
366
+ }
367
+ if (stderr.length >= MAX_INLINE_OUTPUT_LENGTH) {
368
+ await fsPromises.writeFile(path.join(tmpDir, `stderr-${requestId}.txt`), stderr, "utf-8");
369
+ stderr = `stderr is ${stderr.length} characters, saved to ./tmp/stderr-${requestId}.txt\n`;
370
+ }
371
+ return {
372
+ stdout,
373
+ stderr
374
+ };
375
+ }
279
376
  async function generateRequestPreview(request) {
280
377
  let previewContent = `Sandbox Policy Request: ${request.commandName}\n`;
281
378
  previewContent += `ID: ${request.id}\n`;
@@ -283,7 +380,7 @@ async function generateRequestPreview(request) {
283
380
  for (const [name, snapPath] of Object.entries(request.fileMappings)) {
284
381
  previewContent += `File [${name}]:\n`;
285
382
  try {
286
- let content = await fs$1.readFile(snapPath, "utf8");
383
+ let content = await fsPromises.readFile(snapPath, "utf8");
287
384
  if (content.length > 500) content = content.substring(0, 500) + "\n... (truncated)\n";
288
385
  previewContent += content;
289
386
  } catch (e) {
@@ -294,30 +391,13 @@ async function generateRequestPreview(request) {
294
391
  return previewContent;
295
392
  }
296
393
 
297
- //#endregion
298
- //#region src/daemon/events.ts
299
- const daemonEvents = new EventEmitter();
300
- const DAEMON_EVENT_MESSAGE_APPENDED = "message-appended";
301
- const DAEMON_EVENT_TYPING = "typing";
302
- function emitMessageAppended(chatId, message) {
303
- daemonEvents.emit(DAEMON_EVENT_MESSAGE_APPENDED, {
304
- chatId,
305
- message
306
- });
307
- }
308
- function emitTyping(chatId) {
309
- daemonEvents.emit(DAEMON_EVENT_TYPING, { chatId });
310
- }
311
-
312
- //#endregion
313
- //#region src/daemon/chats.ts
314
- async function appendMessage(id, message, startDir = process.cwd()) {
315
- await appendMessage$1(id, message, startDir);
316
- emitMessageAppended(id, message);
317
- }
318
-
319
394
  //#endregion
320
395
  //#region src/daemon/routers/slash-policies.ts
396
+ async function resolveTargetSessionId(chatId, req) {
397
+ const chatSettings = await readChatSettings(chatId);
398
+ if (req.subagentId) return chatSettings?.subagents?.[req.subagentId]?.sessionId ?? "default";
399
+ return chatSettings?.sessions?.[req.agentId] ?? "default";
400
+ }
321
401
  async function loadAndValidateRequest(id, state) {
322
402
  const store = new RequestStore(getWorkspaceRoot());
323
403
  const req = await store.load(id);
@@ -360,37 +440,42 @@ async function slashPolicies(state) {
360
440
  const { req, store, error } = await loadAndValidateRequest(id, state);
361
441
  if (error) return error;
362
442
  if (!req || !store) return state;
363
- const policy = (await readPolicies())?.policies?.[req.commandName];
443
+ const workspaceRoot = getWorkspaceRoot();
444
+ const policy = (await readPoliciesForPath(await resolveAgentDir(req.agentId, workspaceRoot), workspaceRoot))?.policies?.[req.commandName];
364
445
  if (!policy) return {
365
446
  ...state,
366
447
  message: "",
367
448
  reply: `Policy not found: ${req.commandName}`
368
449
  };
369
- req.state = "Approved";
370
- const { stdout, stderr, exitCode } = await executeRequest(req, policy, getWorkspaceRoot());
371
- req.executionResult = {
372
- stdout,
373
- stderr,
374
- exitCode
375
- };
376
- await store.save(req);
450
+ const result = await executeRequest(req, policy, await resolveRequestCwd(req.cwd, state.agentId, workspaceRoot));
451
+ const { exitCode } = result;
452
+ const { stdout, stderr } = await truncateLargeOutput(result.stdout, result.stderr, req.id, state.agentId);
453
+ await store.delete(req.id);
377
454
  const agentMessage = `Request ${id} approved.\n\n${wrapInHtml("stdout", stdout)}\n\n${wrapInHtml("stderr", stderr)}\n\nExit Code: ${exitCode}`;
378
- const logMsg = {
455
+ const targetSessionId = await resolveTargetSessionId(state.chatId, req);
456
+ const userNotificationMsg = {
379
457
  id: randomUUID(),
380
458
  messageId: state.messageId,
381
459
  role: "system",
382
460
  event: "policy_approved",
383
- displayRole: "user",
384
- content: agentMessage,
461
+ displayRole: "agent",
462
+ content: `Request ${id} (\`${req.commandName}\`) approved.`,
385
463
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
386
- ...req.subagentId ? { subagentId: req.subagentId } : {}
464
+ sessionId: state.sessionId
387
465
  };
388
- await appendMessage(state.chatId, logMsg);
466
+ await appendMessage(state.chatId, userNotificationMsg);
467
+ await executeDirectMessage(state.chatId, {
468
+ messageId: randomUUID(),
469
+ message: agentMessage,
470
+ chatId: state.chatId,
471
+ agentId: req.agentId,
472
+ sessionId: targetSessionId,
473
+ ...req.subagentId ? { subagentId: req.subagentId } : {},
474
+ env: state.env || {}
475
+ }, void 0, getWorkspaceRoot(), true, agentMessage, req.subagentId, "policy_approved", "user");
389
476
  return {
390
477
  ...state,
391
- message: agentMessage,
392
- reply: `Approved request, running ${req.commandName}`,
393
- ...req.subagentId ? { subagentId: req.subagentId } : {}
478
+ message: ""
394
479
  };
395
480
  }
396
481
  const rejectMatch = message.match(/^\/reject\s+([^\s]+)(?:\s+(.*))?/);
@@ -401,36 +486,32 @@ async function slashPolicies(state) {
401
486
  const { req, store, error } = await loadAndValidateRequest(id, state);
402
487
  if (error) return error;
403
488
  if (!req || !store) return state;
404
- req.state = "Rejected";
405
- req.rejectionReason = reason;
406
- await store.save(req);
489
+ await store.delete(req.id);
407
490
  const agentMessage = `Request ${id} rejected. Reason: ${reason}`;
408
- const logMsg = {
409
- id: randomUUID(),
410
- messageId: state.messageId,
411
- role: "system",
412
- event: "policy_rejected",
413
- displayRole: "user",
414
- content: agentMessage,
415
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
416
- ...req.subagentId ? { subagentId: req.subagentId } : {}
417
- };
491
+ const targetSessionId = await resolveTargetSessionId(state.chatId, req);
418
492
  const userNotificationMsg = {
419
493
  id: randomUUID(),
420
494
  messageId: state.messageId,
421
495
  role: "system",
422
496
  event: "policy_rejected",
423
497
  displayRole: "agent",
424
- content: agentMessage,
498
+ content: `Request ${id} (\`${req.commandName}\`) rejected. Reason: ${reason}`,
425
499
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
426
- ...req.subagentId ? { subagentId: req.subagentId } : {}
500
+ sessionId: state.sessionId
427
501
  };
428
- await appendMessage(state.chatId, logMsg);
429
502
  await appendMessage(state.chatId, userNotificationMsg);
503
+ await executeDirectMessage(state.chatId, {
504
+ messageId: randomUUID(),
505
+ message: agentMessage,
506
+ chatId: state.chatId,
507
+ agentId: req.agentId,
508
+ sessionId: targetSessionId,
509
+ ...req.subagentId ? { subagentId: req.subagentId } : {},
510
+ env: state.env || {}
511
+ }, void 0, getWorkspaceRoot(), true, agentMessage, req.subagentId, "policy_rejected", "user");
430
512
  return {
431
513
  ...state,
432
- message: agentMessage,
433
- ...req.subagentId ? { subagentId: req.subagentId } : {}
514
+ message: ""
434
515
  };
435
516
  }
436
517
  return state;
@@ -440,6 +521,247 @@ function wrapInHtml(tag, text) {
440
521
  return `<${tag}>\n${text.trim()}\n</${tag}>`;
441
522
  }
442
523
 
524
+ //#endregion
525
+ //#region src/daemon/routers/slash-model.ts
526
+ const RESERVED_SHORTHANDS = new Set([
527
+ "help",
528
+ "add",
529
+ "remove",
530
+ "rm"
531
+ ]);
532
+ function stop$3(state, reply) {
533
+ return {
534
+ ...state,
535
+ message: "",
536
+ reply,
537
+ action: "stop"
538
+ };
539
+ }
540
+ function formatHelp() {
541
+ return [
542
+ "Usage:",
543
+ "- /model — List current model and shorthands.",
544
+ "- /model <name> — Set MODEL (resolves shorthand if defined).",
545
+ "- /model add <shorthand> <full-name> — Add or replace a shorthand.",
546
+ "- /model remove <shorthand> — Remove a shorthand (alias: rm).",
547
+ "- /model help — Show this help."
548
+ ].join("\n");
549
+ }
550
+ function formatList(agent) {
551
+ const current = agent?.env?.MODEL ?? "(unset)";
552
+ const shorthands = agent?.modelShorthands ?? {};
553
+ const entries = Object.entries(shorthands);
554
+ const lines = [`Current model: ${current}`];
555
+ if (entries.length === 0) lines.push("No shorthands defined. Add one with /model add <shorthand> <full-name>.");
556
+ else {
557
+ lines.push("Shorthands:");
558
+ for (const [short, full] of entries.sort(([a], [b]) => a.localeCompare(b))) lines.push(`- ${short} -> ${full}`);
559
+ }
560
+ return lines.join("\n");
561
+ }
562
+ function looksLikeShorthand(name) {
563
+ return name.length <= 16 && !/[-./:]/.test(name);
564
+ }
565
+ async function setModel(agentId, fullModel, workspaceRoot) {
566
+ await updateAgentOverlay(agentId, (overlay) => {
567
+ const nextEnv = {
568
+ ...overlay.env ?? {},
569
+ MODEL: fullModel
570
+ };
571
+ return {
572
+ ...overlay,
573
+ env: nextEnv
574
+ };
575
+ }, workspaceRoot);
576
+ }
577
+ async function addShorthand(agentId, shorthand, fullModel, workspaceRoot) {
578
+ await updateAgentOverlay(agentId, (overlay) => {
579
+ const nextShorthands = {
580
+ ...overlay.modelShorthands ?? {},
581
+ [shorthand]: fullModel
582
+ };
583
+ return {
584
+ ...overlay,
585
+ modelShorthands: nextShorthands
586
+ };
587
+ }, workspaceRoot);
588
+ }
589
+ async function removeOverlayShorthand(agentId, shorthand, workspaceRoot) {
590
+ return await updateAgentOverlay(agentId, (overlay) => {
591
+ const overlayShorthands = overlay.modelShorthands ?? {};
592
+ if (!(shorthand in overlayShorthands)) return null;
593
+ const next = { ...overlayShorthands };
594
+ delete next[shorthand];
595
+ const updated = { ...overlay };
596
+ if (Object.keys(next).length === 0) delete updated.modelShorthands;
597
+ else updated.modelShorthands = next;
598
+ return updated;
599
+ }, workspaceRoot);
600
+ }
601
+ async function ensureOverlay(state, agentId, workspaceRoot) {
602
+ if (await getAgentOverlay(agentId, workspaceRoot) !== null) return null;
603
+ return stop$3(state, `Agent '${agentId}' has no settings overlay; cannot configure model.`);
604
+ }
605
+ async function slashModel(state) {
606
+ const message = state.message.trim();
607
+ if (!/^\/model(\s|$)/.test(message)) return state;
608
+ const agentId = state.agentId;
609
+ if (!agentId) return stop$3(state, "/model requires an agent. Set a defaultAgent for this chat.");
610
+ const workspaceRoot = getWorkspaceRoot();
611
+ const rest = message.slice(6).trim();
612
+ if (rest === "") return stop$3(state, formatList(await getAgent(agentId, workspaceRoot)));
613
+ const firstSpace = rest.search(/\s/);
614
+ const subcommand = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
615
+ const remainder = firstSpace === -1 ? "" : rest.slice(firstSpace + 1).trim();
616
+ if (subcommand === "help") return stop$3(state, formatHelp());
617
+ if (subcommand === "add") {
618
+ const addMatch = remainder.match(/^(\S+)\s+(\S+)\s*$/);
619
+ if (!addMatch) return stop$3(state, "Usage: /model add <shorthand> <full-name>");
620
+ const shorthand = addMatch[1];
621
+ const fullModel = addMatch[2];
622
+ if (RESERVED_SHORTHANDS.has(shorthand)) return stop$3(state, `Invalid shorthand: '${shorthand}' is reserved.`);
623
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
624
+ if (guard) return guard;
625
+ await addShorthand(agentId, shorthand, fullModel, workspaceRoot);
626
+ return stop$3(state, `Added shorthand: ${shorthand} -> ${fullModel}`);
627
+ }
628
+ if (subcommand === "remove" || subcommand === "rm") {
629
+ if (!/^\S+$/.test(remainder)) return stop$3(state, "Usage: /model remove <shorthand>");
630
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
631
+ if (guard) return guard;
632
+ if (!await removeOverlayShorthand(agentId, remainder, workspaceRoot)) {
633
+ if ((await getAgent(agentId, workspaceRoot))?.modelShorthands?.[remainder] !== void 0) return stop$3(state, `Shorthand '${remainder}' is defined in the template, not the overlay. Edit the template to remove it.`);
634
+ return stop$3(state, `Shorthand '${remainder}' not found.`);
635
+ }
636
+ const fallback = (await getAgent(agentId, workspaceRoot))?.modelShorthands?.[remainder];
637
+ return stop$3(state, `Removed shorthand: ${remainder}${fallback !== void 0 ? ` (still resolves to '${fallback}' from template)` : ""}.`);
638
+ }
639
+ if (subcommand.startsWith("-")) return stop$3(state, `Unknown option: ${subcommand}\n${formatHelp()}`);
640
+ if (remainder !== "") return stop$3(state, `Unknown subcommand: ${subcommand}\n${formatHelp()}`);
641
+ const guard = await ensureOverlay(state, agentId, workspaceRoot);
642
+ if (guard) return guard;
643
+ const shorthands = (await getAgent(agentId, workspaceRoot))?.modelShorthands ?? {};
644
+ const matched = Object.prototype.hasOwnProperty.call(shorthands, subcommand);
645
+ const fullModel = matched ? shorthands[subcommand] : subcommand;
646
+ await setModel(agentId, fullModel, workspaceRoot);
647
+ if (matched) return stop$3(state, `Set MODEL to ${fullModel} (shorthand '${subcommand}').`);
648
+ if (looksLikeShorthand(subcommand)) return stop$3(state, `Set MODEL to ${fullModel}. (No shorthand matched — was that the literal model name? Run /model add ${subcommand} <full-name> if not.)`);
649
+ return stop$3(state, `Set MODEL to ${fullModel}.`);
650
+ }
651
+
652
+ //#endregion
653
+ //#region src/daemon/routers/slash-restart.ts
654
+ async function slashRestart(state) {
655
+ if (!/^\/restart(\s|$)/.test(state.message)) return state;
656
+ let res;
657
+ try {
658
+ res = await sendControlRequest({
659
+ action: "restart",
660
+ chatId: state.chatId,
661
+ messageId: state.messageId
662
+ });
663
+ } catch (err) {
664
+ return stop$2(state, `Could not reach supervisor: ${err instanceof Error ? err.message : String(err)}.`);
665
+ }
666
+ if (!res.ok) return stop$2(state, `Restart aborted: ${res.error ?? "unknown error"}.`);
667
+ return stop$2(state, "Restarting clawmini...");
668
+ }
669
+ function stop$2(state, reply) {
670
+ return {
671
+ ...state,
672
+ message: "",
673
+ action: "stop",
674
+ reply
675
+ };
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/daemon/routers/slash-shutdown.ts
680
+ async function slashShutdown(state) {
681
+ if (!/^\/shutdown(\s|$)/.test(state.message)) return state;
682
+ let res;
683
+ try {
684
+ res = await sendControlRequest({ action: "shutdown" });
685
+ } catch (err) {
686
+ return stop$1(state, `Could not reach supervisor: ${err instanceof Error ? err.message : String(err)}.`);
687
+ }
688
+ if (!res.ok) return stop$1(state, `Shutdown aborted: ${res.error ?? "unknown error"}.`);
689
+ return stop$1(state, "Shutting down clawmini supervisor...");
690
+ }
691
+ function stop$1(state, reply) {
692
+ return {
693
+ ...state,
694
+ message: "",
695
+ action: "stop",
696
+ reply
697
+ };
698
+ }
699
+
700
+ //#endregion
701
+ //#region src/shared/version.ts
702
+ let cached = null;
703
+ /**
704
+ * Read the version from the clawmini package.json. Walks up from the current
705
+ * module's location until a package.json named "clawmini" is found.
706
+ */
707
+ function getClawminiVersion() {
708
+ if (cached !== null) return cached;
709
+ let dir = path.dirname(fileURLToPath(import.meta.url));
710
+ while (dir !== path.parse(dir).root) {
711
+ const pkgPath = path.join(dir, "package.json");
712
+ if (fs.existsSync(pkgPath)) try {
713
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
714
+ if (pkg.name === "clawmini" && typeof pkg.version === "string") {
715
+ cached = pkg.version;
716
+ return cached;
717
+ }
718
+ } catch {}
719
+ dir = path.dirname(dir);
720
+ }
721
+ cached = "unknown";
722
+ return cached;
723
+ }
724
+
725
+ //#endregion
726
+ //#region src/daemon/routers/slash-upgrade.ts
727
+ async function slashUpgrade(state) {
728
+ const trimmed = state.message.trim();
729
+ if (!/^\/upgrade(\s|$)/.test(trimmed)) return state;
730
+ const info = detectInstall();
731
+ if (!info.isNpmGlobal) return stop(state, `Cannot upgrade: clawmini is not installed via \`npm install -g\` (running from ${info.entryRealPath}). Skipping.`);
732
+ const rest = trimmed.slice(8).trim();
733
+ if (rest === "") return stop(state, [
734
+ `Currently running clawmini v${getClawminiVersion()}.`,
735
+ "/upgrade requires an explicit target:",
736
+ " /upgrade latest — install whatever npm reports as the latest tag",
737
+ " /upgrade <version> — install a specific version (e.g. /upgrade 0.0.7)"
738
+ ].join("\n"));
739
+ if (!/^\S+$/.test(rest)) return stop(state, "Usage: /upgrade <version>");
740
+ const version = rest;
741
+ if (!isAcceptableVersion(version)) return stop(state, `Invalid version: ${version}`);
742
+ let res;
743
+ try {
744
+ res = await sendControlRequest({
745
+ action: "upgrade",
746
+ version,
747
+ chatId: state.chatId,
748
+ messageId: state.messageId
749
+ });
750
+ } catch (err) {
751
+ return stop(state, `Could not reach supervisor: ${err instanceof Error ? err.message : String(err)}.`);
752
+ }
753
+ if (!res.ok) return stop(state, `Upgrade aborted: ${res.error ?? "unknown error"}.`);
754
+ return stop(state, `Upgrading clawmini to ${version}... services will restart shortly.`);
755
+ }
756
+ function stop(state, reply) {
757
+ return {
758
+ ...state,
759
+ message: "",
760
+ action: "stop",
761
+ reply
762
+ };
763
+ }
764
+
443
765
  //#endregion
444
766
  //#region src/daemon/routers/session-timeout.ts
445
767
  /**
@@ -465,6 +787,7 @@ function createSessionTimeoutRouter(config = {}) {
465
787
  const prompt = config.prompt ?? "This chat session has ended. Save any important details from it to your memory. When finished, reply with NO_REPLY_NECESSARY.";
466
788
  return function(state) {
467
789
  if (state.env?.__SESSION_TIMEOUT__ === "true") return state;
790
+ if (state.subagentId) return state;
468
791
  const sessionId = state.sessionId || crypto.randomUUID();
469
792
  const jobId = `__session_timeout__${sessionId}`;
470
793
  const jobs = {
@@ -506,7 +829,11 @@ const USER_ROUTERS = [
506
829
  "@clawmini/slash-command",
507
830
  "@clawmini/slash-stop",
508
831
  "@clawmini/slash-interrupt",
509
- "@clawmini/slash-policies"
832
+ "@clawmini/slash-policies",
833
+ "@clawmini/slash-model",
834
+ "@clawmini/slash-restart",
835
+ "@clawmini/slash-shutdown",
836
+ "@clawmini/slash-upgrade"
510
837
  ];
511
838
  function resolveRouters(userRouters, isUserMessage) {
512
839
  const resolvedGlobals = [];
@@ -563,6 +890,10 @@ async function executeRouterPipeline(initialState, routers) {
563
890
  else if (router === "@clawmini/slash-stop") state = slashStop(state);
564
891
  else if (router === "@clawmini/slash-interrupt") state = slashInterrupt(state);
565
892
  else if (router === "@clawmini/slash-policies") state = await slashPolicies(state);
893
+ else if (router === "@clawmini/slash-model") state = await slashModel(state);
894
+ else if (router === "@clawmini/slash-restart") state = await slashRestart(state);
895
+ else if (router === "@clawmini/slash-shutdown") state = await slashShutdown(state);
896
+ else if (router === "@clawmini/slash-upgrade") state = await slashUpgrade(state);
566
897
  else if (router === "@clawmini/session-timeout") state = createSessionTimeoutRouter(config)(state);
567
898
  else try {
568
899
  state = await executeCustomRouter(router, state);
@@ -631,15 +962,16 @@ async function executeCustomRouter(command, state) {
631
962
 
632
963
  //#endregion
633
964
  //#region src/daemon/agent/agent-context.ts
634
- function formatEnvironmentPrefix(prefix, replacements) {
965
+ function formatEnvironmentPrefix(prefix, searchDirs, replacements) {
966
+ let out = substituteLayeredEnvDir(prefix, searchDirs);
635
967
  const map = {
636
968
  "{WORKSPACE_DIR}": replacements.targetPath,
637
969
  "{AGENT_DIR}": replacements.executionCwd,
638
- "{ENV_DIR}": replacements.envDir,
639
970
  "{HOME_DIR}": process.env.HOME || "",
640
971
  "{ENV_ARGS}": replacements.envArgs
641
972
  };
642
- return prefix.replace(/{(WORKSPACE_DIR|AGENT_DIR|ENV_DIR|HOME_DIR|ENV_ARGS)}/g, (match) => map[match] || match);
973
+ out = out.replace(/{(WORKSPACE_DIR|AGENT_DIR|HOME_DIR|ENV_ARGS)}/g, (match) => map[match] || match);
974
+ return out;
643
975
  }
644
976
  async function sandboxExecutionContext(initialCommand, env, agentSpecificEnvKeys, executionCwd, cwd) {
645
977
  let command = initialCommand;
@@ -647,13 +979,14 @@ async function sandboxExecutionContext(initialCommand, env, agentSpecificEnvKeys
647
979
  if (!activeEnvInfo) return command;
648
980
  const activeEnvName = activeEnvInfo.name;
649
981
  const activeEnv = await readEnvironment(activeEnvName, cwd);
982
+ const searchDirs = await getEnvironmentSearchDirs(activeEnvName, cwd);
650
983
  if (activeEnv?.env) for (const [key, value] of Object.entries(activeEnv.env)) if (value === false) {
651
984
  delete env[key];
652
985
  agentSpecificEnvKeys.delete(key);
653
986
  } else {
654
987
  let interpolatedValue = String(value);
655
988
  interpolatedValue = interpolatedValue.replace(/\{PATH\}/g, process.env.PATH || "");
656
- interpolatedValue = interpolatedValue.replace(/\{ENV_DIR\}/g, getEnvironmentPath(activeEnvName, cwd));
989
+ interpolatedValue = substituteLayeredEnvDir(interpolatedValue, searchDirs);
657
990
  interpolatedValue = interpolatedValue.replace(/\{WORKSPACE_DIR\}/g, activeEnvInfo.targetPath);
658
991
  env[key] = interpolatedValue;
659
992
  agentSpecificEnvKeys.add(key);
@@ -663,10 +996,9 @@ async function sandboxExecutionContext(initialCommand, env, agentSpecificEnvKeys
663
996
  if (activeEnv.envFormat) return activeEnv.envFormat.replace("{key}", key);
664
997
  return key;
665
998
  }).join(" ");
666
- const prefixReplaced = formatEnvironmentPrefix(activeEnv.prefix, {
999
+ const prefixReplaced = formatEnvironmentPrefix(activeEnv.prefix, searchDirs, {
667
1000
  targetPath: activeEnvInfo.targetPath,
668
1001
  executionCwd,
669
- envDir: getEnvironmentPath(activeEnvName, cwd),
670
1002
  envArgs
671
1003
  });
672
1004
  if (prefixReplaced.includes("{COMMAND}")) command = prefixReplaced.replace("{COMMAND}", command);
@@ -859,12 +1191,17 @@ const runCommand = async ({ command, cwd, env, stdin, signal }) => {
859
1191
 
860
1192
  //#endregion
861
1193
  //#region src/daemon/agent/chat-logger.ts
862
- function createChatLogger(chatId, subagentId) {
1194
+ function createChatLogger(chatId, subagentId, sessionId, turnId) {
863
1195
  async function append(msg) {
864
- const finalMsg = subagentId ? {
865
- ...msg,
1196
+ let finalMsg = msg;
1197
+ if (subagentId) finalMsg = {
1198
+ ...finalMsg,
866
1199
  subagentId
867
- } : msg;
1200
+ };
1201
+ if (turnId) finalMsg = {
1202
+ ...finalMsg,
1203
+ turnId
1204
+ };
868
1205
  await appendMessage(chatId, finalMsg);
869
1206
  return finalMsg;
870
1207
  }
@@ -885,13 +1222,15 @@ function createChatLogger(chatId, subagentId) {
885
1222
  id: crypto.randomUUID(),
886
1223
  role: "user",
887
1224
  content: msg,
888
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1225
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1226
+ sessionId
889
1227
  }),
890
1228
  logCommandResult: async ({ messageId, content, command, cwd, result }) => append({
891
1229
  id: crypto.randomUUID(),
892
1230
  role: "command",
893
1231
  content,
894
1232
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1233
+ sessionId,
895
1234
  messageId,
896
1235
  command,
897
1236
  cwd,
@@ -904,6 +1243,7 @@ function createChatLogger(chatId, subagentId) {
904
1243
  role: "command",
905
1244
  content,
906
1245
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1246
+ sessionId,
907
1247
  messageId: crypto.randomUUID(),
908
1248
  stderr: "",
909
1249
  command: "",
@@ -916,6 +1256,7 @@ function createChatLogger(chatId, subagentId) {
916
1256
  role: "system",
917
1257
  content,
918
1258
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1259
+ sessionId,
919
1260
  messageId,
920
1261
  event: "router",
921
1262
  displayRole: "agent"
@@ -925,6 +1266,7 @@ function createChatLogger(chatId, subagentId) {
925
1266
  role: "command",
926
1267
  content,
927
1268
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1269
+ sessionId,
928
1270
  messageId,
929
1271
  command: "retry-delay",
930
1272
  stderr: "",
@@ -932,16 +1274,18 @@ function createChatLogger(chatId, subagentId) {
932
1274
  cwd,
933
1275
  exitCode: 0
934
1276
  }),
935
- logSystemMessage: async ({ content, event, messageId, displayRole }) => {
1277
+ logSystemMessage: async ({ content, event, messageId, displayRole, jobId }) => {
936
1278
  const msg = {
937
1279
  id: crypto.randomUUID(),
938
1280
  role: "system",
939
1281
  content,
940
1282
  event,
941
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1283
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1284
+ sessionId
942
1285
  };
943
1286
  if (messageId !== void 0) msg.messageId = messageId;
944
1287
  if (displayRole !== void 0) msg.displayRole = displayRole;
1288
+ if (jobId !== void 0) msg.jobId = jobId;
945
1289
  return append(msg);
946
1290
  },
947
1291
  logSubagentStatus: async ({ subagentId: targetSubagentId, status }) => {
@@ -951,7 +1295,8 @@ function createChatLogger(chatId, subagentId) {
951
1295
  content: `Subagent ${status}`,
952
1296
  subagentId: targetSubagentId,
953
1297
  status,
954
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1298
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1299
+ sessionId
955
1300
  });
956
1301
  },
957
1302
  logAgentReply: async ({ content, files }) => {
@@ -959,7 +1304,8 @@ function createChatLogger(chatId, subagentId) {
959
1304
  id: crypto.randomUUID(),
960
1305
  role: "agent",
961
1306
  content,
962
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1307
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1308
+ sessionId
963
1309
  };
964
1310
  if (files !== void 0) msg.files = files;
965
1311
  return append(msg);
@@ -972,7 +1318,8 @@ function createChatLogger(chatId, subagentId) {
972
1318
  messageId,
973
1319
  name,
974
1320
  payload,
975
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1321
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1322
+ sessionId
976
1323
  });
977
1324
  },
978
1325
  logPolicyRequestMessage: async ({ content, messageId, requestId, commandName, args, status }) => {
@@ -985,7 +1332,8 @@ function createChatLogger(chatId, subagentId) {
985
1332
  commandName,
986
1333
  args,
987
1334
  status,
988
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1335
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1336
+ sessionId
989
1337
  });
990
1338
  }
991
1339
  };
@@ -1247,6 +1595,7 @@ var AgentSession = class {
1247
1595
  sessionId;
1248
1596
  chatId;
1249
1597
  subagentId;
1598
+ turnId;
1250
1599
  settings;
1251
1600
  workspaceRoot;
1252
1601
  globalSettings;
@@ -1256,10 +1605,11 @@ var AgentSession = class {
1256
1605
  this.sessionId = config.sessionId;
1257
1606
  this.chatId = config.chatId;
1258
1607
  this.subagentId = config.subagentId;
1608
+ this.turnId = config.turnId;
1259
1609
  this.settings = config.settings;
1260
1610
  this.workspaceRoot = config.workspaceRoot;
1261
1611
  this.globalSettings = config.globalSettings;
1262
- this.logger = config.logger ?? createChatLogger(this.chatId, this.subagentId);
1612
+ this.logger = config.logger ?? createChatLogger(this.chatId, this.subagentId, this.sessionId, this.turnId);
1263
1613
  }
1264
1614
  async buildExecutionContext(messageContent, routerEnv, fallback) {
1265
1615
  const currentAgent = {
@@ -1303,6 +1653,7 @@ var AgentSession = class {
1303
1653
  agentId: this.agentId,
1304
1654
  sessionId: this.sessionId,
1305
1655
  ...this.subagentId ? { subagentId: this.subagentId } : {},
1656
+ ...this.turnId ? { turnId: this.turnId } : {},
1306
1657
  timestamp: Date.now()
1307
1658
  });
1308
1659
  if (currentAgent.apiTokenEnvVar) {
@@ -1372,6 +1723,7 @@ async function createAgentSession(options) {
1372
1723
  sessionId: options.sessionId,
1373
1724
  chatId: options.chatId,
1374
1725
  ...options.subagentId ? { subagentId: options.subagentId } : {},
1726
+ ...options.turnId ? { turnId: options.turnId } : {},
1375
1727
  settings: mergedAgent,
1376
1728
  workspaceRoot,
1377
1729
  globalSettings: settings,
@@ -1398,16 +1750,89 @@ async function resolveMergedAgent(agentId, settings, cwd) {
1398
1750
  return mergedAgent;
1399
1751
  }
1400
1752
 
1753
+ //#endregion
1754
+ //#region src/daemon/agent/turn-registry.ts
1755
+ const turns = /* @__PURE__ */ new Map();
1756
+ /**
1757
+ * Watchdog for turns whose subagent count never drains. Pathological cases
1758
+ * (crashed subagent, bug leaving the counter > 0) would otherwise pin the
1759
+ * turn's activity log open indefinitely. Default is a guess — instrument
1760
+ * before tuning.
1761
+ */
1762
+ const DEFAULT_TURN_MAX_DURATION_MS = 1800 * 1e3;
1763
+ function registerTurn(chatId, turnId, maxDurationMs = DEFAULT_TURN_MAX_DURATION_MS) {
1764
+ if (turns.has(turnId)) return;
1765
+ const state = {
1766
+ chatId,
1767
+ outstanding: 0,
1768
+ parentExited: false,
1769
+ outcome: "ok",
1770
+ timeoutHandle: setTimeout(() => {
1771
+ const s = turns.get(turnId);
1772
+ if (!s) return;
1773
+ console.warn(`Turn ${turnId} force-ended after ${maxDurationMs}ms (outstanding=${s.outstanding}).`);
1774
+ turns.delete(turnId);
1775
+ emitTurnEnded({
1776
+ chatId: s.chatId,
1777
+ turnId,
1778
+ outcome: "error"
1779
+ });
1780
+ }, maxDurationMs)
1781
+ };
1782
+ state.timeoutHandle.unref();
1783
+ turns.set(turnId, state);
1784
+ }
1785
+ function incrementSubagent(turnId) {
1786
+ if (!turnId) return;
1787
+ const state = turns.get(turnId);
1788
+ if (!state) return;
1789
+ state.outstanding++;
1790
+ }
1791
+ function decrementSubagent(turnId) {
1792
+ if (!turnId) return;
1793
+ const state = turns.get(turnId);
1794
+ if (!state) return;
1795
+ state.outstanding = Math.max(0, state.outstanding - 1);
1796
+ maybeFinalize(turnId, state);
1797
+ }
1798
+ /**
1799
+ * Called once, when the parent agent's initial `handleMessage` promise
1800
+ * settles. Records the outcome; the turn actually ends (emits `turnEnded`)
1801
+ * only once the outstanding subagent count also reaches zero.
1802
+ */
1803
+ function markParentExited(turnId, outcome) {
1804
+ const state = turns.get(turnId);
1805
+ if (!state) return;
1806
+ if (state.parentExited) return;
1807
+ state.parentExited = true;
1808
+ state.outcome = outcome;
1809
+ maybeFinalize(turnId, state);
1810
+ }
1811
+ function maybeFinalize(turnId, state) {
1812
+ if (!state.parentExited) return;
1813
+ if (state.outstanding > 0) return;
1814
+ clearTimeout(state.timeoutHandle);
1815
+ turns.delete(turnId);
1816
+ emitTurnEnded({
1817
+ chatId: state.chatId,
1818
+ turnId,
1819
+ outcome: state.outcome
1820
+ });
1821
+ }
1822
+
1401
1823
  //#endregion
1402
1824
  //#region src/daemon/message.ts
1403
- async function executeDirectMessage(chatId, state, settings, cwd, noWait = false, userMessageContent, subagentId, systemEvent, displayRole) {
1404
- const logger = createChatLogger(chatId, subagentId);
1825
+ async function executeDirectMessage(chatId, state, settings, cwd, noWait = false, userMessageContent, subagentId, systemEvent, displayRole, parentTurnId) {
1826
+ const turnId = parentTurnId ?? randomUUID();
1827
+ const emitLifecycle = !parentTurnId;
1828
+ const logger = createChatLogger(chatId, subagentId, state.sessionId, turnId);
1405
1829
  let msgId;
1406
1830
  if (systemEvent) msgId = (await logger.logSystemMessage({
1407
1831
  content: userMessageContent ?? state.message,
1408
1832
  event: systemEvent,
1409
1833
  messageId: state.messageId,
1410
- ...displayRole ? { displayRole } : {}
1834
+ ...displayRole ? { displayRole } : {},
1835
+ ...state.jobId ? { jobId: state.jobId } : {}
1411
1836
  })).id;
1412
1837
  else msgId = (await logger.logUserMessage(userMessageContent ?? state.message)).id;
1413
1838
  if (state.reply) await logger.logAutomaticReply({
@@ -1422,29 +1847,46 @@ async function executeDirectMessage(chatId, state, settings, cwd, noWait = false
1422
1847
  ...subagentId ? { subagentId } : {},
1423
1848
  cwd,
1424
1849
  settings,
1425
- logger
1850
+ logger,
1851
+ turnId
1426
1852
  });
1427
1853
  let finalMessage = {
1428
1854
  id: state.messageId,
1429
1855
  content: state.message,
1430
- env: state.env ?? {}
1856
+ env: state.env ?? {},
1857
+ turnId
1431
1858
  };
1432
1859
  if (state.action === "stop") {
1433
1860
  agentSession.stop();
1861
+ if (!subagentId) await stopActiveSubagents(chatId, cwd);
1434
1862
  return;
1435
1863
  }
1436
1864
  if (state.action === "interrupt") finalMessage = agentSession.interrupt(finalMessage);
1437
- const taskPromise = agentSession.handleMessage(finalMessage);
1438
- if (!noWait) try {
1439
- await taskPromise;
1440
- } catch (err) {
1441
- if (!(err instanceof Error && err.name === "AbortError")) throw err;
1865
+ if (emitLifecycle) {
1866
+ registerTurn(chatId, turnId);
1867
+ emitTurnStarted({
1868
+ chatId,
1869
+ turnId,
1870
+ rootMessageId: msgId,
1871
+ ...state.externalRef ? { externalRef: state.externalRef } : {}
1872
+ });
1442
1873
  }
1443
- else taskPromise.catch((err) => {
1444
- if (err.name !== "AbortError") console.error("Task execution error:", err);
1874
+ const taskPromise = agentSession.handleMessage(finalMessage);
1875
+ const settleTurn = async () => {
1876
+ try {
1877
+ await taskPromise;
1878
+ if (emitLifecycle) markParentExited(turnId, "ok");
1879
+ } catch (err) {
1880
+ if (emitLifecycle) markParentExited(turnId, "error");
1881
+ if (!(err instanceof Error && err.name === "AbortError")) throw err;
1882
+ }
1883
+ };
1884
+ if (!noWait) await settleTurn();
1885
+ else settleTurn().catch((err) => {
1886
+ if (err?.name !== "AbortError") console.error("Task execution error:", err);
1445
1887
  });
1446
1888
  }
1447
- async function getInitialRouterState(chatId, message, chatSettings, overrideAgentId, overrideSessionId) {
1889
+ async function getInitialRouterState(chatId, message, chatSettings, overrideAgentId, overrideSessionId, externalRef) {
1448
1890
  const agentId = overrideAgentId ?? chatSettings.defaultAgent ?? "default";
1449
1891
  const sessionId = overrideSessionId ?? chatSettings.sessions?.[agentId] ?? "default";
1450
1892
  return {
@@ -1453,19 +1895,20 @@ async function getInitialRouterState(chatId, message, chatSettings, overrideAgen
1453
1895
  chatId,
1454
1896
  agentId,
1455
1897
  sessionId,
1456
- env: {}
1898
+ env: {},
1899
+ ...externalRef ? { externalRef } : {}
1457
1900
  };
1458
1901
  }
1459
- async function handleUserMessage(chatId, message, settings, cwd = process.cwd(), noWait = false, sessionId, overrideAgentId) {
1902
+ async function handleUserMessage(chatId, message, settings, cwd = process.cwd(), noWait = false, sessionId, overrideAgentId, externalRef) {
1460
1903
  const chatSettings = await readChatSettings(chatId, cwd) ?? {};
1461
1904
  if (overrideAgentId && chatSettings.defaultAgent !== overrideAgentId) {
1462
1905
  chatSettings.defaultAgent = overrideAgentId;
1463
1906
  await writeChatSettings(chatId, chatSettings, cwd);
1464
1907
  }
1465
- const initialState = await getInitialRouterState(chatId, message, chatSettings, overrideAgentId, sessionId);
1908
+ const initialState = await getInitialRouterState(chatId, message, chatSettings, overrideAgentId, sessionId, externalRef);
1466
1909
  const finalState = await executeRouterPipeline(initialState, resolveRouters(chatSettings.routers ?? settings?.routers ?? [], true));
1467
1910
  await applyRouterStateUpdates(chatId, cwd, finalState, chatSettings, initialState.agentId);
1468
- await executeDirectMessage(chatId, finalState, settings, cwd, noWait, message);
1911
+ await executeDirectMessage(chatId, finalState, settings, cwd, noWait, message, finalState.subagentId);
1469
1912
  }
1470
1913
  async function applyRouterStateUpdates(chatId, cwd, finalState, chatSettings, initialAgent) {
1471
1914
  const finalAgentId = finalState.agentId;
@@ -1490,10 +1933,11 @@ async function applyRouterStateUpdates(chatId, cwd, finalState, chatSettings, in
1490
1933
  settingsChanged = true;
1491
1934
  }
1492
1935
  if (finalState.jobs.add?.length) {
1493
- const addMap = new Map(finalState.jobs.add.map((job) => [job.id, job]));
1494
- for (const job of finalState.jobs.add) cronManager.scheduleJob(chatId, job);
1936
+ const normalized = finalState.jobs.add.map(normalizeJob);
1937
+ const addMap = new Map(normalized.map((job) => [job.id, job]));
1938
+ for (const job of normalized) cronManager.scheduleJob(chatId, job);
1495
1939
  chatSettings.jobs = chatSettings.jobs.filter((job) => !addMap.has(job.id));
1496
- chatSettings.jobs.push(...finalState.jobs.add);
1940
+ chatSettings.jobs.push(...normalized);
1497
1941
  settingsChanged = true;
1498
1942
  }
1499
1943
  }
@@ -1501,9 +1945,45 @@ async function applyRouterStateUpdates(chatId, cwd, finalState, chatSettings, in
1501
1945
  if (finalState.sessionId === void 0) finalState.sessionId = finalSessionId;
1502
1946
  if (finalState.agentId === void 0) finalState.agentId = currentAgentId;
1503
1947
  }
1948
+ async function stopActiveSubagents(chatId, cwd) {
1949
+ const sessionsToAbort = [];
1950
+ await updateChatSettings(chatId, (settings) => {
1951
+ if (settings.subagents) {
1952
+ for (const sub of Object.values(settings.subagents)) if (sub.status === "active") {
1953
+ if (sub.sessionId) sessionsToAbort.push(sub.sessionId);
1954
+ sub.status = "failed";
1955
+ }
1956
+ }
1957
+ return settings;
1958
+ }, cwd);
1959
+ for (const sessionId of sessionsToAbort) taskScheduler.abortTasks(sessionId);
1960
+ }
1504
1961
 
1505
1962
  //#endregion
1506
1963
  //#region src/daemon/cron.ts
1964
+ function normalizeJob(job) {
1965
+ if (!("at" in job.schedule)) return job;
1966
+ const atStr = job.schedule.at;
1967
+ const match = atStr.match(/^(\d+)\s*(m|min|minutes?|h|hours?|d|days?|s|sec|seconds?)$/i);
1968
+ let absolute;
1969
+ if (match) {
1970
+ const val = parseInt(match[1], 10);
1971
+ const unit = match[2].toLowerCase();
1972
+ let ms = 0;
1973
+ if (unit.startsWith("s")) ms = val * 1e3;
1974
+ else if (unit.startsWith("m")) ms = val * 60 * 1e3;
1975
+ else if (unit.startsWith("h")) ms = val * 60 * 60 * 1e3;
1976
+ else if (unit.startsWith("d")) ms = val * 24 * 60 * 60 * 1e3;
1977
+ absolute = new Date(Date.now() + ms);
1978
+ } else {
1979
+ absolute = new Date(atStr);
1980
+ if (isNaN(absolute.getTime())) throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
1981
+ }
1982
+ return {
1983
+ ...job,
1984
+ schedule: { at: absolute.toISOString() }
1985
+ };
1986
+ }
1507
1987
  var CronManager = class {
1508
1988
  jobs = /* @__PURE__ */ new Map();
1509
1989
  getJobKey(chatId, jobId) {
@@ -1538,19 +2018,11 @@ var CronManager = class {
1538
2018
  } else rule = everyStr;
1539
2019
  } else if ("at" in job.schedule) {
1540
2020
  const atStr = job.schedule.at;
1541
- const match = atStr.match(/^(\d+)\s*(m|min|minutes?|h|hours?|d|days?|s|sec|seconds?)$/i);
1542
- if (match) {
1543
- const val = parseInt(match[1], 10);
1544
- const unit = match[2].toLowerCase();
1545
- let ms = 0;
1546
- if (unit.startsWith("s")) ms = val * 1e3;
1547
- else if (unit.startsWith("m")) ms = val * 60 * 1e3;
1548
- else if (unit.startsWith("h")) ms = val * 60 * 60 * 1e3;
1549
- else if (unit.startsWith("d")) ms = val * 24 * 60 * 60 * 1e3;
1550
- rule = new Date(Date.now() + ms);
1551
- } else {
1552
- rule = new Date(atStr);
1553
- if (isNaN(rule.getTime())) throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
2021
+ rule = new Date(atStr);
2022
+ if (isNaN(rule.getTime())) throw new Error(`Invalid date format for 'at' schedule: ${atStr}`);
2023
+ if (rule.getTime() <= Date.now()) {
2024
+ console.warn(`One-off job ${job.id} for chat ${chatId} is overdue (target ${atStr}); firing immediately.`);
2025
+ rule = new Date(Date.now() + 100);
1554
2026
  }
1555
2027
  isOneOff = true;
1556
2028
  } else {
@@ -1579,7 +2051,7 @@ var CronManager = class {
1579
2051
  const settingsPath = getSettingsPath();
1580
2052
  let globalSettings;
1581
2053
  try {
1582
- const settingsStr = await fs$1.readFile(settingsPath, "utf8");
2054
+ const settingsStr = await fsPromises.readFile(settingsPath, "utf8");
1583
2055
  globalSettings = JSON.parse(settingsStr);
1584
2056
  } catch {
1585
2057
  globalSettings = void 0;
@@ -1598,6 +2070,7 @@ var CronManager = class {
1598
2070
  if (job.nextSessionId !== void 0 && !isOutdatedSession) routerState.nextSessionId = job.nextSessionId;
1599
2071
  if (job.action !== void 0) routerState.action = job.action;
1600
2072
  if (job.jobs !== void 0) routerState.jobs = job.jobs;
2073
+ routerState.jobId = job.id;
1601
2074
  const resolvedRouters = resolveRouters(chatSettings.routers ?? globalSettings?.routers ?? [], false);
1602
2075
  const initialState = { ...routerState };
1603
2076
  routerState = await executeRouterPipeline(routerState, resolvedRouters);
@@ -1624,7 +2097,7 @@ async function getUniquePath(p) {
1624
2097
  let currentPath = p;
1625
2098
  let counter = 1;
1626
2099
  while (true) try {
1627
- await fs$1.stat(currentPath);
2100
+ await fsPromises.stat(currentPath);
1628
2101
  const ext = path.extname(p);
1629
2102
  const base = path.basename(p, ext);
1630
2103
  currentPath = path.join(path.dirname(p), `${base}-${counter}${ext}`);
@@ -1667,7 +2140,7 @@ async function validateAttachments(files) {
1667
2140
  message: "File must be inside the temporary directory."
1668
2141
  });
1669
2142
  try {
1670
- await fs$1.access(absoluteFile);
2143
+ await fsPromises.access(absoluteFile);
1671
2144
  } catch {
1672
2145
  throw new TRPCError({
1673
2146
  code: "BAD_REQUEST",
@@ -1683,7 +2156,7 @@ async function validateLogFile(file, agentDir, workspaceRoot) {
1683
2156
  message: "File must be within the agent workspace."
1684
2157
  });
1685
2158
  try {
1686
- await fs$1.access(resolvedPath);
2159
+ await fsPromises.access(resolvedPath);
1687
2160
  } catch {
1688
2161
  throw new TRPCError({
1689
2162
  code: "BAD_REQUEST",
@@ -1696,14 +2169,15 @@ async function listCronJobsShared(chatId) {
1696
2169
  return (await readChatSettings(chatId))?.jobs ?? [];
1697
2170
  }
1698
2171
  async function addCronJobShared(chatId, job) {
2172
+ const normalized = normalizeJob(job);
1699
2173
  const settings = await readChatSettings(chatId) || {};
1700
2174
  const cronJobs = settings.jobs ?? [];
1701
- const existingIndex = cronJobs.findIndex((j) => j.id === job.id);
1702
- if (existingIndex >= 0) cronJobs[existingIndex] = job;
1703
- else cronJobs.push(job);
2175
+ const existingIndex = cronJobs.findIndex((j) => j.id === normalized.id);
2176
+ if (existingIndex >= 0) cronJobs[existingIndex] = normalized;
2177
+ else cronJobs.push(normalized);
1704
2178
  settings.jobs = cronJobs;
1705
2179
  await writeChatSettings(chatId, settings);
1706
- cronManager.scheduleJob(chatId, job);
2180
+ cronManager.scheduleJob(chatId, normalized);
1707
2181
  return { success: true };
1708
2182
  }
1709
2183
  async function deleteCronJobShared(chatId, id) {
@@ -1740,7 +2214,8 @@ const sendMessage = apiProcedure.input(z.object({
1740
2214
  agentId: z.string().optional(),
1741
2215
  noWait: z.boolean().optional(),
1742
2216
  files: z.array(z.string()).optional(),
1743
- adapter: z.string().optional()
2217
+ adapter: z.string().optional(),
2218
+ externalRef: z.string().optional()
1744
2219
  })
1745
2220
  })).mutation(async ({ input }) => {
1746
2221
  let message = input.data.message;
@@ -1751,7 +2226,7 @@ const sendMessage = apiProcedure.input(z.object({
1751
2226
  const settingsPath = getSettingsPath();
1752
2227
  let settings;
1753
2228
  try {
1754
- const settingsStr = await fs$1.readFile(settingsPath, "utf8");
2229
+ const settingsStr = await fsPromises.readFile(settingsPath, "utf8");
1755
2230
  settings = JSON.parse(settingsStr);
1756
2231
  } catch (err) {
1757
2232
  throw new Error(`Failed to read settings from ${settingsPath}: ${err}`, { cause: err });
@@ -1769,23 +2244,23 @@ const sendMessage = apiProcedure.input(z.object({
1769
2244
  message: "Target directory must be within the workspace."
1770
2245
  });
1771
2246
  await validateAttachments(files);
1772
- await fs$1.mkdir(targetDir, { recursive: true });
2247
+ await fsPromises.mkdir(targetDir, { recursive: true });
1773
2248
  const finalPaths = [];
1774
2249
  for (const file of files) {
1775
2250
  const fileName = path.basename(file);
1776
2251
  const targetPath = await getUniquePath(path.join(targetDir, fileName));
1777
2252
  try {
1778
- await fs$1.rename(file, targetPath);
2253
+ await fsPromises.rename(file, targetPath);
1779
2254
  } catch {
1780
- await fs$1.copyFile(file, targetPath);
1781
- await fs$1.unlink(file);
2255
+ await fsPromises.copyFile(file, targetPath);
2256
+ await fsPromises.unlink(file);
1782
2257
  }
1783
2258
  finalPaths.push(path.relative(agentDir, targetPath));
1784
2259
  }
1785
2260
  const fileList = `Attached files:\n${finalPaths.map((p) => `- ${p}`).join("\n")}`;
1786
2261
  message = message ? `${message}\n\n${fileList}` : fileList;
1787
2262
  }
1788
- await handleUserMessage(chatId, message, settings, void 0, noWait, sessionId, agentId);
2263
+ await handleUserMessage(chatId, message, settings, void 0, noWait, sessionId, agentId, input.data.externalRef);
1789
2264
  return { success: true };
1790
2265
  });
1791
2266
  const getMessages = apiProcedure.input(z.object({
@@ -1794,6 +2269,11 @@ const getMessages = apiProcedure.input(z.object({
1794
2269
  })).query(async ({ input }) => {
1795
2270
  return getMessages$1(input.chatId ?? await getDefaultChatId(), input.limit);
1796
2271
  });
2272
+ /**
2273
+ * Interleaved chat stream: `ChatMessage` appends and turn lifecycle events
2274
+ * arrive on a single subscription in emission order. Consumers that only
2275
+ * care about messages can ignore items with `kind: 'turn'`.
2276
+ */
1797
2277
  const waitForMessages = apiProcedure.input(z.object({
1798
2278
  chatId: z.string().optional(),
1799
2279
  lastMessageId: z.string().optional()
@@ -1802,10 +2282,16 @@ const waitForMessages = apiProcedure.input(z.object({
1802
2282
  if (input.lastMessageId) {
1803
2283
  const messages = await getMessages$1(chatId);
1804
2284
  const lastIndex = messages.findIndex((m) => m.id === input.lastMessageId);
1805
- if (lastIndex !== -1 && lastIndex < messages.length - 1) yield messages.slice(lastIndex + 1);
2285
+ if (lastIndex !== -1 && lastIndex < messages.length - 1) yield messages.slice(lastIndex + 1).map((message) => ({
2286
+ kind: "message",
2287
+ message
2288
+ }));
1806
2289
  }
1807
2290
  try {
1808
- for await (const [event] of on(daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, { signal })) if (event.chatId === chatId) yield [event.message];
2291
+ for await (const [envelope] of on(daemonEvents, DAEMON_EVENT_CHAT_STREAM, { signal })) {
2292
+ const e = envelope;
2293
+ if (e.chatId === chatId) yield [e.item];
2294
+ }
1809
2295
  } catch (err) {
1810
2296
  if (err instanceof Error && err.name === "AbortError") return;
1811
2297
  throw err;
@@ -1890,7 +2376,7 @@ var PolicyRequestService = class {
1890
2376
  this.snapshotDir = snapshotDir;
1891
2377
  this.maxPending = maxPending;
1892
2378
  }
1893
- async createRequest(commandName, args, fileMappings, chatId, agentId, skipSave = false, subagentId) {
2379
+ async createRequest(commandName, args, fileMappings, chatId, agentId, skipSave = false, subagentId, cwd) {
1894
2380
  const allRequests = await this.store.list();
1895
2381
  if (allRequests.filter((r) => r.state === "Pending").length >= this.maxPending) throw new Error(`Maximum number of pending requests (${this.maxPending}) reached.`);
1896
2382
  const snapshotMappings = {};
@@ -1904,6 +2390,7 @@ var PolicyRequestService = class {
1904
2390
  commandName,
1905
2391
  args,
1906
2392
  fileMappings: snapshotMappings,
2393
+ ...cwd ? { cwd } : {},
1907
2394
  state: skipSave ? "Approved" : "Pending",
1908
2395
  createdAt: Date.now(),
1909
2396
  chatId,
@@ -1918,6 +2405,174 @@ var PolicyRequestService = class {
1918
2405
  }
1919
2406
  };
1920
2407
 
2408
+ //#endregion
2409
+ //#region src/daemon/api/agent-policy-endpoints.ts
2410
+ const MAX_POLICY_SCRIPT_BYTES = 1 * 1024 * 1024;
2411
+ const MAX_INLINE_SCRIPT_LENGTH = 4e3;
2412
+ const listPolicies = apiProcedure.query(async ({ ctx }) => {
2413
+ const workspaceRoot = getWorkspaceRoot();
2414
+ return { policies: (await readPoliciesForPath(await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot), workspaceRoot))?.policies || {} };
2415
+ });
2416
+ const readPolicyScript = apiProcedure.input(z.object({ commandName: z.string() })).query(async ({ input, ctx }) => {
2417
+ const workspaceRoot = getWorkspaceRoot();
2418
+ const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
2419
+ const policy = (await readPoliciesForPath(agentDir, workspaceRoot))?.policies?.[input.commandName];
2420
+ if (!policy) throw new TRPCError({
2421
+ code: "NOT_FOUND",
2422
+ message: `Policy not found: ${input.commandName}`
2423
+ });
2424
+ const scriptsDir = path.join(getClawminiDir(), "policy-scripts");
2425
+ const resolvedCommand = path.resolve(policy.command);
2426
+ if (!pathIsInsideDir(resolvedCommand, scriptsDir, { allowSameDir: false })) throw new TRPCError({
2427
+ code: "BAD_REQUEST",
2428
+ message: `Policy '${input.commandName}' does not point at a script in policy-scripts/.`
2429
+ });
2430
+ let realCommand;
2431
+ let realScriptsDir;
2432
+ try {
2433
+ realCommand = await fsPromises.realpath(resolvedCommand);
2434
+ } catch (err) {
2435
+ throw new TRPCError({
2436
+ code: "NOT_FOUND",
2437
+ message: `Script file not found for policy '${input.commandName}': ${err instanceof Error ? err.message : String(err)}`
2438
+ });
2439
+ }
2440
+ try {
2441
+ realScriptsDir = await fsPromises.realpath(scriptsDir);
2442
+ } catch {
2443
+ throw new TRPCError({
2444
+ code: "BAD_REQUEST",
2445
+ message: `Policy '${input.commandName}' does not point at a script in policy-scripts/.`
2446
+ });
2447
+ }
2448
+ if (!pathIsInsideDir(realCommand, realScriptsDir, { allowSameDir: false })) throw new TRPCError({
2449
+ code: "BAD_REQUEST",
2450
+ message: `Policy '${input.commandName}' does not point at a script in policy-scripts/.`
2451
+ });
2452
+ let stat;
2453
+ try {
2454
+ stat = await fsPromises.stat(realCommand);
2455
+ } catch (err) {
2456
+ throw new TRPCError({
2457
+ code: "NOT_FOUND",
2458
+ message: `Script file not found for policy '${input.commandName}': ${err instanceof Error ? err.message : String(err)}`
2459
+ });
2460
+ }
2461
+ if (!stat.isFile()) throw new TRPCError({
2462
+ code: "BAD_REQUEST",
2463
+ message: `Script path for policy '${input.commandName}' is not a regular file.`
2464
+ });
2465
+ if (stat.size > MAX_POLICY_SCRIPT_BYTES) throw new TRPCError({
2466
+ code: "BAD_REQUEST",
2467
+ message: `Script file exceeds the ${MAX_POLICY_SCRIPT_BYTES}-byte limit.`
2468
+ });
2469
+ if (stat.size > MAX_INLINE_SCRIPT_LENGTH) {
2470
+ const tmpDir = path.join(agentDir, "tmp");
2471
+ await fsPromises.mkdir(tmpDir, { recursive: true });
2472
+ const ext = path.extname(realCommand);
2473
+ const safeName = input.commandName.replace(/[^a-zA-Z0-9._-]/g, "_");
2474
+ const destPath = path.join(tmpDir, `policy-script-${safeName}${ext}`);
2475
+ await fsPromises.copyFile(realCommand, destPath);
2476
+ return {
2477
+ path: realCommand,
2478
+ size: stat.size,
2479
+ spilledTo: `./tmp/policy-script-${safeName}${ext}`
2480
+ };
2481
+ }
2482
+ const content = await fsPromises.readFile(realCommand, "utf8");
2483
+ return {
2484
+ path: realCommand,
2485
+ size: stat.size,
2486
+ content
2487
+ };
2488
+ });
2489
+ const executePolicyHelp = apiProcedure.input(z.object({ commandName: z.string() })).query(async ({ input, ctx }) => {
2490
+ const workspaceRoot = getWorkspaceRoot();
2491
+ const policy = (await readPoliciesForPath(await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot), workspaceRoot))?.policies?.[input.commandName];
2492
+ if (!policy) throw new TRPCError({
2493
+ code: "NOT_FOUND",
2494
+ message: `Policy not found: ${input.commandName}`
2495
+ });
2496
+ if (!policy.allowHelp) return {
2497
+ stdout: "",
2498
+ stderr: "This command does not support --help\n",
2499
+ exitCode: 1
2500
+ };
2501
+ const fullArgs = [...policy.args || [], "--help"];
2502
+ const { stdout, stderr, exitCode } = await executeSafe(policy.command, fullArgs, { cwd: getWorkspaceRoot() });
2503
+ return {
2504
+ stdout,
2505
+ stderr,
2506
+ exitCode
2507
+ };
2508
+ });
2509
+ const createPolicyRequest = apiProcedure.input(z.object({
2510
+ commandName: z.string(),
2511
+ args: z.array(z.string()),
2512
+ fileMappings: z.record(z.string(), z.string()),
2513
+ cwd: z.string().optional()
2514
+ })).mutation(async ({ input, ctx }) => {
2515
+ if (!ctx.tokenPayload) throw new TRPCError({
2516
+ code: "UNAUTHORIZED",
2517
+ message: "Missing token"
2518
+ });
2519
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
2520
+ const snapshotDir = path.join(getClawminiDir(process.cwd()), "tmp", "snapshots");
2521
+ const store = new RequestStore(process.cwd());
2522
+ const agentDir = await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot);
2523
+ const service = new PolicyRequestService(store, agentDir, snapshotDir);
2524
+ const chatId = ctx.tokenPayload.chatId;
2525
+ const agentId = ctx.tokenPayload.agentId;
2526
+ const policy = (await readPoliciesForPath(agentDir, workspaceRoot))?.policies?.[input.commandName];
2527
+ if (!policy) throw new TRPCError({
2528
+ code: "NOT_FOUND",
2529
+ message: `Policy not found: ${input.commandName}`
2530
+ });
2531
+ const isAutoApprove = !!policy.autoApprove;
2532
+ const request = await service.createRequest(input.commandName, input.args, input.fileMappings, chatId, agentId, isAutoApprove, ctx.tokenPayload.subagentId, input.cwd);
2533
+ if (isAutoApprove) {
2534
+ const result = await executeRequest(request, policy, await resolveRequestCwd(request.cwd, agentId, workspaceRoot));
2535
+ const { exitCode, commandStr } = result;
2536
+ const { stdout, stderr } = await truncateLargeOutput(result.stdout, result.stderr, request.id, agentId);
2537
+ request.executionResult = {
2538
+ stdout,
2539
+ stderr,
2540
+ exitCode
2541
+ };
2542
+ await appendMessage(chatId, {
2543
+ id: randomUUID(),
2544
+ messageId: randomUUID(),
2545
+ role: "policy",
2546
+ requestId: request.id,
2547
+ commandName: input.commandName,
2548
+ args: input.args,
2549
+ status: "approved",
2550
+ content: `[Auto-approved] Policy ${input.commandName} was executed.\n\nCommand: ${commandStr}\nExit Code: ${exitCode}\n\nStdout:\n${stdout}\n\nStderr:\n${stderr}`,
2551
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2552
+ sessionId: ctx.tokenPayload.sessionId,
2553
+ ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {},
2554
+ ...ctx.tokenPayload.turnId ? { turnId: ctx.tokenPayload.turnId } : {}
2555
+ });
2556
+ return request;
2557
+ }
2558
+ const previewContent = await generateRequestPreview(request);
2559
+ await appendMessage(chatId, {
2560
+ id: randomUUID(),
2561
+ messageId: randomUUID(),
2562
+ role: "policy",
2563
+ requestId: request.id,
2564
+ commandName: input.commandName,
2565
+ args: input.args,
2566
+ status: "pending",
2567
+ content: previewContent,
2568
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2569
+ displayRole: "agent",
2570
+ sessionId: ctx.tokenPayload.sessionId,
2571
+ ...ctx.tokenPayload.turnId ? { turnId: ctx.tokenPayload.turnId } : {}
2572
+ });
2573
+ return request;
2574
+ });
2575
+
1921
2576
  //#endregion
1922
2577
  //#region src/daemon/api/subagent-utils.ts
1923
2578
  function getSubagentDepth(settings, parentId) {
@@ -1929,62 +2584,87 @@ function getSubagentDepth(settings, parentId) {
1929
2584
  }
1930
2585
  return depth;
1931
2586
  }
2587
+ /**
2588
+ * Executes a subagent. Callers MUST have already called `incrementSubagent`
2589
+ * for the parent's turn synchronously before any `await` — this function's
2590
+ * `finally` block decrements, and we need the caller to increment earlier
2591
+ * so that a sibling's completing task cannot decrement the parent's counter
2592
+ * to zero (firing `turnEnded`) before this call's task is enqueued.
2593
+ */
1932
2594
  async function executeSubagent(chatId, subagentId, agentId, sessionId, prompt, isAsync, parentTokenPayload, workspaceRoot) {
2595
+ const parentTurnId = parentTokenPayload?.turnId;
1933
2596
  try {
1934
- const settings = await readChatSettings(chatId) || {};
1935
- const resolvedRouters = resolveRouters(settings.routers ?? [], false);
1936
- let routerState = {
1937
- messageId: randomUUID(),
1938
- message: prompt,
1939
- chatId,
1940
- agentId,
1941
- sessionId,
1942
- env: {}
1943
- };
1944
- const initialState = { ...routerState };
1945
- routerState = await executeRouterPipeline(routerState, resolvedRouters);
1946
- await applyRouterStateUpdates(chatId, workspaceRoot, routerState, settings, initialState.agentId);
1947
- await executeDirectMessage(chatId, routerState, void 0, workspaceRoot, false, void 0, subagentId);
1948
- if (taskScheduler.hasTasks(sessionId)) return;
1949
- await updateChatSettings(chatId, (finalSettings) => {
1950
- if (finalSettings.subagents?.[subagentId]) finalSettings.subagents[subagentId].status = "completed";
1951
- return finalSettings;
1952
- });
1953
- const logger = createChatLogger(chatId, subagentId);
1954
- await logger.logSubagentStatus({
1955
- subagentId,
1956
- status: "completed"
1957
- });
1958
- if (isAsync) {
1959
- const lastLogMessage = await logger.findLastMessage((m) => m.role === "agent" || m.displayRole === "agent");
1960
- let outputContent = "";
1961
- if (lastLogMessage && "content" in lastLogMessage) outputContent = `\n\n<subagent_output>\n${lastLogMessage.content}\n</subagent_output>`;
1962
- console.log("Notifying parent", chatId, parentTokenPayload?.agentId, parentTokenPayload?.subagentId);
1963
- await executeDirectMessage(chatId, {
2597
+ try {
2598
+ const settings = await readChatSettings(chatId) || {};
2599
+ const resolvedRouters = resolveRouters(settings.routers ?? [], false);
2600
+ let routerState = {
1964
2601
  messageId: randomUUID(),
1965
- message: `<notification>Subagent ${subagentId} completed.</notification>${outputContent}`,
2602
+ message: prompt,
1966
2603
  chatId,
1967
- agentId: parentTokenPayload?.agentId || "default",
1968
- ...parentTokenPayload?.subagentId ? { subagentId: parentTokenPayload.subagentId } : {},
1969
- sessionId: parentTokenPayload?.sessionId || "default",
2604
+ agentId,
2605
+ sessionId,
2606
+ subagentId,
1970
2607
  env: {}
1971
- }, void 0, workspaceRoot, true, void 0, parentTokenPayload?.subagentId, "subagent_update");
2608
+ };
2609
+ const initialState = { ...routerState };
2610
+ routerState = await executeRouterPipeline(routerState, resolvedRouters);
2611
+ await applyRouterStateUpdates(chatId, workspaceRoot, routerState, settings, initialState.agentId);
2612
+ await executeDirectMessage(chatId, routerState, void 0, workspaceRoot, false, void 0, subagentId, void 0, void 0, parentTurnId);
2613
+ if (taskScheduler.hasTasks(sessionId)) return;
2614
+ await updateChatSettings(chatId, (finalSettings) => {
2615
+ if (finalSettings.subagents?.[subagentId]) finalSettings.subagents[subagentId].status = "completed";
2616
+ return finalSettings;
2617
+ });
2618
+ const logger = createChatLogger(chatId, subagentId, sessionId, parentTurnId);
2619
+ await logger.logSubagentStatus({
2620
+ subagentId,
2621
+ status: "completed"
2622
+ });
2623
+ if (isAsync) {
2624
+ const lastLogMessage = await logger.findLastMessage((m) => m.role === "agent" || m.displayRole === "agent");
2625
+ let outputContent = "";
2626
+ if (lastLogMessage && "content" in lastLogMessage) outputContent = `\n\n<subagent_output>\n${lastLogMessage.content}\n</subagent_output>`;
2627
+ console.log("Notifying parent", chatId, parentTokenPayload?.agentId, parentTokenPayload?.subagentId);
2628
+ await executeDirectMessage(chatId, {
2629
+ messageId: randomUUID(),
2630
+ message: `<notification>Subagent ${subagentId} completed.</notification>${outputContent}`,
2631
+ chatId,
2632
+ agentId: parentTokenPayload?.agentId || "default",
2633
+ ...parentTokenPayload?.subagentId ? { subagentId: parentTokenPayload.subagentId } : {},
2634
+ sessionId: parentTokenPayload?.sessionId || "default",
2635
+ env: {}
2636
+ }, void 0, workspaceRoot, true, void 0, parentTokenPayload?.subagentId, "subagent_update", void 0, parentTurnId);
2637
+ }
2638
+ } catch {
2639
+ await updateChatSettings(chatId, (errSettings) => {
2640
+ if (errSettings.subagents?.[subagentId]) errSettings.subagents[subagentId].status = "failed";
2641
+ return errSettings;
2642
+ });
2643
+ await createChatLogger(chatId, subagentId, sessionId, parentTurnId).logSubagentStatus({
2644
+ subagentId,
2645
+ status: "failed"
2646
+ });
1972
2647
  }
1973
- } catch {
1974
- await updateChatSettings(chatId, (errSettings) => {
1975
- if (errSettings.subagents?.[subagentId]) errSettings.subagents[subagentId].status = "failed";
1976
- return errSettings;
1977
- });
1978
- await createChatLogger(chatId, subagentId).logSubagentStatus({
1979
- subagentId,
1980
- status: "failed"
1981
- });
2648
+ } finally {
2649
+ decrementSubagent(parentTurnId);
1982
2650
  }
1983
2651
  }
1984
2652
 
1985
2653
  //#endregion
1986
2654
  //#region src/daemon/api/subagent-router.ts
1987
2655
  const MAX_SUBAGENT_DEPTH = 2;
2656
+ function assertSubagentAccess(settings, subagentId, callerSubagentId) {
2657
+ const sub = settings?.subagents?.[subagentId];
2658
+ if (!sub) throw new TRPCError({
2659
+ code: "NOT_FOUND",
2660
+ message: "Subagent not found"
2661
+ });
2662
+ if (sub.parentId !== callerSubagentId) throw new TRPCError({
2663
+ code: "FORBIDDEN",
2664
+ message: "Subagent is not a child of the caller"
2665
+ });
2666
+ return sub;
2667
+ }
1988
2668
  const subagentSpawn = apiProcedure.input(z.object({
1989
2669
  subagentId: z.string().optional(),
1990
2670
  targetAgentId: z.string().optional(),
@@ -1998,39 +2678,47 @@ const subagentSpawn = apiProcedure.input(z.object({
1998
2678
  const chatId = ctx.tokenPayload.chatId;
1999
2679
  const parentAgentId = ctx.tokenPayload.agentId;
2000
2680
  const parentId = ctx.tokenPayload.subagentId;
2681
+ const parentTurnId = ctx.tokenPayload.turnId;
2001
2682
  const id = input.subagentId || randomUUID();
2002
2683
  const sessionId = randomUUID();
2003
2684
  const agentId = input.targetAgentId || parentAgentId;
2004
2685
  let depth = 0;
2005
- await updateChatSettings(chatId, (settings) => {
2006
- settings.subagents = settings.subagents || {};
2007
- depth = getSubagentDepth(settings, parentId);
2008
- if (depth >= MAX_SUBAGENT_DEPTH) throw new TRPCError({
2009
- code: "BAD_REQUEST",
2010
- message: "Max subagent depth reached"
2011
- });
2012
- if (settings.subagents[id]) throw new TRPCError({
2013
- code: "BAD_REQUEST",
2014
- message: "Subagent ID already exists"
2686
+ incrementSubagent(parentTurnId);
2687
+ let handedOff = false;
2688
+ try {
2689
+ await updateChatSettings(chatId, (settings) => {
2690
+ settings.subagents = settings.subagents || {};
2691
+ depth = getSubagentDepth(settings, parentId);
2692
+ if (depth >= MAX_SUBAGENT_DEPTH) throw new TRPCError({
2693
+ code: "BAD_REQUEST",
2694
+ message: "Max subagent depth reached"
2695
+ });
2696
+ if (settings.subagents[id]) throw new TRPCError({
2697
+ code: "BAD_REQUEST",
2698
+ message: "Subagent ID already exists"
2699
+ });
2700
+ settings.subagents[id] = {
2701
+ id,
2702
+ agentId,
2703
+ sessionId,
2704
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2705
+ status: "active",
2706
+ parentId
2707
+ };
2708
+ return settings;
2015
2709
  });
2016
- settings.subagents[id] = {
2710
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
2711
+ const isAsync = input.async ?? depth === 0;
2712
+ handedOff = true;
2713
+ executeSubagent(chatId, id, agentId, sessionId, input.prompt, isAsync, ctx.tokenPayload, workspaceRoot);
2714
+ return {
2017
2715
  id,
2018
- agentId,
2019
- sessionId,
2020
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2021
- status: "active",
2022
- parentId
2716
+ depth,
2717
+ isAsync
2023
2718
  };
2024
- return settings;
2025
- });
2026
- const workspaceRoot = getWorkspaceRoot(process.cwd());
2027
- const isAsync = input.async ?? depth === 0;
2028
- executeSubagent(chatId, id, agentId, sessionId, input.prompt, isAsync, ctx.tokenPayload, workspaceRoot);
2029
- return {
2030
- id,
2031
- depth,
2032
- isAsync
2033
- };
2719
+ } finally {
2720
+ if (!handedOff) decrementSubagent(parentTurnId);
2721
+ }
2034
2722
  });
2035
2723
  const subagentSend = apiProcedure.input(z.object({
2036
2724
  subagentId: z.string(),
@@ -2042,26 +2730,26 @@ const subagentSend = apiProcedure.input(z.object({
2042
2730
  message: "Missing token"
2043
2731
  });
2044
2732
  const chatId = ctx.tokenPayload.chatId;
2733
+ const parentTurnId = ctx.tokenPayload.turnId;
2045
2734
  let sub;
2046
- await updateChatSettings(chatId, (settings) => {
2047
- if (!settings.subagents?.[input.subagentId]) throw new TRPCError({
2048
- code: "NOT_FOUND",
2049
- message: "Subagent not found"
2735
+ incrementSubagent(parentTurnId);
2736
+ let handedOff = false;
2737
+ try {
2738
+ await updateChatSettings(chatId, (settings) => {
2739
+ sub = assertSubagentAccess(settings, input.subagentId, ctx.tokenPayload.subagentId);
2740
+ sub.status = "active";
2741
+ return settings;
2050
2742
  });
2051
- sub = settings.subagents[input.subagentId];
2052
- sub.status = "active";
2053
- return settings;
2054
- });
2055
- const workspaceRoot = getWorkspaceRoot(process.cwd());
2056
- executeSubagent(chatId, sub.id, sub.agentId || "default", sub.sessionId || "default", input.prompt, input.async, ctx.tokenPayload, workspaceRoot);
2057
- return { success: true };
2743
+ const workspaceRoot = getWorkspaceRoot(process.cwd());
2744
+ handedOff = true;
2745
+ executeSubagent(chatId, sub.id, sub.agentId || "default", sub.sessionId || "default", input.prompt, input.async, ctx.tokenPayload, workspaceRoot);
2746
+ return { success: true };
2747
+ } finally {
2748
+ if (!handedOff) decrementSubagent(parentTurnId);
2749
+ }
2058
2750
  });
2059
- async function checkSubagentStatus(chatId, subagentId) {
2060
- const sub = (await readChatSettings(chatId))?.subagents?.[subagentId];
2061
- if (!sub) throw new TRPCError({
2062
- code: "NOT_FOUND",
2063
- message: "Subagent not found"
2064
- });
2751
+ async function checkSubagentStatus(chatId, subagentId, callerSubagentId) {
2752
+ const sub = assertSubagentAccess(await readChatSettings(chatId), subagentId, callerSubagentId);
2065
2753
  if (sub.status === "completed" || sub.status === "failed") {
2066
2754
  let outputContent;
2067
2755
  if (sub.status === "completed") {
@@ -2090,7 +2778,7 @@ const subagentWait = apiProcedure.input(z.object({ subagentId: z.string() })).mu
2090
2778
  if (signal) signal.addEventListener("abort", onAbort);
2091
2779
  const eventIterator = on(daemonEvents, DAEMON_EVENT_MESSAGE_APPENDED, { signal: ac.signal });
2092
2780
  try {
2093
- const initialStatus = await checkSubagentStatus(chatId, input.subagentId);
2781
+ const initialStatus = await checkSubagentStatus(chatId, input.subagentId, ctx.tokenPayload.subagentId);
2094
2782
  if (initialStatus) {
2095
2783
  clearTimeout(timeout);
2096
2784
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2098,7 +2786,7 @@ const subagentWait = apiProcedure.input(z.object({ subagentId: z.string() })).mu
2098
2786
  }
2099
2787
  for await (const [event] of eventIterator) if (event.chatId === chatId && event.message?.subagentId === input.subagentId) {
2100
2788
  if (event.message.role === "subagent_status") {
2101
- const status = await checkSubagentStatus(chatId, input.subagentId);
2789
+ const status = await checkSubagentStatus(chatId, input.subagentId, ctx.tokenPayload.subagentId);
2102
2790
  if (status) {
2103
2791
  clearTimeout(timeout);
2104
2792
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -2130,13 +2818,9 @@ const subagentStop = apiProcedure.input(z.object({ subagentId: z.string() })).mu
2130
2818
  const chatId = ctx.tokenPayload.chatId;
2131
2819
  let subToStop;
2132
2820
  await updateChatSettings(chatId, (settings) => {
2133
- if (settings.subagents) {
2134
- const sub = settings.subagents[input.subagentId];
2135
- if (sub) {
2136
- sub.status = "failed";
2137
- subToStop = sub;
2138
- }
2139
- }
2821
+ const sub = assertSubagentAccess(settings, input.subagentId, ctx.tokenPayload.subagentId);
2822
+ sub.status = "failed";
2823
+ subToStop = sub;
2140
2824
  return settings;
2141
2825
  });
2142
2826
  if (subToStop) (await createAgentSession({
@@ -2156,10 +2840,8 @@ const subagentDelete = apiProcedure.input(z.object({ subagentId: z.string() })).
2156
2840
  const chatId = ctx.tokenPayload.chatId;
2157
2841
  let subToDelete;
2158
2842
  await updateChatSettings(chatId, (settings) => {
2159
- if (settings.subagents && settings.subagents[input.subagentId]) {
2160
- subToDelete = settings.subagents[input.subagentId];
2161
- delete settings.subagents[input.subagentId];
2162
- }
2843
+ subToDelete = assertSubagentAccess(settings, input.subagentId, ctx.tokenPayload.subagentId);
2844
+ delete settings.subagents[input.subagentId];
2163
2845
  return settings;
2164
2846
  });
2165
2847
  if (subToDelete) {
@@ -2204,6 +2886,7 @@ const subagentTail = apiProcedure.input(z.object({
2204
2886
  message: "Missing token"
2205
2887
  });
2206
2888
  const chatId = ctx.tokenPayload.chatId;
2889
+ assertSubagentAccess(await readChatSettings(chatId), input.subagentId, ctx.tokenPayload.subagentId);
2207
2890
  return { messages: await createChatLogger(chatId, input.subagentId).getMessages(input.limit) };
2208
2891
  });
2209
2892
 
@@ -2241,7 +2924,9 @@ const logMessage = apiProcedure.input(z.object({
2241
2924
  command: `clawmini-lite log${filesArgStr}`,
2242
2925
  cwd: process.cwd(),
2243
2926
  exitCode: 0,
2927
+ sessionId: ctx.tokenPayload.sessionId,
2244
2928
  ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {},
2929
+ ...ctx.tokenPayload.turnId ? { turnId: ctx.tokenPayload.turnId } : {},
2245
2930
  ...filePaths.length > 0 ? { files: filePaths } : {}
2246
2931
  });
2247
2932
  return { success: true };
@@ -2271,7 +2956,9 @@ const logReplyMessage = apiProcedure.input(z.object({
2271
2956
  role: "agent",
2272
2957
  content: input.message,
2273
2958
  timestamp,
2959
+ sessionId: ctx.tokenPayload.sessionId,
2274
2960
  ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {},
2961
+ ...ctx.tokenPayload.turnId ? { turnId: ctx.tokenPayload.turnId } : {},
2275
2962
  ...filePaths.length > 0 ? { files: filePaths } : {}
2276
2963
  });
2277
2964
  return { success: true };
@@ -2304,7 +2991,9 @@ const logToolMessage = apiProcedure.input(z.object({
2304
2991
  payload: payloadObj,
2305
2992
  content: contentStr,
2306
2993
  timestamp,
2307
- ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}
2994
+ sessionId: ctx.tokenPayload.sessionId,
2995
+ ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {},
2996
+ ...ctx.tokenPayload.turnId ? { turnId: ctx.tokenPayload.turnId } : {}
2308
2997
  });
2309
2998
  return { success: true };
2310
2999
  });
@@ -2316,15 +3005,34 @@ const agentListCronJobs = apiProcedure.query(async ({ ctx }) => {
2316
3005
  const chatId = ctx.tokenPayload.chatId;
2317
3006
  return listCronJobsShared(chatId);
2318
3007
  });
2319
- const agentAddCronJob = apiProcedure.input(z.object({ job: CronJobSchema })).mutation(async ({ input, ctx }) => {
3008
+ const AgentCronJobInputSchema = z.strictObject({
3009
+ id: z.string().min(1),
3010
+ message: z.string().default(""),
3011
+ reply: z.string().optional(),
3012
+ session: z.union([z.strictObject({ type: z.literal("new") }), z.strictObject({
3013
+ type: z.literal("existing"),
3014
+ id: z.string()
3015
+ })]).optional(),
3016
+ schedule: z.union([
3017
+ z.strictObject({ cron: z.string() }),
3018
+ z.strictObject({ every: z.string() }),
3019
+ z.strictObject({ at: z.string() })
3020
+ ])
3021
+ });
3022
+ const agentAddCronJob = apiProcedure.input(z.object({ job: AgentCronJobInputSchema })).mutation(async ({ input, ctx }) => {
2320
3023
  if (!ctx.tokenPayload) throw new TRPCError({
2321
3024
  code: "UNAUTHORIZED",
2322
3025
  message: "Missing token"
2323
3026
  });
2324
3027
  const chatId = ctx.tokenPayload.chatId;
2325
3028
  return addCronJobShared(chatId, {
2326
- ...input.job,
2327
- agentId: ctx.tokenPayload.agentId
3029
+ id: input.job.id,
3030
+ message: input.job.message,
3031
+ schedule: input.job.schedule,
3032
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3033
+ agentId: ctx.tokenPayload.agentId,
3034
+ ...input.job.reply !== void 0 ? { reply: input.job.reply } : {},
3035
+ ...input.job.session !== void 0 ? { session: input.job.session } : {}
2328
3036
  });
2329
3037
  });
2330
3038
  const agentDeleteCronJob = apiProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
@@ -2335,87 +3043,6 @@ const agentDeleteCronJob = apiProcedure.input(z.object({ id: z.string() })).muta
2335
3043
  const chatId = ctx.tokenPayload.chatId;
2336
3044
  return deleteCronJobShared(chatId, input.id);
2337
3045
  });
2338
- const listPolicies = apiProcedure.query(async () => {
2339
- return await readPolicies();
2340
- });
2341
- const executePolicyHelp = apiProcedure.input(z.object({ commandName: z.string() })).query(async ({ input }) => {
2342
- const policy = (await readPolicies())?.policies?.[input.commandName];
2343
- if (!policy) throw new TRPCError({
2344
- code: "NOT_FOUND",
2345
- message: `Policy not found: ${input.commandName}`
2346
- });
2347
- if (!policy.allowHelp) return {
2348
- stdout: "",
2349
- stderr: "This command does not support --help\n",
2350
- exitCode: 1
2351
- };
2352
- const fullArgs = [...policy.args || [], "--help"];
2353
- const { stdout, stderr, exitCode } = await executeSafe(policy.command, fullArgs, { cwd: getWorkspaceRoot() });
2354
- return {
2355
- stdout,
2356
- stderr,
2357
- exitCode
2358
- };
2359
- });
2360
- const createPolicyRequest = apiProcedure.input(z.object({
2361
- commandName: z.string(),
2362
- args: z.array(z.string()),
2363
- fileMappings: z.record(z.string(), z.string())
2364
- })).mutation(async ({ input, ctx }) => {
2365
- if (!ctx.tokenPayload) throw new TRPCError({
2366
- code: "UNAUTHORIZED",
2367
- message: "Missing token"
2368
- });
2369
- const workspaceRoot = getWorkspaceRoot(process.cwd());
2370
- const snapshotDir = path.join(getClawminiDir(process.cwd()), "tmp", "snapshots");
2371
- const store = new RequestStore(process.cwd());
2372
- const service = new PolicyRequestService(store, await resolveAgentDir(ctx.tokenPayload?.agentId, workspaceRoot), snapshotDir);
2373
- const chatId = ctx.tokenPayload.chatId;
2374
- const agentId = ctx.tokenPayload.agentId;
2375
- const policy = (await readPolicies())?.policies?.[input.commandName];
2376
- if (!policy) throw new TRPCError({
2377
- code: "NOT_FOUND",
2378
- message: `Policy not found: ${input.commandName}`
2379
- });
2380
- const isAutoApprove = !!policy.autoApprove;
2381
- const request = await service.createRequest(input.commandName, input.args, input.fileMappings, chatId, agentId, isAutoApprove, ctx.tokenPayload.subagentId);
2382
- if (isAutoApprove) {
2383
- const { stdout, stderr, exitCode, commandStr } = await executeRequest(request, policy, getWorkspaceRoot());
2384
- request.executionResult = {
2385
- stdout,
2386
- stderr,
2387
- exitCode
2388
- };
2389
- await store.save(request);
2390
- await appendMessage(chatId, {
2391
- id: randomUUID(),
2392
- messageId: randomUUID(),
2393
- role: "policy",
2394
- requestId: request.id,
2395
- commandName: input.commandName,
2396
- args: input.args,
2397
- status: "approved",
2398
- content: `[Auto-approved] Policy ${input.commandName} was executed.\n\nCommand: ${commandStr}\nExit Code: ${exitCode}\n\nStdout:\n${stdout}\n\nStderr:\n${stderr}`,
2399
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2400
- ...ctx.tokenPayload.subagentId ? { subagentId: ctx.tokenPayload.subagentId } : {}
2401
- });
2402
- return request;
2403
- }
2404
- const previewContent = await generateRequestPreview(request);
2405
- await appendMessage(chatId, {
2406
- id: randomUUID(),
2407
- messageId: randomUUID(),
2408
- role: "policy",
2409
- requestId: request.id,
2410
- commandName: input.commandName,
2411
- args: input.args,
2412
- status: "pending",
2413
- content: previewContent,
2414
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2415
- displayRole: "agent"
2416
- });
2417
- return request;
2418
- });
2419
3046
  const fetchPendingMessages = apiProcedure.mutation(async ({ ctx }) => {
2420
3047
  if (!ctx.tokenPayload?.agentId) throw new TRPCError({
2421
3048
  code: "UNAUTHORIZED",
@@ -2436,6 +3063,7 @@ const agentRouter = router({
2436
3063
  listPolicies,
2437
3064
  executePolicyHelp,
2438
3065
  createPolicyRequest,
3066
+ readPolicyScript,
2439
3067
  fetchPendingMessages,
2440
3068
  ping,
2441
3069
  subagentSpawn,
@@ -2482,8 +3110,7 @@ async function initDaemon() {
2482
3110
  env: {
2483
3111
  ...process.env,
2484
3112
  ENV_DIR: envDir
2485
- },
2486
- timeout: hookType === "down" ? 1e4 : void 0
3113
+ }
2487
3114
  });
2488
3115
  }
2489
3116
  } catch (err) {
@@ -2557,9 +3184,20 @@ async function initDaemon() {
2557
3184
  }
2558
3185
  };
2559
3186
  await cleanOrphanedSubagents();
3187
+ try {
3188
+ const removed = await new RequestStore(getWorkspaceRoot()).cleanupCompleted();
3189
+ if (removed > 0) console.log(`Cleaned up ${removed} completed policy request file(s).`);
3190
+ } catch (err) {
3191
+ console.warn("Failed to clean completed policy requests:", err);
3192
+ }
2560
3193
  await runHooks("up");
2561
3194
  isReady = true;
2562
3195
  readyPromiseResolve();
3196
+ try {
3197
+ await drainPendingReplies(getClawminiVersion());
3198
+ } catch (err) {
3199
+ console.warn("Failed to drain pending replies:", err);
3200
+ }
2563
3201
  cronManager.init().catch((err) => {
2564
3202
  console.error("Failed to initialize cron manager:", err);
2565
3203
  });