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,1078 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ getTRPCClient,
4
+ startGoogleChatIngestion,
5
+ } from '../../src/adapter-google-chat/client.js';
6
+ import { updateGoogleChatState } from '../../src/adapter-google-chat/state.js';
7
+ import { getSocketPath } from '../../src/shared/workspace.js';
8
+ import {
9
+ BASE_CONFIG,
10
+ makeDmMessage,
11
+ makeFakeChatApi,
12
+ makeFakeSubscription,
13
+ makePubsubMessage,
14
+ runForwarder,
15
+ useGoogleChatAdapterEnv,
16
+ } from './_google-chat-fixtures.js';
17
+
18
+ /**
19
+ * E2E tests for the threaded activity log. We exercise the full inbound path
20
+ * (pubsub → client → daemon → forwarder) so the GChat `message.name` gets
21
+ * correlated to the daemon turn via `externalRef`, just like in production.
22
+ *
23
+ * `command` messages are dropped from the turn log, so a plain-echo agent
24
+ * wouldn't produce any thread-log content. These tests drive the `debug-agent`
25
+ * with a `clawmini-lite.js subagents spawn` inbound — the emitted
26
+ * `subagent_status` events route to `thread-log` and give each turn a real
27
+ * entry to anchor on.
28
+ */
29
+ const SPAWN_COMMAND = (id: string) =>
30
+ `clawmini-lite.js subagents spawn --id ${id} --async "echo x"`;
31
+ function makeThreadedMessage(opts: {
32
+ space: string;
33
+ messageId: string;
34
+ threadName: string;
35
+ text: string;
36
+ }) {
37
+ return makePubsubMessage({
38
+ type: 'MESSAGE',
39
+ space: { name: opts.space, type: 'SPACE' },
40
+ user: { email: 'user@example.com' },
41
+ message: {
42
+ name: `${opts.space}/messages/${opts.messageId}`,
43
+ thread: { name: opts.threadName },
44
+ text: opts.text,
45
+ },
46
+ });
47
+ }
48
+
49
+ describe('Google Chat Adapter E2E — threaded activity log', () => {
50
+ const envRef = useGoogleChatAdapterEnv('e2e-google-chat-threads', { subagents: true });
51
+
52
+ it('opens a thread anchored on the user thread and edits the log on subsequent events', async () => {
53
+ const { env } = envRef;
54
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
55
+ const subscription = makeFakeSubscription();
56
+ const { api, create } = makeFakeChatApi();
57
+
58
+ create.mockImplementation(
59
+ async () => ({ data: { name: 'spaces/thr/messages/log-1' } }) as unknown as object
60
+ );
61
+
62
+ await updateGoogleChatState(
63
+ { channelChatMap: { 'spaces/thr': { chatId: 'gc-threads' } } },
64
+ env.e2eDir
65
+ );
66
+ await env.addChat('gc-threads', 'debug-agent');
67
+
68
+ startGoogleChatIngestion(
69
+ BASE_CONFIG,
70
+ trpc,
71
+ {},
72
+ { subscription, chatApi: api, startDir: env.e2eDir }
73
+ );
74
+
75
+ const config = { ...BASE_CONFIG, chatId: 'gc-threads' };
76
+
77
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
78
+ subscription.emitMessage(
79
+ makeThreadedMessage({
80
+ space: 'spaces/thr',
81
+ messageId: 'u1',
82
+ threadName: 'spaces/thr/threads/t1',
83
+ text: SPAWN_COMMAND('t1-sub'),
84
+ })
85
+ );
86
+
87
+ // Main agent's reply lands at top-level; the subagent's status events
88
+ // land in the thread-log anchored on t1.
89
+ await vi.waitFor(
90
+ () => {
91
+ const reply = create.mock.calls.find(
92
+ ([p]) =>
93
+ typeof p.requestBody.text === 'string' &&
94
+ p.requestBody.text.includes('Subagent spawned successfully with ID: t1-sub') &&
95
+ !('thread' in p.requestBody)
96
+ );
97
+ expect(reply).toBeDefined();
98
+ },
99
+ { timeout: 15000 }
100
+ );
101
+
102
+ await vi.waitFor(
103
+ () => {
104
+ const threaded = create.mock.calls.find(
105
+ ([p]) =>
106
+ (p.requestBody as { thread?: { name?: string } }).thread?.name ===
107
+ 'spaces/thr/threads/t1'
108
+ );
109
+ expect(threaded).toBeDefined();
110
+ },
111
+ { timeout: 15000 }
112
+ );
113
+
114
+ const threaded = create.mock.calls.find(
115
+ ([p]) =>
116
+ (p.requestBody as { thread?: { name?: string } }).thread?.name ===
117
+ 'spaces/thr/threads/t1'
118
+ )![0];
119
+ expect(threaded.parent).toBe('spaces/thr');
120
+ expect(
121
+ (threaded as unknown as { messageReplyOption?: string }).messageReplyOption
122
+ ).toBe('REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD');
123
+ });
124
+ }, 60000);
125
+
126
+ it('routes the final agent reply to top-level, not the thread', async () => {
127
+ const { env } = envRef;
128
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
129
+ const subscription = makeFakeSubscription();
130
+ const { api, create } = makeFakeChatApi();
131
+
132
+ create.mockImplementation(
133
+ async () => ({ data: { name: 'spaces/thr2/messages/log-1' } }) as unknown as object
134
+ );
135
+
136
+ await updateGoogleChatState(
137
+ { channelChatMap: { 'spaces/thr2': { chatId: 'gc-threads-2' } } },
138
+ env.e2eDir
139
+ );
140
+ await env.addChat('gc-threads-2');
141
+
142
+ startGoogleChatIngestion(
143
+ BASE_CONFIG,
144
+ trpc,
145
+ {},
146
+ { subscription, chatApi: api, startDir: env.e2eDir }
147
+ );
148
+
149
+ const config = { ...BASE_CONFIG, chatId: 'gc-threads-2' };
150
+
151
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
152
+ subscription.emitMessage(
153
+ makeThreadedMessage({
154
+ space: 'spaces/thr2',
155
+ messageId: 'u1',
156
+ threadName: 'spaces/thr2/threads/t2',
157
+ text: 'final reply payload',
158
+ })
159
+ );
160
+
161
+ await vi.waitFor(
162
+ () => {
163
+ const reply = create.mock.calls.find(
164
+ ([p]) =>
165
+ p.requestBody.text === 'final reply payload' && !('thread' in p.requestBody)
166
+ );
167
+ expect(reply).toBeDefined();
168
+ },
169
+ { timeout: 15000 }
170
+ );
171
+ });
172
+ }, 45000);
173
+
174
+ it('anchors the activity log on the triggering message, not an earlier slash command', async () => {
175
+ const { env } = envRef;
176
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
177
+ const subscription = makeFakeSubscription();
178
+ const { api, create } = makeFakeChatApi();
179
+
180
+ create.mockImplementation(
181
+ async () => ({ data: { name: 'spaces/newcmd/messages/log-1' } }) as unknown as object
182
+ );
183
+
184
+ await updateGoogleChatState(
185
+ { channelChatMap: { 'spaces/newcmd': { chatId: 'gc-newcmd' } } },
186
+ env.e2eDir
187
+ );
188
+ await env.addChat('gc-newcmd');
189
+
190
+ // Register /new as a router so that a bare `/new` message does not spawn
191
+ // an agent turn — mirrors real deployments where /new resets the session
192
+ // and returns an automatic reply with no agent work. The chat's agent is
193
+ // `debug-agent` so the real turn can spawn a subagent (producing the
194
+ // subagent_status events that actually anchor the thread log).
195
+ env.writeChatSettings('gc-newcmd', {
196
+ routers: ['@clawmini/slash-new'],
197
+ defaultAgent: 'debug-agent',
198
+ });
199
+
200
+ startGoogleChatIngestion(
201
+ BASE_CONFIG,
202
+ trpc,
203
+ {},
204
+ { subscription, chatApi: api, startDir: env.e2eDir }
205
+ );
206
+
207
+ const config = { ...BASE_CONFIG, chatId: 'gc-newcmd' };
208
+
209
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
210
+ // First inbound: `/new`. Does not trigger a turn.
211
+ subscription.emitMessage(
212
+ makeThreadedMessage({
213
+ space: 'spaces/newcmd',
214
+ messageId: 'slash',
215
+ threadName: 'spaces/newcmd/threads/slash-thread',
216
+ text: '/new',
217
+ })
218
+ );
219
+
220
+ // Second inbound: the real message. This is the one that triggers a
221
+ // turn and whose thread should anchor the activity log.
222
+ subscription.emitMessage(
223
+ makeThreadedMessage({
224
+ space: 'spaces/newcmd',
225
+ messageId: 'real',
226
+ threadName: 'spaces/newcmd/threads/real-thread',
227
+ text: SPAWN_COMMAND('newcmd-sub'),
228
+ })
229
+ );
230
+
231
+ await vi.waitFor(
232
+ () => {
233
+ const threaded = create.mock.calls.find(
234
+ ([p]) =>
235
+ (p.requestBody as { thread?: { name?: string } }).thread?.name ===
236
+ 'spaces/newcmd/threads/real-thread'
237
+ );
238
+ expect(threaded).toBeDefined();
239
+ },
240
+ // The subagent spawn takes a few seconds to fully round-trip through
241
+ // the daemon; 15s was tight enough to be flaky under full-suite load.
242
+ { timeout: 45000 }
243
+ );
244
+
245
+ // Crucially: nothing should anchor to the /new thread.
246
+ const anchoredOnSlash = create.mock.calls.find(
247
+ ([p]) =>
248
+ (p.requestBody as { thread?: { name?: string } }).thread?.name ===
249
+ 'spaces/newcmd/threads/slash-thread'
250
+ );
251
+ expect(anchoredOnSlash).toBeUndefined();
252
+ });
253
+ }, 60000);
254
+
255
+ it('drops thread-log activity entirely when threadsDisabled is set on the space', async () => {
256
+ const { env } = envRef;
257
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
258
+ const subscription = makeFakeSubscription();
259
+ const { api, create } = makeFakeChatApi();
260
+
261
+ await updateGoogleChatState(
262
+ {
263
+ channelChatMap: {
264
+ 'spaces/noth': { chatId: 'gc-noth', threadsDisabled: true },
265
+ },
266
+ },
267
+ env.e2eDir
268
+ );
269
+ await env.addChat('gc-noth');
270
+
271
+ startGoogleChatIngestion(
272
+ BASE_CONFIG,
273
+ trpc,
274
+ {},
275
+ { subscription, chatApi: api, startDir: env.e2eDir }
276
+ );
277
+
278
+ const config = { ...BASE_CONFIG, chatId: 'gc-noth' };
279
+
280
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
281
+ subscription.emitMessage(
282
+ makeThreadedMessage({
283
+ space: 'spaces/noth',
284
+ messageId: 'u1',
285
+ threadName: 'spaces/noth/threads/tn',
286
+ text: 'threads off',
287
+ })
288
+ );
289
+
290
+ await vi.waitFor(
291
+ () => {
292
+ expect(
293
+ create.mock.calls.find(
294
+ ([p]) =>
295
+ p.requestBody.text === 'threads off' && !('thread' in p.requestBody)
296
+ )
297
+ ).toBeDefined();
298
+ },
299
+ { timeout: 15000 }
300
+ );
301
+
302
+ const threadedCalls = create.mock.calls.filter(
303
+ ([p]) =>
304
+ p.parent === 'spaces/noth' &&
305
+ (p.requestBody as { thread?: unknown }).thread !== undefined
306
+ );
307
+ expect(threadedCalls).toHaveLength(0);
308
+ });
309
+ }, 45000);
310
+
311
+ it('DM spaces thread activity onto the user message, same as group spaces', async () => {
312
+ const { env } = envRef;
313
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
314
+ const subscription = makeFakeSubscription();
315
+ const { api, create } = makeFakeChatApi();
316
+
317
+ create.mockImplementation(
318
+ async () => ({ data: { name: 'spaces/dmsp/messages/log-1' } }) as unknown as object
319
+ );
320
+
321
+ await updateGoogleChatState(
322
+ { channelChatMap: { 'spaces/dmsp': { chatId: 'gc-dm' } } },
323
+ env.e2eDir
324
+ );
325
+ await env.addChat('gc-dm', 'debug-agent');
326
+
327
+ startGoogleChatIngestion(
328
+ BASE_CONFIG,
329
+ trpc,
330
+ {},
331
+ { subscription, chatApi: api, startDir: env.e2eDir }
332
+ );
333
+
334
+ const config = { ...BASE_CONFIG, chatId: 'gc-dm' };
335
+
336
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
337
+ // DM messages carry a thread.name even though DMs have no UI thread —
338
+ // GChat uses it as a reply anchor.
339
+ const dm = makeDmMessage({
340
+ space: 'spaces/dmsp',
341
+ messageId: 'u1',
342
+ text: SPAWN_COMMAND('dm-sub'),
343
+ });
344
+ // Inject a thread on the DM payload since makeDmMessage doesn't set one.
345
+ const parsed = JSON.parse(dm.data.toString('utf8'));
346
+ parsed.message.thread = { name: 'spaces/dmsp/threads/td' };
347
+ dm.data = Buffer.from(JSON.stringify(parsed));
348
+ subscription.emitMessage(dm);
349
+
350
+ await vi.waitFor(
351
+ () => {
352
+ expect(
353
+ create.mock.calls.find(
354
+ ([p]) =>
355
+ typeof p.requestBody.text === 'string' &&
356
+ p.requestBody.text.includes('Subagent spawned successfully with ID: dm-sub') &&
357
+ !('thread' in p.requestBody)
358
+ )
359
+ ).toBeDefined();
360
+ },
361
+ { timeout: 15000 }
362
+ );
363
+
364
+ await vi.waitFor(
365
+ () => {
366
+ const threaded = create.mock.calls.find(
367
+ ([p]) =>
368
+ (p.requestBody as { thread?: { name?: string } }).thread?.name ===
369
+ 'spaces/dmsp/threads/td'
370
+ );
371
+ expect(threaded).toBeDefined();
372
+ },
373
+ { timeout: 15000 }
374
+ );
375
+ });
376
+ }, 60000);
377
+
378
+ it('falls back to top-level when visibility.threads is disabled globally', async () => {
379
+ const { env } = envRef;
380
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
381
+ const subscription = makeFakeSubscription();
382
+ const { api, create } = makeFakeChatApi();
383
+
384
+ await updateGoogleChatState(
385
+ { channelChatMap: { 'spaces/gth': { chatId: 'gc-globalthr' } } },
386
+ env.e2eDir
387
+ );
388
+ await env.addChat('gc-globalthr');
389
+
390
+ startGoogleChatIngestion(
391
+ BASE_CONFIG,
392
+ trpc,
393
+ {},
394
+ { subscription, chatApi: api, startDir: env.e2eDir }
395
+ );
396
+
397
+ const config = {
398
+ ...BASE_CONFIG,
399
+ chatId: 'gc-globalthr',
400
+ visibility: { threads: false as const },
401
+ };
402
+
403
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
404
+ subscription.emitMessage(
405
+ makeThreadedMessage({
406
+ space: 'spaces/gth',
407
+ messageId: 'u1',
408
+ threadName: 'spaces/gth/threads/tg',
409
+ text: 'global off',
410
+ })
411
+ );
412
+
413
+ await vi.waitFor(
414
+ () => {
415
+ expect(
416
+ create.mock.calls.find(
417
+ ([p]) =>
418
+ p.requestBody.text === 'global off' && !('thread' in p.requestBody)
419
+ )
420
+ ).toBeDefined();
421
+ },
422
+ { timeout: 15000 }
423
+ );
424
+
425
+ const threaded = create.mock.calls.filter(
426
+ ([p]) => (p.requestBody as { thread?: unknown }).thread !== undefined
427
+ );
428
+ expect(threaded).toHaveLength(0);
429
+ });
430
+ }, 45000);
431
+
432
+ it('renders the turn log for a debug-agent subagent spawn', async () => {
433
+ const { env } = envRef;
434
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
435
+ const subscription = makeFakeSubscription();
436
+ const { api, create, update } = makeFakeChatApi();
437
+
438
+ // One activity-log message per turn; the forwarder opens it with `create`
439
+ // and then appends via `update` calls. Returning a stable name keeps the
440
+ // snapshot deterministic across runs.
441
+ create.mockImplementation(
442
+ async () => ({ data: { name: 'spaces/snap/messages/log-1' } }) as unknown as object
443
+ );
444
+
445
+ await updateGoogleChatState(
446
+ { channelChatMap: { 'spaces/snap': { chatId: 'gc-snap' } } },
447
+ env.e2eDir
448
+ );
449
+ await env.addChat('gc-snap', 'debug-agent');
450
+
451
+ startGoogleChatIngestion(
452
+ BASE_CONFIG,
453
+ trpc,
454
+ {},
455
+ { subscription, chatApi: api, startDir: env.e2eDir }
456
+ );
457
+
458
+ const config = { ...BASE_CONFIG, chatId: 'gc-snap' };
459
+
460
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
461
+ subscription.emitMessage(
462
+ makeThreadedMessage({
463
+ space: 'spaces/snap',
464
+ messageId: 'u1',
465
+ threadName: 'spaces/snap/threads/t1',
466
+ text: 'clawmini-lite.js subagents spawn --id hello-sub --async "sleep 5 && echo hello"',
467
+ })
468
+ );
469
+
470
+ // `✅ hello-sub` is the last status entry that lands in the activity
471
+ // log, so waiting for it in an `update` payload guarantees the final
472
+ // debounced flush has fired.
473
+ await vi.waitFor(
474
+ () => {
475
+ const last = [...update.mock.calls]
476
+ .reverse()
477
+ .find(([p]) => p.name === 'spaces/snap/messages/log-1');
478
+ expect(last).toBeDefined();
479
+ const text = (last![0].requestBody as { text?: string }).text ?? '';
480
+ expect(text).toMatch(/✅ hello-sub/);
481
+ },
482
+ { timeout: 45000, interval: 500 }
483
+ );
484
+ });
485
+
486
+ const lastUpdate = [...update.mock.calls]
487
+ .reverse()
488
+ .find(([p]) => p.name === 'spaces/snap/messages/log-1')!;
489
+ const rawText = (lastUpdate[0].requestBody as { text?: string }).text ?? '';
490
+ // Relative timestamps depend on wall-clock scheduling (sleep 5 + flush
491
+ // debounce drift); normalize to a placeholder for snapshot stability.
492
+ const normalized = rawText
493
+ .replace(/^• (?:\d+m)?\d+[ms]/gm, '• Δs')
494
+ .replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
495
+
496
+ expect(normalized).toMatchInlineSnapshot(`
497
+ "• Δs ▶️ Started processing…
498
+ • Δs 👉 hello-sub: sleep 5 && echo hello
499
+ • Δs 👈 hello-sub: [DEBUG] sleep 5 && echo hello: \`\`\` hello \`\`\`
500
+ • Δs ✅ hello-sub"
501
+ `);
502
+ }, 120000);
503
+
504
+ /**
505
+ * Summarize the interleaved sequence of `create` / `update` calls for a
506
+ * snapshot. Strips wall-clock dependent bits (relative timestamps, temp-dir
507
+ * paths) so the expected output is stable run-to-run.
508
+ */
509
+ function transcribe(
510
+ create: ReturnType<typeof makeFakeChatApi>['create'],
511
+ update: ReturnType<typeof makeFakeChatApi>['update']
512
+ ): string {
513
+ const events = [
514
+ ...create.mock.calls.map((c, i) => ({
515
+ kind: 'create' as const,
516
+ order: create.mock.invocationCallOrder[i]!,
517
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
518
+ call: c[0] as any,
519
+ })),
520
+ ...update.mock.calls.map((c, i) => ({
521
+ kind: 'update' as const,
522
+ order: update.mock.invocationCallOrder[i]!,
523
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
524
+ call: c[0] as any,
525
+ })),
526
+ ].sort((a, b) => a.order - b.order);
527
+
528
+ const normalize = (text: string) =>
529
+ (text ?? '')
530
+ .replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
531
+ .replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
532
+
533
+ const lines: string[] = [];
534
+ for (const ev of events) {
535
+ if (ev.kind === 'create') {
536
+ const thread: string | undefined = ev.call.requestBody?.thread?.name;
537
+ const text = normalize(ev.call.requestBody?.text ?? '');
538
+ const card = Array.isArray(ev.call.requestBody?.cardsV2)
539
+ ? ev.call.requestBody.cardsV2.length > 0
540
+ : false;
541
+ const where = thread ? `thread=${thread}` : 'top-level';
542
+ const body = card && !text ? '<card>' : text;
543
+ lines.push(`CREATE ${where}\n ${body.replace(/\n/g, '\n ')}`);
544
+ } else {
545
+ const text = normalize(ev.call.requestBody?.text ?? '');
546
+ lines.push(`UPDATE name=${ev.call.name}\n ${text.replace(/\n/g, '\n ')}`);
547
+ }
548
+ }
549
+ return lines.join('\n---\n');
550
+ }
551
+
552
+ /**
553
+ * Wait for the `✅ <subagentId>` status entry to land in any posted
554
+ * activity-log content — signals that the final debounced flush for that
555
+ * subagent has fired. Checks every create/update (not just the most
556
+ * recent), since rollover may land the ✅ in a later threaded `create`
557
+ * rather than an `update`.
558
+ */
559
+ async function waitForSubagentComplete(
560
+ update: ReturnType<typeof makeFakeChatApi>['update'],
561
+ create: ReturnType<typeof makeFakeChatApi>['create'],
562
+ subagentId: string,
563
+ timeout = 45000
564
+ ): Promise<void> {
565
+ const needle = `✅ ${subagentId}`;
566
+ await vi.waitFor(
567
+ () => {
568
+ const allTexts = [
569
+ ...create.mock.calls.map((c) => (c[0].requestBody as { text?: string }).text ?? ''),
570
+ ...update.mock.calls.map((c) => (c[0].requestBody as { text?: string }).text ?? ''),
571
+ ];
572
+ expect(allTexts.some((t) => t.includes(needle))).toBe(true);
573
+ },
574
+ { timeout, interval: 500 }
575
+ );
576
+ }
577
+
578
+ it('rolls over into a new threaded log message when content exceeds maxLogMessageChars', async () => {
579
+ const { env } = envRef;
580
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
581
+ const subscription = makeFakeSubscription();
582
+ const { api, create, update } = makeFakeChatApi();
583
+
584
+ // Distinct names per open so the transcript clearly shows rollover
585
+ // opening a fresh log-N inside the same thread.
586
+ let logCount = 0;
587
+ create.mockImplementation(
588
+ async () => ({ data: { name: `spaces/roll/messages/log-${++logCount}` } }) as unknown as object
589
+ );
590
+
591
+ await updateGoogleChatState(
592
+ { channelChatMap: { 'spaces/roll': { chatId: 'gc-roll' } } },
593
+ env.e2eDir
594
+ );
595
+ await env.addChat('gc-roll', 'debug-agent');
596
+
597
+ startGoogleChatIngestion(
598
+ BASE_CONFIG,
599
+ trpc,
600
+ {},
601
+ { subscription, chatApi: api, startDir: env.e2eDir }
602
+ );
603
+
604
+ // Budget of 80 chars forces rollover after ~1-2 entries.
605
+ const config = {
606
+ ...BASE_CONFIG,
607
+ chatId: 'gc-roll',
608
+ visibility: {
609
+ threads: true,
610
+ threadLog: { maxLogMessageChars: 80, editDebounceMs: 100 },
611
+ },
612
+ };
613
+
614
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
615
+ subscription.emitMessage(
616
+ makeThreadedMessage({
617
+ space: 'spaces/roll',
618
+ messageId: 'u1',
619
+ threadName: 'spaces/roll/threads/t1',
620
+ text: 'clawmini-lite.js subagents spawn --id roll-sub --async "echo roll-output"',
621
+ })
622
+ );
623
+ await waitForSubagentComplete(update, create, 'roll-sub');
624
+ });
625
+
626
+ // All threaded creates land in the same thread. There should be multiple
627
+ // of them because the 80-char budget cannot hold all three status entries
628
+ // in one message — rollover opens new log-N messages in the same thread.
629
+ const threadedCreates = create.mock.calls.filter(([p]) =>
630
+ Boolean((p.requestBody as { thread?: { name?: string } }).thread)
631
+ );
632
+ expect(threadedCreates.length).toBeGreaterThanOrEqual(2);
633
+ for (const call of threadedCreates) {
634
+ expect((call[0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
635
+ 'spaces/roll/threads/t1'
636
+ );
637
+ }
638
+
639
+ // The combined posted content (including edits) both names every stage of
640
+ // the subagent's lifecycle AND carries the `…log continues` marker,
641
+ // evidence that at least one rollover happened.
642
+ const allText = [
643
+ ...create.mock.calls.map((c) => (c[0].requestBody.text ?? '') as string),
644
+ ...update.mock.calls.map((c) => (c[0].requestBody.text ?? '') as string),
645
+ ].join('\n');
646
+ expect(allText).toContain('…log continues');
647
+ expect(allText).toContain('👉 roll-sub');
648
+ expect(allText).toContain('👈 roll-sub');
649
+ expect(allText).toContain('✅ roll-sub');
650
+ }, 120000);
651
+
652
+ it('drops thread-log events for the rest of the turn when thread creation fails', async () => {
653
+ const { env } = envRef;
654
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
655
+ const subscription = makeFakeSubscription();
656
+ const { api, create, update } = makeFakeChatApi();
657
+
658
+ // First threaded create fails; any later create (e.g. the agent's final
659
+ // top-level reply) succeeds.
660
+ let firstThreadedAttempt = true;
661
+ create.mockImplementation(async (params) => {
662
+ const threaded = Boolean((params.requestBody as { thread?: { name?: string } }).thread);
663
+ if (threaded && firstThreadedAttempt) {
664
+ firstThreadedAttempt = false;
665
+ throw new Error('GChat 503 — thread open failed');
666
+ }
667
+ return { data: { name: `spaces/deg/messages/${Math.random().toString(36).slice(2, 8)}` } };
668
+ });
669
+
670
+ await updateGoogleChatState(
671
+ { channelChatMap: { 'spaces/deg': { chatId: 'gc-deg' } } },
672
+ env.e2eDir
673
+ );
674
+ await env.addChat('gc-deg', 'debug-agent');
675
+
676
+ startGoogleChatIngestion(
677
+ BASE_CONFIG,
678
+ trpc,
679
+ {},
680
+ { subscription, chatApi: api, startDir: env.e2eDir }
681
+ );
682
+
683
+ const config = { ...BASE_CONFIG, chatId: 'gc-deg' };
684
+
685
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
686
+ subscription.emitMessage(
687
+ makeThreadedMessage({
688
+ space: 'spaces/deg',
689
+ messageId: 'u1',
690
+ threadName: 'spaces/deg/threads/t1',
691
+ text: 'clawmini-lite.js subagents spawn --id deg-sub --async "echo hi"',
692
+ })
693
+ );
694
+
695
+ // The agent's final reply is a top-level post and still lands even
696
+ // after the thread-log was abandoned. Wait for any non-threaded
697
+ // create to observe the turn progressing past the failure.
698
+ await vi.waitFor(
699
+ () => {
700
+ const topLevelCalls = create.mock.calls.filter(
701
+ ([p]) => !('thread' in (p.requestBody as { thread?: unknown }))
702
+ );
703
+ expect(topLevelCalls.length).toBeGreaterThan(0);
704
+ },
705
+ { timeout: 45000, interval: 500 }
706
+ );
707
+ });
708
+
709
+ // Exactly one failed thread-open attempt; no retries.
710
+ const threadedCreates = create.mock.calls.filter(([p]) =>
711
+ Boolean((p.requestBody as { thread?: { name?: string } }).thread)
712
+ );
713
+ expect(threadedCreates).toHaveLength(1);
714
+
715
+ // No updates — the log message was never successfully opened.
716
+ expect(update.mock.calls).toHaveLength(0);
717
+
718
+ // Thread-log activity (subagent markers) must NOT leak into top-level
719
+ // posts: once thread-open fails we drop the rest of the turn's log.
720
+ const topLevelText = create.mock.calls
721
+ .filter(([p]) => !('thread' in (p.requestBody as { thread?: unknown })))
722
+ .map(([p]) => (p.requestBody.text ?? '') as string)
723
+ .join('\n');
724
+ expect(topLevelText).not.toContain('👉 deg-sub');
725
+ expect(topLevelText).not.toContain('✅ deg-sub');
726
+ }, 120000);
727
+
728
+ it('coalesces a multi-subagent turn into a single threaded log message', async () => {
729
+ const { env } = envRef;
730
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
731
+ const subscription = makeFakeSubscription();
732
+ const { api, create, update } = makeFakeChatApi();
733
+
734
+ create.mockImplementation(
735
+ async () => ({ data: { name: 'spaces/multi/messages/log-1' } }) as unknown as object
736
+ );
737
+
738
+ await updateGoogleChatState(
739
+ { channelChatMap: { 'spaces/multi': { chatId: 'gc-multi' } } },
740
+ env.e2eDir
741
+ );
742
+ await env.addChat('gc-multi', 'debug-agent');
743
+
744
+ startGoogleChatIngestion(
745
+ BASE_CONFIG,
746
+ trpc,
747
+ {},
748
+ { subscription, chatApi: api, startDir: env.e2eDir }
749
+ );
750
+
751
+ const config = { ...BASE_CONFIG, chatId: 'gc-multi' };
752
+
753
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
754
+ // The parent agent's single eval spawns two async subagents. Both
755
+ // produce prompt/reply/status entries; the forwarder must fold them
756
+ // into the same thread-log message.
757
+ subscription.emitMessage(
758
+ makeThreadedMessage({
759
+ space: 'spaces/multi',
760
+ messageId: 'u1',
761
+ threadName: 'spaces/multi/threads/t1',
762
+ text:
763
+ 'clawmini-lite.js subagents spawn --id multi-a --async "echo A" && ' +
764
+ 'clawmini-lite.js subagents spawn --id multi-b --async "echo B"',
765
+ })
766
+ );
767
+
768
+ await waitForSubagentComplete(update, create, 'multi-a');
769
+ await waitForSubagentComplete(update, create, 'multi-b');
770
+ });
771
+
772
+ // Exactly one threaded `create` (the log is opened once and edited).
773
+ const threadedCreates = create.mock.calls.filter(([p]) =>
774
+ Boolean((p.requestBody as { thread?: { name?: string } }).thread)
775
+ );
776
+ expect(threadedCreates).toHaveLength(1);
777
+ expect((threadedCreates[0]![0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
778
+ 'spaces/multi/threads/t1'
779
+ );
780
+
781
+ // Coalescing: the number of `update` calls is far below the number of
782
+ // logged entries (we never post one edit per entry). Both subagents
783
+ // produce 3 entries each = 6; we expect the update count to be small
784
+ // (<=6). With a debounce of ~1s and two fast echoes, all entries often
785
+ // arrive before the first flush fires — the content ends up in the
786
+ // create payload with zero updates. Either way the final text should
787
+ // name both subagents end-to-end.
788
+ expect(update.mock.calls.length).toBeLessThanOrEqual(8);
789
+
790
+ const lastLogWrite =
791
+ [...update.mock.calls].reverse().find(([p]) => p.name === 'spaces/multi/messages/log-1') ??
792
+ threadedCreates[0]!;
793
+ const rawText = (lastLogWrite[0].requestBody.text ?? '') as string;
794
+ const normalized = rawText
795
+ .replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
796
+ .replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
797
+
798
+ // Both subagents are spawned with `--async`, so they run in parallel and
799
+ // their events can interleave under load. Assert each lifecycle is
800
+ // present and per-subagent order is preserved (👉 → 👈 → ✅), without
801
+ // pinning the relative order between the two.
802
+ const indexOf = (needle: string): number => {
803
+ const i = normalized.indexOf(needle);
804
+ expect(i, `expected log to contain ${JSON.stringify(needle)}`).toBeGreaterThanOrEqual(0);
805
+ return i;
806
+ };
807
+ const startedIdx = indexOf('▶️ Started processing…');
808
+ const aPromptIdx = indexOf('👉 multi-a: echo A');
809
+ const aReplyIdx = indexOf('👈 multi-a: [DEBUG] echo A: ``` A ```');
810
+ const aDoneIdx = indexOf('✅ multi-a');
811
+ const bPromptIdx = indexOf('👉 multi-b: echo B');
812
+ const bReplyIdx = indexOf('👈 multi-b: [DEBUG] echo B: ``` B ```');
813
+ const bDoneIdx = indexOf('✅ multi-b');
814
+
815
+ expect(startedIdx).toBeLessThan(aPromptIdx);
816
+ expect(startedIdx).toBeLessThan(bPromptIdx);
817
+ expect(aPromptIdx).toBeLessThan(aReplyIdx);
818
+ expect(aReplyIdx).toBeLessThan(aDoneIdx);
819
+ expect(bPromptIdx).toBeLessThan(bReplyIdx);
820
+ expect(bReplyIdx).toBeLessThan(bDoneIdx);
821
+ }, 120000);
822
+
823
+ it('snapshots the interleaved create/update transcript for a successful turn', async () => {
824
+ // End-to-end visibility: one snapshot showing exactly what a GChat client
825
+ // would see when a subagent runs — the thread open, the series of edits
826
+ // as events arrive, and the top-level final reply. `transcribe()` keeps
827
+ // ordering stable across coalescing changes.
828
+ const { env } = envRef;
829
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
830
+ const subscription = makeFakeSubscription();
831
+ const { api, create, update } = makeFakeChatApi();
832
+
833
+ create.mockImplementation(
834
+ async () => ({ data: { name: 'spaces/txn/messages/log-1' } }) as unknown as object
835
+ );
836
+
837
+ await updateGoogleChatState(
838
+ { channelChatMap: { 'spaces/txn': { chatId: 'gc-txn' } } },
839
+ env.e2eDir
840
+ );
841
+ await env.addChat('gc-txn', 'debug-agent');
842
+
843
+ startGoogleChatIngestion(
844
+ BASE_CONFIG,
845
+ trpc,
846
+ {},
847
+ { subscription, chatApi: api, startDir: env.e2eDir }
848
+ );
849
+
850
+ // Large editDebounceMs forces all activity to collapse into as few
851
+ // updates as possible, keeping the transcript short and deterministic.
852
+ const config = {
853
+ ...BASE_CONFIG,
854
+ chatId: 'gc-txn',
855
+ visibility: {
856
+ threads: true,
857
+ threadLog: { editDebounceMs: 2000 },
858
+ },
859
+ };
860
+
861
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
862
+ subscription.emitMessage(
863
+ makeThreadedMessage({
864
+ space: 'spaces/txn',
865
+ messageId: 'u1',
866
+ threadName: 'spaces/txn/threads/t1',
867
+ text: 'clawmini-lite.js subagents spawn --id txn-sub --async "echo done"',
868
+ })
869
+ );
870
+ await waitForSubagentComplete(update, create, 'txn-sub');
871
+ });
872
+
873
+ // The exact number of intermediate updates is allowed to vary (it depends
874
+ // on how debounce windows line up with subagent events); the important
875
+ // guarantees are: (1) first threaded post opens log-1 in t1, (2) every
876
+ // subsequent threaded touch edits log-1, (3) the final top-level reply
877
+ // names the subagent.
878
+ const threadedCreates = create.mock.calls.filter(([p]) =>
879
+ Boolean((p.requestBody as { thread?: { name?: string } }).thread)
880
+ );
881
+ expect(threadedCreates).toHaveLength(1);
882
+ expect((threadedCreates[0]![0].requestBody as { thread?: { name?: string } }).thread?.name).toBe(
883
+ 'spaces/txn/threads/t1'
884
+ );
885
+ for (const u of update.mock.calls) {
886
+ expect(u[0].name).toBe('spaces/txn/messages/log-1');
887
+ }
888
+
889
+ const lastUpdate = [...update.mock.calls].reverse()[0];
890
+ const lastCreate = threadedCreates[0]![0];
891
+ const finalLog =
892
+ ((lastUpdate?.[0].requestBody.text as string) ??
893
+ (lastCreate.requestBody.text as string) ??
894
+ '')
895
+ .replace(/• (?:\d+m)?\d+[ms]/g, '• Δs')
896
+ .replace(/\/clawmini-e2e-google-chat-threads-[^/\s"]+/g, '/CLAWMINI_DIR');
897
+ expect(finalLog).toMatchInlineSnapshot(`
898
+ "• Δs ▶️ Started processing…
899
+ • Δs 👉 txn-sub: echo done
900
+ • Δs 👈 txn-sub: [DEBUG] echo done: \`\`\` done \`\`\`
901
+ • Δs ✅ txn-sub"
902
+ `);
903
+
904
+ // Reference the transcript helper so it stays part of the compiled test
905
+ // surface even if a future run doesn't need its full output.
906
+ const transcript = transcribe(create, update);
907
+ expect(transcript).toContain('CREATE thread=spaces/txn/threads/t1');
908
+ expect(transcript).toContain('✅ txn-sub');
909
+ }, 120000);
910
+
911
+ /**
912
+ * Stand up a session-timeout cron that spawns a subagent. Returns the
913
+ * `create`/`update` mocks and the threaded-creates helper. Shared between
914
+ * the silent-mode and header-mode tests, which differ only in the
915
+ * `visibility.jobs` config they install.
916
+ */
917
+ async function runCronScenario(opts: {
918
+ chatId: string;
919
+ space: string;
920
+ jobsMode?: 'silent' | 'header';
921
+ }) {
922
+ const { env } = envRef;
923
+ const trpc = getTRPCClient({ socketPath: getSocketPath(env.e2eDir) });
924
+ const subscription = makeFakeSubscription();
925
+ const { api, create, update } = makeFakeChatApi();
926
+
927
+ let threadCounter = 0;
928
+ let msgCounter = 0;
929
+ create.mockImplementation(async (params) => {
930
+ msgCounter++;
931
+ const msgName = `${opts.space}/messages/msg-${msgCounter}`;
932
+ const isThreaded = Boolean(
933
+ (params.requestBody as { thread?: { name?: string } }).thread
934
+ );
935
+ if (isThreaded) {
936
+ return {
937
+ data: {
938
+ name: msgName,
939
+ thread: (params.requestBody as { thread?: { name?: string } }).thread,
940
+ },
941
+ };
942
+ }
943
+ threadCounter++;
944
+ return {
945
+ data: {
946
+ name: msgName,
947
+ thread: { name: `${opts.space}/threads/auto-${threadCounter}` },
948
+ },
949
+ };
950
+ });
951
+
952
+ await updateGoogleChatState(
953
+ { channelChatMap: { [opts.space]: { chatId: opts.chatId } } },
954
+ env.e2eDir
955
+ );
956
+ await env.addChat(opts.chatId, 'debug-agent');
957
+
958
+ const cronPrompt =
959
+ 'clawmini-lite.js subagents spawn --id cron-sub --async "echo session-ended"';
960
+ env.writeChatSettings(opts.chatId, {
961
+ defaultAgent: 'debug-agent',
962
+ routers: [{ use: '@clawmini/session-timeout', with: { timeout: '3s', prompt: cronPrompt } }],
963
+ });
964
+
965
+ startGoogleChatIngestion(
966
+ BASE_CONFIG,
967
+ trpc,
968
+ {},
969
+ { subscription, chatApi: api, startDir: env.e2eDir }
970
+ );
971
+
972
+ const config: typeof BASE_CONFIG = {
973
+ ...BASE_CONFIG,
974
+ chatId: opts.chatId,
975
+ ...(opts.jobsMode ? { visibility: { jobs: opts.jobsMode } } : {}),
976
+ };
977
+
978
+ await runForwarder({ trpc, chatApi: api, startDir: env.e2eDir, config }, async () => {
979
+ subscription.emitMessage(
980
+ makeThreadedMessage({
981
+ space: opts.space,
982
+ messageId: 'kick',
983
+ threadName: `${opts.space}/threads/kick`,
984
+ text: 'hello',
985
+ })
986
+ );
987
+ await waitForSubagentComplete(update, create, 'cron-sub');
988
+ });
989
+
990
+ const kickThread = `${opts.space}/threads/kick`;
991
+ const cronThreadedCreates = create.mock.calls
992
+ .filter(([p]) => Boolean((p.requestBody as { thread?: { name?: string } }).thread))
993
+ .filter(([p]) => {
994
+ const thread = (p.requestBody as { thread?: { name?: string } }).thread!.name!;
995
+ return thread !== kickThread;
996
+ });
997
+
998
+ return { create, update, cronThreadedCreates };
999
+ }
1000
+
1001
+ it('silent mode (default): cron prompt stays hidden; activity anchors on the agent reply', async () => {
1002
+ // Default `visibility.jobs: 'silent'` drops the cron SystemMessage. The
1003
+ // agent's auto-reply ("Starting a fresh session…") becomes the first
1004
+ // top-level post and therefore the thread anchor for the cron turn's
1005
+ // subagent activity.
1006
+ const { create, cronThreadedCreates } = await runCronScenario({
1007
+ chatId: 'gc-cron-silent',
1008
+ space: 'spaces/cron-silent',
1009
+ });
1010
+
1011
+ // No `[SYSTEM]`-prefixed post: the cron prompt is never surfaced.
1012
+ const sysPosts = create.mock.calls.filter(
1013
+ ([p]) =>
1014
+ typeof p.requestBody.text === 'string' &&
1015
+ (p.requestBody.text as string).startsWith('[SYSTEM] ')
1016
+ );
1017
+ expect(sysPosts).toHaveLength(0);
1018
+
1019
+ // The auto-reply lands top-level and seeds the cron turn's thread.
1020
+ const replyPost = create.mock.calls.find(
1021
+ ([p]) =>
1022
+ typeof p.requestBody.text === 'string' &&
1023
+ (p.requestBody.text as string).includes('Starting a fresh session') &&
1024
+ !('thread' in (p.requestBody as { thread?: unknown }))
1025
+ );
1026
+ expect(replyPost).toBeDefined();
1027
+
1028
+ // All thread-log activity for the cron turn anchors on exactly one thread
1029
+ // (the one auto-created by the reply post), not on the prior user turn.
1030
+ expect(cronThreadedCreates.length).toBeGreaterThanOrEqual(1);
1031
+ const anchored = new Set(
1032
+ cronThreadedCreates.map(
1033
+ ([p]) => (p.requestBody as { thread?: { name?: string } }).thread!.name!
1034
+ )
1035
+ );
1036
+ expect(anchored.size).toBe(1);
1037
+ expect([...anchored][0]).toMatch(/\/threads\/auto-/);
1038
+ }, 60000);
1039
+
1040
+ it('header mode: posts 🕒 <jobId> as the anchor for cron activity', async () => {
1041
+ const { create, cronThreadedCreates } = await runCronScenario({
1042
+ chatId: 'gc-cron-header',
1043
+ space: 'spaces/cron-header',
1044
+ jobsMode: 'header',
1045
+ });
1046
+
1047
+ // The cron heartbeat posts top-level with a 🕒 prefix and carries the
1048
+ // job id, not the (potentially sensitive) prompt text.
1049
+ const headerPost = create.mock.calls.find(
1050
+ ([p]) =>
1051
+ typeof p.requestBody.text === 'string' &&
1052
+ (p.requestBody.text as string).startsWith('🕒 ') &&
1053
+ !('thread' in (p.requestBody as { thread?: unknown }))
1054
+ );
1055
+ expect(headerPost).toBeDefined();
1056
+ const headerText = headerPost![0].requestBody.text as string;
1057
+ expect(headerText).toContain('__session_timeout__');
1058
+ expect(headerText).not.toContain('clawmini-lite.js');
1059
+
1060
+ // And no `[SYSTEM] `-prefixed top-level post was generated.
1061
+ const sysPosts = create.mock.calls.filter(
1062
+ ([p]) =>
1063
+ typeof p.requestBody.text === 'string' &&
1064
+ (p.requestBody.text as string).startsWith('[SYSTEM] ')
1065
+ );
1066
+ expect(sysPosts).toHaveLength(0);
1067
+
1068
+ // Subagent activity threads under the header's auto-created thread.
1069
+ expect(cronThreadedCreates.length).toBeGreaterThanOrEqual(1);
1070
+ const anchored = new Set(
1071
+ cronThreadedCreates.map(
1072
+ ([p]) => (p.requestBody as { thread?: { name?: string } }).thread!.name!
1073
+ )
1074
+ );
1075
+ expect(anchored.size).toBe(1);
1076
+ expect([...anchored][0]).toMatch(/\/threads\/auto-/);
1077
+ }, 60000);
1078
+ });