clawmini 0.0.7 → 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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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 +42 -13
  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 -176
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
  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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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/DMtIqaiV.js +0 -2
  328. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  329. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  331. package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.oBhvQhcA.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/DMtIqaiV.js +0 -2
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.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,461 @@
1
+ # Adapter Visibility: Google Chat Threaded Activity Log (MVP)
2
+
3
+ ## Problem
4
+
5
+ The Google Chat adapter offers no visibility into what the agent is doing between a user's message and the final reply. Enabling `/show` surfaces intermediate activity as top-level messages, which buzzes the user's phone for every tool call and every subagent update. Disabling `/show` leaves the user staring at nothing until the final reply lands.
6
+
7
+ We want a lightweight, low-noise way to expose agent progress on demand, without turning every tool call and subagent event into a top-level message.
8
+
9
+ ## MVP scope
10
+
11
+ **In scope (this doc):**
12
+
13
+ - Google Chat only.
14
+ - One turn's tool / subagent / system activity accumulated into a single **activity-log message** posted inside a thread anchored on the triggering user message.
15
+ - The log message is **edited** as events arrive (not reposted) so only the first event in a turn buzzes.
16
+ - Per-space opt-out (threads on by default, can be disabled per space).
17
+ - End-to-end tests covering the core paths.
18
+
19
+ **Out of scope (see [Deferred](#deferred) at the bottom):**
20
+
21
+ - Discord adapter.
22
+ - Emoji reactions (any platform).
23
+ - Merged messages via `fetch-pending` / `/interrupt`.
24
+ - Proactive turns (cron / scheduled wake-ups) getting their own threads.
25
+ - Expand-on-demand full tool detail.
26
+
27
+ Top-level message flow (the final agent reply) is unchanged by this MVP.
28
+
29
+ ## Key constraint: edits, not new thread messages
30
+
31
+ Google Chat push-notifies thread participants (including the user who started the thread) on every new thread message, but **not** on edits. So the activity log inside the thread must be one message that we edit, not N separate messages. This shapes the whole design.
32
+
33
+ When a log message gets too long for a single GChat message (4096-char limit), we finalize it and post a new one to the same thread — accepting the one extra buzz as the cost of continued detail.
34
+
35
+ ## Concepts
36
+
37
+ ### Turn
38
+
39
+ One unit of agent work: a user message plus all agent / subagent / tool activity that follows, until the agent stops. A turn has a stable `turnId` assigned by the daemon when the user message starts executing.
40
+
41
+ ### Turn root
42
+
43
+ The adapter-visible user message the turn hangs off. In this MVP, every turn has exactly one root: the user message the daemon was handed via `executeDirectMessage`. Merged roots (adoption via `fetch-pending` / `/interrupt`) are deferred.
44
+
45
+ ### Activity log message
46
+
47
+ A single message inside the turn's thread that accumulates an event log (tool calls, subagent updates, system events) via edits. One buzz when it is first posted; zero buzzes for subsequent activity within that log message.
48
+
49
+ ## Data model changes
50
+
51
+ ### 1. `turnId` in the daemon
52
+
53
+ `BaseMessage` in `src/shared/chats.ts` does not currently carry any turn concept. Add:
54
+
55
+ - `turnId?: string` on `BaseMessage` — populated on every non-user message generated during a turn. Also populated on the triggering user message once the turn is formed.
56
+
57
+ Add lifecycle events to `src/daemon/events.ts`:
58
+
59
+ - `DAEMON_EVENT_TURN_STARTED` — emitted with `{ chatId, turnId, rootMessageId }` when `executeDirectMessage` begins agent work (not for pure no-op routes like `/stop`).
60
+ - `DAEMON_EVENT_TURN_ENDED` — emitted with `{ chatId, turnId, outcome: 'ok' | 'error' }` when the agent session's `handleMessage` promise settles.
61
+
62
+ Emission sits in `src/daemon/message.ts` around the `agentSession.handleMessage(finalMessage)` call at `message.ts:83`. Wrap with try/finally so `turnEnded` fires on both success and error paths. The `turnId` is a fresh UUID generated just before the call; the daemon stamps it onto every message logged during that call (via `createChatLogger` — which needs a `turnId` field threaded through, or can read it from an async-local).
63
+
64
+ Expose a new TRPC subscription in `src/daemon/api/user-router.ts` alongside `waitForMessages` and `waitForTyping`:
65
+
66
+ - `waitForTurns({ chatId, lastTurnCursor? })` — yields `{ type: 'started' | 'ended', turnId, rootMessageId?, outcome? }`.
67
+
68
+ Adapters use this subscription to drive thread creation and log finalization.
69
+
70
+ **Note:** `messagesAdopted` (for merged turns) is explicitly *not* added in the MVP. When we add it later, the shape of `TurnContext.rootAdapterMessageIds` already supports multiple roots.
71
+
72
+ ### 1a. `turnId` propagation through subagents
73
+
74
+ Subagents do not naturally inherit the parent's turn identity. Each subagent is its own `AgentSession` (`src/daemon/api/subagent-router.ts:28`), spawned via `executeSubagent` in `src/daemon/api/subagent-utils.ts`, which generates a *fresh* `messageId` (line 36) and routes through its own `executeDirectMessage` call. Without explicit propagation, every subagent spawn would start a new "turn" from the forwarder's perspective and fragment the activity log into multiple threads.
75
+
76
+ The MVP must propagate `turnId` end-to-end:
77
+
78
+ - **Spawn site** (`subagent-utils.ts`): accept the parent's `turnId` as input, attach it to the synthetic user message handed to the subagent's session, and thread it into the subagent's `createChatLogger` call so every message that subagent emits carries the same `turnId`.
79
+ - **Subagent spawn API** (`subagent-router.ts:subagentSpawn`): accept and forward `turnId` from the calling parent context. The parent agent's tool dispatch must have access to the current `turnId` (same plumbing as the parent's logger).
80
+ - **Nested subagents** (depth up to `MAX_SUBAGENT_DEPTH = 2`, `subagent-router.ts:13`): propagate the *root* `turnId`, not the immediate parent's session-level identity. A grandchild subagent's messages must carry the same `turnId` as the original user-triggered turn.
81
+ - **API-path log endpoints** that today generate fresh `messageId` UUIDs (`agent-router.ts:logToolMessage` line 119, `agent-policy-endpoints.ts` line 112): they must read `turnId` from the calling session's context and stamp it onto the logged message. These call sites are the highest-risk places to miss propagation, since they're invoked from inside subagent execution and currently have no link back to the spawning context.
82
+ - **`SubagentStatusMessage`** (`src/shared/chats.ts:70`) currently has no `messageId` field. Add `turnId` to this message type explicitly. It is emitted from inside the subagent's logger context (`subagent-utils.ts:81`), so once the logger carries `turnId`, this falls out for free.
83
+
84
+ Verification: a unit test that spawns a 2-level-deep subagent tree and asserts every message logged across all three sessions carries the same `turnId` as the originating user message. This test is the gate for step 1 of the implementation order — without it the threaded log will silently fragment in any turn that uses subagents (which is most non-trivial turns).
85
+
86
+ `AgentReplyMessage` from a subagent is internal to the parent agent's flow (returned as a tool result, not surfaced to the user), so its routing is unchanged — but it must still carry `turnId` so logging/debugging stays coherent.
87
+
88
+ ### 2. Capture the GChat `message.name` of the user's message
89
+
90
+ The GChat client currently discards the inbound user message's `message.name` after routing it to the daemon. For threading, we need two things from the inbound event: `message.name` (for thread anchoring) and `message.thread.name` (GChat's thread identifier — which may already exist if the user posted in a thread, or will be assigned once we open one).
91
+
92
+ Store on the existing `channelChatMap[space]` entry (`src/adapter-google-chat/state.ts:8–18`) — per chat, a small ring buffer of recent `{ daemonMessageId, gchatMessageName, gchatThreadName }`. This avoids needing a new adapter→daemon round-trip for `adapterMessageId` (previous design). Ring buffer size: last 50 entries per chat; older entries age out.
93
+
94
+ This keeps all the wiring inside the GChat adapter — no schema changes in the shared layer for message IDs.
95
+
96
+ ### 3. Capture the GChat `message.name` of each activity log message
97
+
98
+ When the forwarder posts the first thread-log message of a turn, it must remember the returned `message.name` (currently discarded at `forwarder.ts:238`) so subsequent events can edit it. Store on an in-memory `TurnContext`:
99
+
100
+ ```ts
101
+ type TurnContext = {
102
+ turnId: string;
103
+ chatId: string;
104
+ rootDaemonMessageId: string;
105
+ rootGchatMessageName: string; // spaces/XXX/messages/YYY of user message
106
+ gchatThreadName: string; // spaces/XXX/threads/ZZZ
107
+ activityLogMessageName?: string; // spaces/XXX/messages/WWW — current log message
108
+ entries: TurnLogEntry[]; // all entries since the current log message was opened
109
+ renderedEntryCount: number; // how many of `entries` are already reflected in the posted text
110
+ editTimer?: NodeJS.Timeout; // debounce handle
111
+ };
112
+ ```
113
+
114
+ Entries are the source of truth; the posted text is a *view* produced by the condenser (see [Formatting](#formatting)). Keeping the structured list — rather than a pre-rendered text buffer — is what lets the condenser try different strategies (drop earliest, re-truncate more aggressively, collapse runs) without the forwarder having to reconstruct history.
115
+
116
+ Keyed by `turnId`, held in a `Map` on the forwarder module. Deleted on `turnEnded` after the final flush completes.
117
+
118
+ ## Event routing
119
+
120
+ The forwarder already calls `shouldDisplayMessage()` (from `src/shared/adapters/filtering.ts`) which returns `boolean`. Extend that function to return a `Destination`:
121
+
122
+ ```ts
123
+ type Destination =
124
+ | { kind: 'drop' }
125
+ | { kind: 'top-level' }
126
+ | { kind: 'thread-log' }
127
+ | { kind: 'thread-message' }; // policy cards — needs its own message with a card
128
+ ```
129
+
130
+ The function still honors the same `filters` config (verbose / user / subagent_status); it just now also maps the *allowed* messages to a destination instead of a flat boolean.
131
+
132
+ Default routing for the MVP:
133
+
134
+ | Message role / event | Destination |
135
+ |---|---|
136
+ | `UserMessage` | `drop` (echoing user text back is not useful) |
137
+ | `AgentReplyMessage` (final reply) | `top-level` |
138
+ | `ToolMessage` | `thread-log` (truncated, see [Formatting](#formatting)) |
139
+ | `SubagentStatusMessage` | `thread-log` |
140
+ | `SystemMessage{event: 'subagent_update'}` | `thread-log` |
141
+ | `SystemMessage{event: 'cron'}` | `top-level` (unchanged; proactive-turn threading is deferred) |
142
+ | `SystemMessage{event: 'policy_approved' / 'policy_rejected'}` | `thread-log` |
143
+ | `PolicyRequestMessage` (pending) | `thread-message` (keeps current cardsV2 behavior; posts inside the thread) |
144
+ | `CommandLogMessage` | `thread-log` |
145
+ | `LegacyLogMessage` | existing behavior (respect `filters.verbose`) |
146
+
147
+ All defaults overridable by existing `filters` config. Per-space `visibility.threads: false` collapses `thread-log` and `thread-message` back to `top-level` (preserving current behavior for spaces that opt out).
148
+
149
+ ## Threading behavior
150
+
151
+ ### Thread anchoring
152
+
153
+ When the adapter receives the first `thread-log` event for a turn:
154
+
155
+ 1. Look up `TurnContext` by `turnId`. If absent (first event), build one:
156
+ - Resolve `rootGchatMessageName` + `gchatThreadName` from the ring buffer (populated when the user message came in).
157
+ - If the user message is not in the ring buffer (edge case: daemon turn started before adapter caught up), fall back to `top-level` for this turn and log a warning.
158
+ 2. Post the log message using `spaces.messages.create` with:
159
+ ```ts
160
+ {
161
+ parent: 'spaces/XXX',
162
+ requestBody: { text: formattedEvent, thread: { name: gchatThreadName } },
163
+ messageReplyOption: 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
164
+ }
165
+ ```
166
+ 3. Store the returned `message.name` as `activityLogMessageName`.
167
+
168
+ Subsequent `thread-log` events for the same turn:
169
+
170
+ 1. Run `formatTurnLogEntry(message)` and push the result onto `entries`.
171
+ 2. Schedule (or extend) a debounce timer — 1000ms coalescing window.
172
+ 3. When the timer fires, run `condenseTurnLog(entries, maxLogMessageChars)` to produce the text, then call `spaces.messages.update` with `updateMask: 'text'`.
173
+
174
+ ### Overflow handling
175
+
176
+ The condenser owns the decision of how to fit `entries` into `maxLogMessageChars`. It returns one of:
177
+
178
+ - `{ kind: 'fits', text }` — text fits in one message; post as-is.
179
+ - `{ kind: 'rollover', finalText, carryEntries }` — the current log message should be finalized with `finalText` (ending in `• …log continues`), and `carryEntries` should seed a fresh log message in the same thread.
180
+
181
+ On `rollover`: flush the final edit to `activityLogMessageName`, clear it, reset `entries` to `carryEntries` and `renderedEntryCount` to 0. The next scheduled flush posts a new `spaces.messages.create` under the same `gchatThreadName`.
182
+
183
+ Rollover is thus one possible condenser strategy, not a separate code path. A condenser that never returns `rollover` (e.g., one that aggressively drops earliest entries) keeps the whole turn in a single message; a condenser that always rolls over matches today's "fixed max, new message on overflow" behavior. See [Formatting](#formatting) for the strategies the MVP ships.
184
+
185
+ ### Thread-message (non-log) routing
186
+
187
+ `PolicyRequestMessage` currently posts as a cardsV2 message at the top level of the space. With threading enabled, route it to the turn's thread (same `thread.name`, same `messageReplyOption`). This buzzes once; acceptable because the user has to act on it. The existing plain-text fallback path (`forwarder.ts:157` — when cardsV2 fails) uses the same thread.
188
+
189
+ ### `turnEnded` handling
190
+
191
+ On `turnEnded`:
192
+
193
+ 1. Flush any pending edit immediately (cancel the debounce, send now).
194
+ 2. Drop the `TurnContext` from the map.
195
+
196
+ The final agent reply is routed `top-level` (not into the thread) so the user sees it as a normal reply in the space, same as today.
197
+
198
+ ## DM behavior
199
+
200
+ Google Chat DMs (`space.type === 'DIRECT_MESSAGE'` — detected today at `client.ts:318`) do not support threading the same way spaces do. In the MVP:
201
+
202
+ - Detect the DM at turn start (the `channelChatMap` entry already tells us the space).
203
+ - For DMs, force destination `thread-log` → `drop`, `thread-message` → `top-level`. This preserves today's DM behavior exactly; activity-log visibility is a spaces-only feature in the MVP.
204
+ - Document this clearly in the config — users who DM the bot will not see threaded activity.
205
+
206
+ A later iteration could flatten the log into a collapsible summary line sent top-level at turn end; out of scope now.
207
+
208
+ ## Formatting
209
+
210
+ Formatting the turn log is split into two pure functions with distinct responsibilities. Keeping them separate is what lets us try different condensation strategies (aggressive per-entry truncation, dropping earliest entries, collapsing runs of similar events) without rewriting the per-message formatter.
211
+
212
+ Both functions live in a new module `src/shared/adapters/turn-log.ts` so Discord and any future adapter can reuse them.
213
+
214
+ ### 1. `formatTurnLogEntry(message: ChatMessage): TurnLogEntry | null`
215
+
216
+ Pure function. Takes a single `ChatMessage` and returns a structured entry, or `null` if the message is not part of the log (e.g., `UserMessage`, `AgentReplyMessage`, filtered-out by role).
217
+
218
+ ```ts
219
+ type TurnLogEntry = {
220
+ timestamp: string; // HH:MM:SS, daemon-local
221
+ kind: 'tool' | 'subagent' | 'policy' | 'system' | 'command';
222
+ summary: string; // one-line rendered form, already per-entry truncated
223
+ rawLength: number; // untruncated content length — lets the condenser decide how much to re-cut
224
+ subagentId?: string; // for indentation/grouping in future formatters
225
+ messageRole: string; // for telemetry / debugging
226
+ };
227
+ ```
228
+
229
+ Example output (the `summary` field), one line per entry:
230
+
231
+ ```
232
+ • 12:04:02 tool: Read(src/app.ts)
233
+ • 12:04:04 tool: Grep("TODO") — 7 matches
234
+ • 12:04:08 subagent: Explore started
235
+ • 12:04:41 subagent: Explore done (14s)
236
+ • 12:05:02 policy: approved rm -rf /tmp/cache
237
+ ```
238
+
239
+ The formatter applies `maxToolPreview` (default 400 chars) to tool content, replaces embedded newlines with spaces, and appends `…[truncated]` when it cuts. It does **not** know about the overall message length budget — that's the condenser's job. Timestamp is wall-clock in the daemon's local timezone, `HH:MM:SS`; no date, since the thread anchors the day.
240
+
241
+ ### 2. `condenseTurnLog(entries: TurnLogEntry[], opts): CondenseResult`
242
+
243
+ Pure function. Takes the full list of entries and a max-length budget and decides how to fit within a single GChat message (4096 chars, minus safety margin). Returns either a text that fits, or a rollover signal (see [Overflow handling](#overflow-handling)).
244
+
245
+ ```ts
246
+ type CondenseOpts = {
247
+ maxChars: number; // default 3500
248
+ strategy: 'rollover' | 'drop-earliest' | 'aggressive-truncate' | 'hybrid';
249
+ };
250
+
251
+ type CondenseResult =
252
+ | { kind: 'fits'; text: string }
253
+ | { kind: 'rollover'; finalText: string; carryEntries: TurnLogEntry[] };
254
+ ```
255
+
256
+ MVP ships multiple strategies behind a config flag so we can A/B them without code changes:
257
+
258
+ | Strategy | Behavior | Tradeoff |
259
+ |---|---|---|
260
+ | `rollover` (default) | Accumulate entries; when total exceeds `maxChars`, emit the full current text plus a `• …log continues` marker as `finalText`, carry any overflow entries into a fresh log message. | Preserves all detail; costs one extra buzz per rollover (each new log message in the thread notifies). |
261
+ | `drop-earliest` | Keep the most recent entries that fit; prepend `• …N earlier entries dropped` when anything was cut. | Zero buzzes past the first, but early context (which tool started a chain) is lost. |
262
+ | `aggressive-truncate` | First pass uses `formatTurnLogEntry` output. If overflowing, re-truncate entries in place (shorter per-entry caps: 400 → 200 → 100) until it fits. | Keeps all entries visible; loses per-entry detail. Deterministic ordering. |
263
+ | `hybrid` | Aggressive-truncate first; if still over budget, drop earliest. Rollover only if even an empty-except-latest message would overflow (pathological). | Best of both; more code to maintain. |
264
+
265
+ The condenser is the *only* place that reads `maxChars`. `formatTurnLogEntry` is oblivious to the budget.
266
+
267
+ ### Testability
268
+
269
+ Because both functions are pure and operate on plain data, they get unit-tested independently of the forwarder, the debounce machinery, and the GChat API fakes. The forwarder's E2E tests only need to verify that the right function is called at the right time — not exercise every condensation branch. This keeps the condensation strategy A/B safe to iterate on.
270
+
271
+ ## Configuration
272
+
273
+ Per-adapter config in `src/adapter-google-chat/config.ts`:
274
+
275
+ ```ts
276
+ visibility: {
277
+ threads: boolean; // default true; global kill switch
278
+ threadLog: {
279
+ maxToolPreview: number; // default 400 — passed to formatTurnLogEntry
280
+ maxLogMessageChars: number; // default 3500 — passed to condenseTurnLog
281
+ editDebounceMs: number; // default 1000
282
+ condenseStrategy: 'rollover' | 'drop-earliest' | 'aggressive-truncate' | 'hybrid';
283
+ // default 'rollover' — see Formatting
284
+ };
285
+ };
286
+ ```
287
+
288
+ Per-space override on the existing `channelChatMap[spaceName]` entry in `state.ts`:
289
+
290
+ ```ts
291
+ channelChatMap: {
292
+ [spaceName]: {
293
+ // …existing fields…
294
+ threadsDisabled?: boolean; // per-space opt-out
295
+ }
296
+ }
297
+ ```
298
+
299
+ Resolution order: per-space `threadsDisabled === true` wins; otherwise use global `visibility.threads`. A space admin can set `threadsDisabled` via a new slash command (not in MVP — set via config file edit or TRPC for now; slash command is trivial follow-up).
300
+
301
+ ## Error handling
302
+
303
+ Failures the forwarder handles explicitly:
304
+
305
+ - **Thread creation fails** (first `spaces.messages.create` of a turn returns an error): log the error, fall back to `top-level` for this turn's remaining thread-log events, keep the `TurnContext` in a "degraded" state so we don't re-attempt on every event.
306
+ - **Edit fails** (transient error on `spaces.messages.update`): retry once after 500ms. If still failing, finalize the current log message (log a warning in the last successful edit's content) and start a fresh one on the next event.
307
+ - **Log message deleted by a user**: GChat returns 404 on edit. Treat as a finalize event: open a new log message on the next event.
308
+ - **Turn ended with no `thread-log` events**: no thread was ever opened; nothing to clean up. Drop the `TurnContext`.
309
+
310
+ ## Known limitations (called out in docs)
311
+
312
+ - **Public threads:** GChat threads in a multi-person space are visible to every human in the space. Tool output containing file paths, stack traces, or anything the user wouldn't have posted publicly is exposed. `threadsDisabled` per space is the mitigation.
313
+ - **Thread interleaving:** humans can reply in the thread alongside the activity log. Our edits only touch our own message; human replies are preserved and will appear interleaved. Acceptable.
314
+ - **Very long turns:** produce a growing thread (many rolled-over log messages). No hard cap; thread is scrollable.
315
+ - **Daemon restart mid-turn:** `turnEnded` never fires; the activity log message is left in its last-edited state. Self-heals: the next turn starts a new thread. No recovery logic in MVP.
316
+ - **Adapter restart mid-turn:** in-memory `TurnContext` is lost; same outcome as daemon restart. The next event for that turn can't find the log message, so it opens a new one (visible as two log messages in the thread). Acceptable in MVP.
317
+
318
+ ## Implementation order
319
+
320
+ 1. **Daemon turn lifecycle.** Add `turnId` to `BaseMessage` (and to `SubagentStatusMessage`, which lacks `messageId` today); thread it through `createChatLogger`; emit `DAEMON_EVENT_TURN_STARTED` / `DAEMON_EVENT_TURN_ENDED` in `src/daemon/message.ts` around `agentSession.handleMessage`; add `waitForTurns` TRPC subscription. **Critical:** propagate `turnId` through `subagentSpawn` → `executeSubagent` → the subagent's `executeDirectMessage` and logger, recursively for nested subagents (see [§1a](#1a-turnid-propagation-through-subagents)). Also fix the API-path log endpoints (`agent-router.ts:logToolMessage`, `agent-policy-endpoints.ts`) that currently generate fresh `messageId` UUIDs — they must stamp the calling session's `turnId`. No adapter changes yet — verify via unit tests that (a) `turnId` appears on parent-agent messages and (b) a 2-level-deep subagent tree propagates the root `turnId` to every emitted message.
321
+ 2. **GChat inbound: capture `message.name` + `thread.name`.** Update `client.ts` message-handling paths to push onto the per-chat ring buffer in state before routing to the daemon.
322
+ 3. **`Destination` routing.** Change `shouldDisplayMessage()` in `src/shared/adapters/filtering.ts` to return `Destination`. Keep existing callers (Discord forwarder) working by mapping `{ kind: 'drop' }` → `false` and everything else → `true` via a tiny shim until Discord's turn to migrate.
323
+ 4. **Forwarder: `TurnContext` + thread posting.** Subscribe to `waitForTurns` alongside `waitForMessages`. On first `thread-log` event, post threaded message; on subsequent events, coalesce + edit with debounce. On `turnEnded`, flush + clean up.
324
+ 5. **Thread-message routing.** Thread policy-request cards into the same thread as the turn's activity log.
325
+ 6. **Per-space config + DM fallback.** Honor `threadsDisabled` and DM detection.
326
+ 7. **E2E tests.** See [E2E test plan](#e2e-test-plan).
327
+
328
+ Each step merges independently; after step 4 the feature is functional behind config; steps 5–6 are polish.
329
+
330
+ ## E2E test plan
331
+
332
+ All tests extend the existing fixtures in `e2e/adapters/_google-chat-fixtures.ts` (`makeFakeChatApi`, `runForwarder`, `useGoogleChatAdapterEnv`, `seedChatForForwarderCatchup`) — no new harness needed. The fake Chat API already records `create` and `update` calls; we add assertions against their `thread` field, `messageReplyOption`, and `updateMask`.
333
+
334
+ New file: **`e2e/adapters/adapter-google-chat-threads.test.ts`**. Tests:
335
+
336
+ ### Core happy path
337
+
338
+ 1. **`opens a thread anchored on the user's message for the first thread-log event`**
339
+ Seed a chat with a user message (capture its synthetic `message.name` in state); inject a `ToolMessage` with `turnId=T1` while `waitForTurns` has just emitted `started`. Assert `create` was called with `thread: { name: <user thread name> }` and `messageReplyOption: 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD'`, and that the returned `message.name` is retained for the next event.
340
+
341
+ 2. **`edits the same log message on subsequent thread-log events`**
342
+ Inject a second `ToolMessage` on the same `turnId=T1`. Wait past the debounce window. Assert exactly one `update` call was made with `name` = first log message's name, `updateMask: 'text'`, and `requestBody.text` containing both events.
343
+
344
+ 3. **`coalesces bursts of thread-log events into a single edit`**
345
+ Inject three `ToolMessage`s within the debounce window (1000ms). Assert one `update` call, not three, and the final text contains all three events in order.
346
+
347
+ 4. **`routes the final AgentReplyMessage to top-level, not the thread`**
348
+ After thread-log activity, inject an `AgentReplyMessage` with the same `turnId`. Assert the create call for the reply has no `thread` field (or uses the space's root, not the turn thread).
349
+
350
+ 5. **`flushes pending edits on turnEnded`**
351
+ Inject a `ToolMessage` then immediately emit `turnEnded` (before the debounce fires). Assert one `update` landed with the buffered event.
352
+
353
+ ### Routing & filters
354
+
355
+ 6. **`drops UserMessage regardless of filters`**
356
+ Inject a `UserMessage` with `turnId`. Assert no `create` or `update` call was made for it.
357
+
358
+ 7. **`routes PolicyRequestMessage into the turn thread with its cardsV2 payload`**
359
+ After opening a thread via a ToolMessage, inject a pending `PolicyRequestMessage` with the same `turnId`. Assert the cardsV2 create call included `thread: { name: <thread> }` and `messageReplyOption: 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD'`.
360
+
361
+ 8. **`honors existing filters config (verbose off hides subagent_update)`**
362
+ With `filters: { verbose: false }`, inject `SystemMessage{event: 'subagent_update'}`. Assert no create/update was issued.
363
+
364
+ ### Formatter & condenser (unit tests)
365
+
366
+ These live next to the pure functions in `src/shared/adapters/turn-log.test.ts`, not in the E2E suite — they don't touch the forwarder.
367
+
368
+ - **`formatTurnLogEntry` returns null for UserMessage / AgentReplyMessage`**, and a structured entry for ToolMessage / SubagentStatusMessage / PolicyRequestMessage / CommandLogMessage / applicable SystemMessages.
369
+ - **`formatTurnLogEntry` truncates tool content longer than maxToolPreview`** — assert `…[truncated]` suffix, line length ≤ budget.
370
+ - **`formatTurnLogEntry` replaces newlines in tool content with spaces`** — prevents multi-line entries from visually breaking the log.
371
+ - **`condenseTurnLog(strategy: rollover) fits when under budget`** — returns `{ kind: 'fits', text }`.
372
+ - **`condenseTurnLog(strategy: rollover) rolls over when exceeded`** — returns `{ kind: 'rollover' }` with `finalText` ending in `…log continues` and `carryEntries` equal to the overflow tail.
373
+ - **`condenseTurnLog(strategy: drop-earliest) drops oldest entries and prepends a count marker`** — asserts `• …N earlier entries dropped` line and that only latest entries remain.
374
+ - **`condenseTurnLog(strategy: aggressive-truncate) shortens per-entry summaries to fit`** — inject entries whose full-length form overflows; assert output fits and `rawLength > summary.length` for the truncated entries.
375
+ - **`condenseTurnLog(strategy: hybrid) truncates first, then drops`** — budget tight enough that truncation alone can't fit; assert truncation applied *and* earliest entries dropped.
376
+ - **`condenseTurnLog is pure`** — call twice with identical inputs, assert identical output and no mutation of `entries`.
377
+
378
+ ### Roll-over & length (E2E)
379
+
380
+ 9. **`rolls over to a new thread-log message under the default rollover strategy`**
381
+ Configure `maxLogMessageChars: 200, condenseStrategy: 'rollover'`. Inject ToolMessages whose formatted lines exceed 200 chars total. Assert two separate `create` calls inside the same thread, the first ends with the `…log continues` marker, the second begins with the carry-over entries.
382
+
383
+ 10. **`drop-earliest strategy keeps one message and drops old entries`**
384
+ Configure `maxLogMessageChars: 200, condenseStrategy: 'drop-earliest'`. Inject enough ToolMessages to overflow multiple times. Assert exactly one `create` and N `update`s (no rollover), and the final text starts with `• …N earlier entries dropped`.
385
+
386
+ 11. **`aggressive-truncate strategy keeps all entries with shortened summaries`**
387
+ Configure `maxLogMessageChars: 300, condenseStrategy: 'aggressive-truncate'`. Inject several ToolMessages with long content. Assert one `create`, text fits under 300 chars, contains one line per injected entry, and entries show `…[truncated]` markers.
388
+
389
+ ### Config & DM
390
+
391
+ 12. **`falls back to top-level when threadsDisabled is set on the space`**
392
+ Mark the space as `threadsDisabled: true`. Inject a ToolMessage with a turnId. Assert create has no `thread` field — activity is posted at top-level just like today.
393
+
394
+ 13. **`falls back to top-level when visibility.threads is false globally`**
395
+ Same as above but via global config.
396
+
397
+ 14. **`DM spaces never open a thread`**
398
+ Seed a DM space (`singleUserBotDm: true`). Inject a ToolMessage. Assert the thread-log event was dropped entirely (not posted anywhere); only final AgentReplyMessage appears at top-level.
399
+
400
+ ### Error handling
401
+
402
+ 15. **`falls back to top-level when thread-log create fails`**
403
+ Mock `create` to reject on the first thread-log post. Assert the error is logged, subsequent thread-log events in the same turn post top-level (not retry thread creation), and `turnEnded` cleans up state.
404
+
405
+ 16. **`finalizes and re-creates on edit failure`**
406
+ Create succeeds; mock `update` to reject twice in a row. Assert that after the retry fails, the next thread-log event posts a fresh log message in the same thread (new `create`), not another edit.
407
+
408
+ 17. **`recovers from a 404 on edit by opening a new log message`**
409
+ Mock `update` to reject with a 404-shaped error. Assert the next event opens a new log message rather than editing the missing one.
410
+
411
+ ### Lifecycle & state
412
+
413
+ 18. **`cleans up TurnContext on turnEnded`**
414
+ After `turnEnded`, inject a new turn with the same chatId. Assert the new turn opens its own thread (does not reuse the prior turn's log message).
415
+
416
+ 19. **`handles turnEnded with no thread-log events as a no-op`**
417
+ Emit `turnStarted` then `turnEnded` with only an AgentReplyMessage between them. Assert no thread was opened and no errors surfaced.
418
+
419
+ ### Subagent propagation
420
+
421
+ 20. **`groups parent-agent and subagent activity into one thread`**
422
+ Within a single `turnId=T1`, inject a parent `ToolMessage`, then a `SubagentStatusMessage` (started), then a `ToolMessage` emitted from inside the subagent (carrying the same `turnId=T1` and a `subagentId`), then a `SubagentStatusMessage` (done). Assert exactly one thread was opened and all four events appear in the same activity log message.
423
+
424
+ 21. **`propagates turnId through nested subagents`**
425
+ Inject events from a 2-level-deep subagent tree (parent + child + grandchild), all carrying `turnId=T1`. Assert all events land in the same thread and same activity log message — no fragmentation across spawn boundaries.
426
+
427
+ 22. **`activity from a subagent without turnId does not open a second thread`**
428
+ Regression guard: if a subagent message somehow arrives without a `turnId` (propagation bug), assert the forwarder logs a warning and routes it `top-level` rather than opening an unrelated thread. Prevents silent fragmentation.
429
+
430
+ All tests must run with `npm run validate` green before merge.
431
+
432
+ ## Deferred
433
+
434
+ Preserved from the prior spec; implemented after the MVP ships and users have lived on it for a bit.
435
+
436
+ ### Discord adapter
437
+
438
+ Same shape (threaded activity log, edit-coalescing). Discord has a richer API (`Message.startThread`, `ThreadChannel.send`, `Message.edit` with `MessageFlags.SuppressNotifications`) but also a 5-edits-per-5-seconds rate limit — the same debounce logic applies. Discord DMs don't support threads at all, so the DM fallback is more prominent there.
439
+
440
+ ### Emoji reactions
441
+
442
+ One reaction per turn root (👀 queued / 🤔 thinking / 🔧 tool running / ✅ done / ❌ error / 🔁 superseded), with atomic swap. This is purely additive — it does not touch the thread-log flow.
443
+
444
+ ### Merged messages via `fetch-pending` / `/interrupt`
445
+
446
+ Add `messagesAdopted(turnId, messageIds[])` event; generalize `TurnContext.rootGchatMessageName` → `rootGchatMessageNames[]`; pick the anchor (first root vs latest root) based on UX testing. The MVP's single-root design doesn't prevent this — it's additive.
447
+
448
+ ### Proactive turns (cron / wake-ups)
449
+
450
+ Add `showJobNotifications: 'none' | 'top-level'` config; when `'top-level'`, the adapter posts a synthetic `🕒 Cron: <name>` top-level message, registers it as the turn's root, and the same threaded activity log hangs off it. Plumbing already in place — just add the synthetic-root path.
451
+
452
+ ### Expand-on-demand
453
+
454
+ User reacts ➕ on a log entry to replace it with the full detail (useful when a tool output was truncated). Requires per-line message IDs inside the log (we don't have them today; entries are lines in one message) or separate log messages per entry — neither is worth it in the MVP.
455
+
456
+ ## Open questions
457
+
458
+ 1. **Turn ID plumbing:** easiest to thread `turnId` through `createChatLogger` (logger is already per-chat, per-session; adding per-turn is a small step) vs. async-local-storage. Leaning toward explicit parameter for testability.
459
+ 2. **Thread anchor when the user posted inside an existing thread:** if the user's message is itself a reply in some existing thread, do we post the activity log in *that* thread or a new one anchored on the user message? Current plan: reuse the existing thread (honor the user's choice to be in a thread). Confirm with a real GChat test.
460
+ 3. **`threadsDisabled` surface:** MVP requires editing state file directly. Post-MVP: `/threads off` slash command? Per-user preference in DMs (moot since DMs don't thread)?
461
+ 4. **Final-reply-in-thread option:** some users may prefer the final reply *also* goes in the thread (clean up space noise). Config option for later; MVP keeps final reply top-level.