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
@@ -0,0 +1,266 @@
1
+ import type { ChatMessage } from '../chats.js';
2
+ import {
3
+ buildTurnStartEntry,
4
+ condenseTurnLog,
5
+ formatTurnLogEntry,
6
+ type TurnLogEntry,
7
+ } from './turn-log.js';
8
+
9
+ export interface TurnLogBufferOptions {
10
+ maxToolPreview: number;
11
+ maxLogMessageChars: number;
12
+ editDebounceMs: number;
13
+ }
14
+
15
+ export interface TurnLogBufferDeps<TAnchor> {
16
+ /**
17
+ * Posts a new threaded message under `anchor` and returns the new message's
18
+ * id (whatever the transport uses to address it later for editing). Throws
19
+ * on failure; the buffer treats the *initial* post failure as an
20
+ * unrecoverable per-turn abort.
21
+ */
22
+ postThreaded: (anchor: TAnchor, text: string) => Promise<string | undefined>;
23
+ /** Edits a previously-posted threaded message by id. Throws on failure. */
24
+ editThreaded: (anchor: TAnchor, messageId: string, text: string) => Promise<void>;
25
+ /**
26
+ * Predicate that decides whether an error from `editThreaded` indicates the
27
+ * underlying message no longer exists (e.g. user deleted it). When true,
28
+ * the buffer recovers by posting a fresh log message rather than retrying.
29
+ */
30
+ isMissingMessageError: (err: unknown) => boolean;
31
+ options: TurnLogBufferOptions;
32
+ /** When false, no thread-log activity is ever posted. */
33
+ threadsEnabled: boolean;
34
+ }
35
+
36
+ export interface StartParams<TAnchor> {
37
+ turnId: string;
38
+ /** Per-chat opt-out. When true, thread-log is suppressed for this turn. */
39
+ threadsDisabled: boolean;
40
+ /** Anchor to post into, if known at turn start. */
41
+ anchorThread: TAnchor | undefined;
42
+ }
43
+
44
+ export interface TurnLogBuffer<TAnchor> {
45
+ start(params: StartParams<TAnchor>): void;
46
+ append(turnId: string, message: ChatMessage): void;
47
+ assignAnchor(turnId: string, anchor: TAnchor): void;
48
+ end(turnId: string): Promise<void>;
49
+ has(turnId: string): boolean;
50
+ isAnchored(turnId: string): boolean;
51
+ threadsDisabledFor(turnId: string): boolean;
52
+ shutdown(): void;
53
+ }
54
+
55
+ interface Ctx<TAnchor> {
56
+ turnId: string;
57
+ threadsDisabled: boolean;
58
+ startedAt: string;
59
+ anchor: TAnchor | undefined;
60
+ activityLogMessageId: string | undefined;
61
+ entries: TurnLogEntry[];
62
+ editTimer: NodeJS.Timeout | null;
63
+ flushChain: Promise<void>;
64
+ /**
65
+ * Once the thread-log post fails, the whole turn's activity log is
66
+ * abandoned: subsequent appends drop silently rather than trying to post
67
+ * anything else. Matches the user-level expectation that if the thread
68
+ * never opened, we simply stop logging for that turn.
69
+ */
70
+ aborted: boolean;
71
+ }
72
+
73
+ export function createTurnLogBuffer<TAnchor>(
74
+ deps: TurnLogBufferDeps<TAnchor>
75
+ ): TurnLogBuffer<TAnchor> {
76
+ const { options, threadsEnabled, postThreaded, editThreaded, isMissingMessageError } = deps;
77
+ const ctxs = new Map<string, Ctx<TAnchor>>();
78
+
79
+ const engaged = (ctx: Ctx<TAnchor>) => threadsEnabled && !ctx.threadsDisabled && !ctx.aborted;
80
+
81
+ const runFlush = async (ctx: Ctx<TAnchor>): Promise<void> => {
82
+ if (!engaged(ctx)) {
83
+ ctx.entries = [];
84
+ return;
85
+ }
86
+ if (ctx.anchor === undefined) return;
87
+ if (ctx.entries.length === 0) return;
88
+
89
+ let result = condenseTurnLog(ctx.entries, { maxChars: options.maxLogMessageChars });
90
+
91
+ const send = async (): Promise<void> => {
92
+ const text = result.kind === 'fits' ? result.text : result.finalText;
93
+ if (!ctx.activityLogMessageId) {
94
+ try {
95
+ const id = await postThreaded(ctx.anchor!, text);
96
+ if (id) ctx.activityLogMessageId = id;
97
+ } catch (err) {
98
+ console.error(
99
+ `Failed to open thread-log for turn ${ctx.turnId}; dropping further thread-log events for this turn.`,
100
+ err
101
+ );
102
+ ctx.aborted = true;
103
+ ctx.entries = [];
104
+ }
105
+ return;
106
+ }
107
+ try {
108
+ await editThreaded(ctx.anchor!, ctx.activityLogMessageId, text);
109
+ } catch (err) {
110
+ if (isMissingMessageError(err)) {
111
+ // The activity-log message is gone (user deleted it, transport
112
+ // returned a "not found" code). Open a fresh log message on the
113
+ // next event rather than retrying the edit.
114
+ console.warn('Log message missing on edit — opening a fresh log message.');
115
+ ctx.activityLogMessageId = undefined;
116
+ try {
117
+ const id = await postThreaded(ctx.anchor!, text);
118
+ if (id) ctx.activityLogMessageId = id;
119
+ } catch (innerErr) {
120
+ console.error('Failed to re-open log message after missing edit:', innerErr);
121
+ ctx.activityLogMessageId = undefined;
122
+ }
123
+ } else {
124
+ await new Promise((resolve) => setTimeout(resolve, 500));
125
+ try {
126
+ await editThreaded(ctx.anchor!, ctx.activityLogMessageId, text);
127
+ } catch (retryErr) {
128
+ console.warn('Edit failed twice — finalizing log message.', retryErr);
129
+ ctx.activityLogMessageId = undefined;
130
+ }
131
+ }
132
+ }
133
+ };
134
+
135
+ await send();
136
+
137
+ // On rollover, the finalized message is sealed; carry-over entries seed a
138
+ // brand-new activity-log message. A single flush can rollover multiple
139
+ // times (tight budget with several entries), so loop until the carry fits
140
+ // or is empty.
141
+ while (!ctx.aborted && result.kind === 'rollover') {
142
+ const prevLen = ctx.entries.length;
143
+ ctx.entries = result.carryEntries.slice();
144
+ ctx.activityLogMessageId = undefined;
145
+ if (ctx.entries.length === 0) break;
146
+ // Degenerate case: a single entry's rendered line is itself larger
147
+ // than the per-message budget. Drop the stuck head so we can make
148
+ // progress rather than spinning the flush loop forever.
149
+ if (ctx.entries.length >= prevLen) {
150
+ console.warn(
151
+ `Turn-log entry larger than maxLogMessageChars — dropping head for turn ${ctx.turnId}`
152
+ );
153
+ ctx.entries = ctx.entries.slice(1);
154
+ if (ctx.entries.length === 0) break;
155
+ }
156
+ result = condenseTurnLog(ctx.entries, { maxChars: options.maxLogMessageChars });
157
+ await send();
158
+ }
159
+ };
160
+
161
+ const enqueueFlush = (ctx: Ctx<TAnchor>): void => {
162
+ ctx.flushChain = ctx.flushChain
163
+ .then(() => runFlush(ctx))
164
+ .catch((err) => console.error('Flush error:', err));
165
+ };
166
+
167
+ const scheduleFlush = (ctx: Ctx<TAnchor>) => {
168
+ if (!engaged(ctx)) return;
169
+ if (ctx.editTimer) return;
170
+ ctx.editTimer = setTimeout(() => {
171
+ ctx.editTimer = null;
172
+ enqueueFlush(ctx);
173
+ }, options.editDebounceMs);
174
+ };
175
+
176
+ return {
177
+ start(params) {
178
+ const ctx: Ctx<TAnchor> = {
179
+ turnId: params.turnId,
180
+ threadsDisabled: params.threadsDisabled,
181
+ startedAt: new Date().toISOString(),
182
+ anchor: params.anchorThread,
183
+ activityLogMessageId: undefined,
184
+ // Seed the turn's activity log with an opening entry so the thread
185
+ // appears as soon as the turn starts, rather than only after the first
186
+ // tool call / subagent event. Skipped when threads are disabled.
187
+ entries: threadsEnabled && !params.threadsDisabled ? [buildTurnStartEntry()] : [],
188
+ editTimer: null,
189
+ flushChain: Promise.resolve(),
190
+ aborted: false,
191
+ };
192
+ ctxs.set(params.turnId, ctx);
193
+ // If the anchor is known at start (inbound-user turn, or proactive turn
194
+ // whose top-level post already landed), flush immediately so the
195
+ // "Started processing…" line appears without waiting for the debounce.
196
+ if (ctx.anchor !== undefined && engaged(ctx)) {
197
+ enqueueFlush(ctx);
198
+ }
199
+ },
200
+
201
+ append(turnId, message) {
202
+ const ctx = ctxs.get(turnId);
203
+ if (!ctx) return;
204
+ if (!engaged(ctx)) return;
205
+ const entry = formatTurnLogEntry(message, {
206
+ maxToolPreview: options.maxToolPreview,
207
+ turnStartedAt: ctx.startedAt,
208
+ });
209
+ if (!entry) return;
210
+ ctx.entries.push(entry);
211
+ // Without an anchor yet, entries accumulate; assignAnchor will flush
212
+ // them once the anchor arrives.
213
+ if (ctx.anchor !== undefined) {
214
+ scheduleFlush(ctx);
215
+ }
216
+ },
217
+
218
+ assignAnchor(turnId, anchor) {
219
+ const ctx = ctxs.get(turnId);
220
+ if (!ctx) return;
221
+ if (ctx.anchor !== undefined) return;
222
+ ctx.anchor = anchor;
223
+ if (engaged(ctx) && ctx.entries.length > 0) {
224
+ enqueueFlush(ctx);
225
+ }
226
+ },
227
+
228
+ async end(turnId) {
229
+ const ctx = ctxs.get(turnId);
230
+ if (!ctx) return;
231
+ if (ctx.editTimer) {
232
+ clearTimeout(ctx.editTimer);
233
+ ctx.editTimer = null;
234
+ }
235
+ // Tack a final flush onto the chain so any pending entries are sent.
236
+ enqueueFlush(ctx);
237
+ try {
238
+ await ctx.flushChain;
239
+ } catch (err) {
240
+ console.error('Final flush error:', err);
241
+ }
242
+ ctxs.delete(turnId);
243
+ },
244
+
245
+ has(turnId) {
246
+ return ctxs.has(turnId);
247
+ },
248
+
249
+ isAnchored(turnId) {
250
+ const ctx = ctxs.get(turnId);
251
+ return ctx?.anchor !== undefined;
252
+ },
253
+
254
+ threadsDisabledFor(turnId) {
255
+ const ctx = ctxs.get(turnId);
256
+ return ctx?.threadsDisabled ?? false;
257
+ },
258
+
259
+ shutdown() {
260
+ for (const ctx of ctxs.values()) {
261
+ if (ctx.editTimer) clearTimeout(ctx.editTimer);
262
+ }
263
+ ctxs.clear();
264
+ },
265
+ };
266
+ }
@@ -0,0 +1,389 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ formatTurnLogEntry,
4
+ condenseTurnLog,
5
+ buildTurnStartEntry,
6
+ TURN_START_EMOJI,
7
+ type TurnLogEntry,
8
+ } from './turn-log.js';
9
+ import type {
10
+ AgentReplyMessage,
11
+ CommandLogMessage,
12
+ PolicyRequestMessage,
13
+ SubagentStatusMessage,
14
+ SystemMessage,
15
+ ToolMessage,
16
+ UserMessage,
17
+ } from '../chats.js';
18
+
19
+ const FIXED_TS = '2026-04-20T12:04:02.000Z';
20
+
21
+ function makeTool(overrides: Partial<ToolMessage> = {}): ToolMessage {
22
+ return {
23
+ id: 'id',
24
+ role: 'tool',
25
+ content: '',
26
+ timestamp: FIXED_TS,
27
+ sessionId: 's',
28
+ messageId: 'mid',
29
+ name: 'Read',
30
+ payload: { file_path: 'src/app.ts' },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe('formatTurnLogEntry', () => {
36
+ it('returns null for top-level UserMessage / AgentReplyMessage', () => {
37
+ const user: UserMessage = {
38
+ id: '1',
39
+ role: 'user',
40
+ content: 'hi',
41
+ timestamp: FIXED_TS,
42
+ sessionId: 's',
43
+ };
44
+ const reply: AgentReplyMessage = {
45
+ id: '2',
46
+ role: 'agent',
47
+ content: 'hi back',
48
+ timestamp: FIXED_TS,
49
+ sessionId: 's',
50
+ };
51
+ expect(formatTurnLogEntry(user)).toBeNull();
52
+ expect(formatTurnLogEntry(reply)).toBeNull();
53
+ });
54
+
55
+ it('renders a subagent prompt (user role with subagentId) as a subagent entry', () => {
56
+ const msg: UserMessage = {
57
+ id: '1',
58
+ role: 'user',
59
+ content: 'research auth flow',
60
+ timestamp: FIXED_TS,
61
+ sessionId: 's',
62
+ subagentId: 'sub-1',
63
+ };
64
+ const entry = formatTurnLogEntry(msg);
65
+ expect(entry).not.toBeNull();
66
+ expect(entry!.kind).toBe('subagent');
67
+ expect(entry!.summary).toContain('sub-1');
68
+ expect(entry!.summary).toContain('research auth flow');
69
+ expect(entry!.summary).toContain('👉');
70
+ });
71
+
72
+ it('renders a subagent reply (agent role with subagentId) as a subagent entry', () => {
73
+ const msg: AgentReplyMessage = {
74
+ id: '2',
75
+ role: 'agent',
76
+ content: 'found 3 callers',
77
+ timestamp: FIXED_TS,
78
+ sessionId: 's',
79
+ subagentId: 'sub-1',
80
+ };
81
+ const entry = formatTurnLogEntry(msg);
82
+ expect(entry).not.toBeNull();
83
+ expect(entry!.kind).toBe('subagent');
84
+ expect(entry!.summary).toContain('sub-1');
85
+ expect(entry!.summary).toContain('found 3 callers');
86
+ expect(entry!.summary).toContain('👈');
87
+ });
88
+
89
+ it('formats a ToolMessage with a known extractor (emoji replaces verb)', () => {
90
+ const msg = makeTool({ name: 'Read', payload: { file_path: 'src/app.ts' } });
91
+ const entry = formatTurnLogEntry(msg);
92
+ expect(entry).not.toBeNull();
93
+ expect(entry!.kind).toBe('tool');
94
+ expect(entry!.summary).toBe('📖 src/app.ts');
95
+ });
96
+
97
+ it('falls back to <name>: <json> for unknown tools (no emoji)', () => {
98
+ const msg = makeTool({ name: 'mystery_tool', payload: { foo: 1, bar: 'two' } });
99
+ const entry = formatTurnLogEntry(msg)!;
100
+ expect(entry.summary).toContain('mystery_tool');
101
+ expect(entry.summary).toContain('"foo"');
102
+ expect(entry.summary).toContain('"bar"');
103
+ });
104
+
105
+ it('uses a verb-specific emoji for common tools', () => {
106
+ expect(
107
+ formatTurnLogEntry(makeTool({ name: 'run_shell_command', payload: { command: 'ls -la' } }))!
108
+ .summary
109
+ ).toBe('🧑‍💻 ls -la');
110
+ expect(
111
+ formatTurnLogEntry(
112
+ makeTool({ name: 'activate_skill', payload: { name: 'clawmini-subagents' } })
113
+ )!.summary
114
+ ).toBe('📚 clawmini-subagents');
115
+ expect(
116
+ formatTurnLogEntry(makeTool({ name: 'Bash', payload: { command: 'echo hi' } }))!.summary
117
+ ).toBe('🧑‍💻 echo hi');
118
+ expect(
119
+ formatTurnLogEntry(makeTool({ name: 'Grep', payload: { pattern: 'TODO' } }))!.summary
120
+ ).toBe('🔎 TODO');
121
+ });
122
+
123
+ it('formats a SubagentStatusMessage with a sigil', () => {
124
+ const msg: SubagentStatusMessage = {
125
+ id: '1',
126
+ role: 'subagent_status',
127
+ content: 'done',
128
+ timestamp: FIXED_TS,
129
+ sessionId: 's',
130
+ subagentId: 'sub-1',
131
+ status: 'completed',
132
+ };
133
+ expect(formatTurnLogEntry(msg)!.summary).toBe('✅ sub-1');
134
+
135
+ const failed = { ...msg, status: 'failed' as const };
136
+ expect(formatTurnLogEntry(failed)!.summary).toBe('❌ sub-1');
137
+ });
138
+
139
+ it('shortens UUID subagent ids but leaves human ids alone', () => {
140
+ const uuid = '5ea7b9ba-5103-40ee-95b6-c90808bbc431';
141
+ const status: SubagentStatusMessage = {
142
+ id: '1',
143
+ role: 'subagent_status',
144
+ content: '',
145
+ timestamp: FIXED_TS,
146
+ sessionId: 's',
147
+ subagentId: uuid,
148
+ status: 'completed',
149
+ };
150
+ expect(formatTurnLogEntry(status)!.summary).toBe('✅ 5ea7b9ba');
151
+
152
+ const human = { ...status, subagentId: 'hello-sub' };
153
+ expect(formatTurnLogEntry(human)!.summary).toBe('✅ hello-sub');
154
+ });
155
+
156
+ it('formats a PolicyRequestMessage with a status verb', () => {
157
+ const msg: PolicyRequestMessage = {
158
+ id: '1',
159
+ role: 'policy',
160
+ content: '',
161
+ timestamp: FIXED_TS,
162
+ sessionId: 's',
163
+ messageId: 'mid',
164
+ requestId: 'req',
165
+ commandName: 'rm',
166
+ args: ['-rf', '/tmp/cache'],
167
+ status: 'approved',
168
+ };
169
+ const entry = formatTurnLogEntry(msg)!;
170
+ expect(entry.kind).toBe('policy');
171
+ expect(entry.summary).toBe('policy approved: rm -rf /tmp/cache');
172
+ });
173
+
174
+ it('prefixes subagent-produced tool calls with the subagent marker and id', () => {
175
+ // A tool call emitted *inside* a subagent turn. The formatter flags it via
176
+ // `subagentId`; the renderer then prefixes the entry with 🤖 <short-id> so
177
+ // the reader knows which delegated turn produced the activity.
178
+ const parent = formatTurnLogEntry(makeTool({ name: 'Bash', payload: { command: 'ls' } }))!;
179
+ const child = formatTurnLogEntry(
180
+ makeTool({
181
+ name: 'Bash',
182
+ payload: { command: 'sleep 20' },
183
+ subagentId: 'sub-1',
184
+ })
185
+ )!;
186
+ // Run both through condenseTurnLog to observe the rendered form.
187
+ const rendered = condenseTurnLog([parent, child], { maxChars: 500 });
188
+ expect(rendered.kind).toBe('fits');
189
+ if (rendered.kind !== 'fits') return;
190
+ const [parentLine, childLine] = rendered.text.split('\n');
191
+ expect(parentLine).not.toContain('🤖');
192
+ expect(parentLine).toContain('🧑‍💻 ls');
193
+ expect(childLine).toContain('🤖 sub-1 🧑‍💻 sleep 20');
194
+ });
195
+
196
+ it('shortens UUID subagent ids in the marker prefix', () => {
197
+ const uuid = '5ea7b9ba-5103-40ee-95b6-c90808bbc431';
198
+ const entry = formatTurnLogEntry(
199
+ makeTool({
200
+ name: 'Bash',
201
+ payload: { command: 'ls' },
202
+ subagentId: uuid,
203
+ })
204
+ )!;
205
+ const rendered = condenseTurnLog([entry], { maxChars: 500 });
206
+ if (rendered.kind !== 'fits') throw new Error('expected fits');
207
+ expect(rendered.text).toContain('🤖 5ea7b9ba 🧑‍💻 ls');
208
+ expect(rendered.text).not.toContain(uuid);
209
+ });
210
+
211
+ it('does not mark subagent boundary events (prompt/reply/status)', () => {
212
+ // These kinds already name the subagent via 👉/👈/✅; a second marker is noise.
213
+ const prompt: UserMessage = {
214
+ id: 'p',
215
+ role: 'user',
216
+ content: 'do it',
217
+ timestamp: FIXED_TS,
218
+ sessionId: 's',
219
+ subagentId: 'sub-1',
220
+ };
221
+ const reply: AgentReplyMessage = {
222
+ id: 'r',
223
+ role: 'agent',
224
+ content: 'done',
225
+ timestamp: FIXED_TS,
226
+ sessionId: 's',
227
+ subagentId: 'sub-1',
228
+ };
229
+ const status: SubagentStatusMessage = {
230
+ id: 'st',
231
+ role: 'subagent_status',
232
+ content: '',
233
+ timestamp: FIXED_TS,
234
+ sessionId: 's',
235
+ subagentId: 'sub-1',
236
+ status: 'completed',
237
+ };
238
+ const entries = [prompt, reply, status].map((m) => formatTurnLogEntry(m)!);
239
+ const rendered = condenseTurnLog(entries, { maxChars: 500 });
240
+ if (rendered.kind !== 'fits') throw new Error('expected fits');
241
+ expect(rendered.text).not.toContain('🤖');
242
+ });
243
+
244
+ it('drops CommandLogMessage from the turn log', () => {
245
+ const msg: CommandLogMessage = {
246
+ id: '1',
247
+ role: 'command',
248
+ content: '',
249
+ timestamp: FIXED_TS,
250
+ sessionId: 's',
251
+ messageId: 'mid',
252
+ command: 'echo hi',
253
+ cwd: '/tmp',
254
+ stdout: '',
255
+ stderr: '',
256
+ exitCode: 0,
257
+ };
258
+ expect(formatTurnLogEntry(msg)).toBeNull();
259
+ });
260
+
261
+ it('formats a SystemMessage', () => {
262
+ const msg: SystemMessage = {
263
+ id: '1',
264
+ role: 'system',
265
+ content: 'stuff',
266
+ timestamp: FIXED_TS,
267
+ sessionId: 's',
268
+ event: 'cron',
269
+ };
270
+ const entry = formatTurnLogEntry(msg);
271
+ expect(entry!.kind).toBe('system');
272
+ expect(entry!.summary).toBe('cron: stuff');
273
+ });
274
+
275
+ it('drops subagent_update system messages (orchestration plumbing)', () => {
276
+ const msg: SystemMessage = {
277
+ id: '1',
278
+ role: 'system',
279
+ content: '<notification>Subagent x completed.</notification>',
280
+ timestamp: FIXED_TS,
281
+ sessionId: 's',
282
+ event: 'subagent_update',
283
+ };
284
+ expect(formatTurnLogEntry(msg)).toBeNull();
285
+ });
286
+
287
+ it('truncates tool principal-arg longer than maxToolPreview', () => {
288
+ const msg = makeTool({ name: 'Bash', payload: { command: 'x'.repeat(500) } });
289
+ const entry = formatTurnLogEntry(msg, { maxToolPreview: 100 })!;
290
+ expect(entry.summary).toContain('[truncated]');
291
+ expect(entry.summary.length).toBeLessThanOrEqual(200);
292
+ expect(entry.rawLength).toBe(500);
293
+ });
294
+
295
+ it('replaces newlines in tool principal-arg with spaces', () => {
296
+ const msg = makeTool({ name: 'Bash', payload: { command: 'line1\nline2\nline3' } });
297
+ const entry = formatTurnLogEntry(msg)!;
298
+ expect(entry.summary).not.toContain('\n');
299
+ expect(entry.summary).toContain('line1');
300
+ expect(entry.summary).toContain('line3');
301
+ });
302
+
303
+ it('renders relative timestamps when turnStartedAt is supplied', () => {
304
+ const start = '2026-04-20T12:04:02.000Z';
305
+ const make = (offsetMs: number): SubagentStatusMessage => ({
306
+ id: '1',
307
+ role: 'subagent_status',
308
+ content: '',
309
+ timestamp: new Date(new Date(start).getTime() + offsetMs).toISOString(),
310
+ sessionId: 's',
311
+ subagentId: 'sub-1',
312
+ status: 'completed',
313
+ });
314
+ expect(formatTurnLogEntry(make(0), { turnStartedAt: start })!.timestamp).toBe('0s');
315
+ expect(formatTurnLogEntry(make(5_000), { turnStartedAt: start })!.timestamp).toBe('5s');
316
+ expect(formatTurnLogEntry(make(60_000), { turnStartedAt: start })!.timestamp).toBe('1m');
317
+ expect(formatTurnLogEntry(make(125_000), { turnStartedAt: start })!.timestamp).toBe('2m5s');
318
+ });
319
+
320
+ it('falls back to wall-clock timestamps when turnStartedAt is omitted', () => {
321
+ const msg = makeTool({ payload: { file_path: 'x.md' } });
322
+ const entry = formatTurnLogEntry(msg)!;
323
+ expect(entry.timestamp).toMatch(/^\d{2}:\d{2}:\d{2}$/);
324
+ });
325
+ });
326
+
327
+ describe('buildTurnStartEntry', () => {
328
+ it('renders the opening line with a 0s timestamp and start emoji', () => {
329
+ const entry = buildTurnStartEntry();
330
+ expect(entry.timestamp).toBe('0s');
331
+ expect(entry.kind).toBe('system');
332
+ expect(entry.summary).toContain(TURN_START_EMOJI);
333
+ expect(entry.summary).toContain('Started processing');
334
+ expect(entry.subagentId).toBeUndefined();
335
+ });
336
+
337
+ it('renders cleanly through condenseTurnLog (no subagent marker)', () => {
338
+ const entry = buildTurnStartEntry();
339
+ const rendered = condenseTurnLog([entry], { maxChars: 500 });
340
+ expect(rendered.kind).toBe('fits');
341
+ if (rendered.kind !== 'fits') return;
342
+ expect(rendered.text).not.toContain('🤖');
343
+ expect(rendered.text).toContain('0s');
344
+ expect(rendered.text).toContain('Started processing');
345
+ });
346
+ });
347
+
348
+ function mkEntry(summary: string, overrides: Partial<TurnLogEntry> = {}): TurnLogEntry {
349
+ return {
350
+ timestamp: '12:04:02',
351
+ kind: 'tool',
352
+ summary,
353
+ rawLength: summary.length,
354
+ messageRole: 'tool',
355
+ ...overrides,
356
+ };
357
+ }
358
+
359
+ describe('condenseTurnLog', () => {
360
+ it('fits when under budget', () => {
361
+ const entries = [mkEntry('Read(small)'), mkEntry('Grep(small)')];
362
+ const result = condenseTurnLog(entries, { maxChars: 500 });
363
+ expect(result.kind).toBe('fits');
364
+ if (result.kind === 'fits') {
365
+ expect(result.text).toContain('Read');
366
+ expect(result.text).toContain('Grep');
367
+ }
368
+ });
369
+
370
+ it('rolls over when exceeded', () => {
371
+ const entries = Array.from({ length: 20 }, (_, i) => mkEntry('X'.repeat(40) + ` #${i}`));
372
+ const result = condenseTurnLog(entries, { maxChars: 200 });
373
+ expect(result.kind).toBe('rollover');
374
+ if (result.kind === 'rollover') {
375
+ expect(result.finalText).toContain('…log continues');
376
+ expect(result.carryEntries.length).toBeGreaterThan(0);
377
+ expect(result.carryEntries.length).toBeLessThan(entries.length);
378
+ }
379
+ });
380
+
381
+ it('is pure — does not mutate input', () => {
382
+ const entries = [mkEntry('one'), mkEntry('two')];
383
+ const copy = entries.map((e) => ({ ...e }));
384
+ const r1 = condenseTurnLog(entries, { maxChars: 500 });
385
+ const r2 = condenseTurnLog(entries, { maxChars: 500 });
386
+ expect(r1).toEqual(r2);
387
+ expect(entries).toEqual(copy);
388
+ });
389
+ });