thursday-agent 0.3.0 → 0.5.0

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 (253) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/server/app/api/artifact/route.js +4 -4
  18. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  19. package/.next/server/app/api/bot/memory/files/route.js +3 -3
  20. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/task/[id]/route.js +8 -8
  24. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/bot/task/route.js +6 -6
  26. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  27. package/.next/server/app/api/config/route.js.nft.json +1 -1
  28. package/.next/server/app/api/events/route.js.nft.json +1 -1
  29. package/.next/server/app/api/file/[...path]/route.js +4 -4
  30. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/llm-model/catalog/route.js +2 -2
  32. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  33. package/.next/server/app/api/llm-model/credits/route.js +2 -2
  34. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  35. package/.next/server/app/api/llm-model/route.js +2 -2
  36. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  37. package/.next/server/app/api/llm-model/subscription/route.js +2 -2
  38. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  39. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  40. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  41. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  42. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  43. package/.next/server/app/api/memory/edit/route.js +11 -11
  44. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  45. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  46. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  48. package/.next/server/app/api/thursday/call/route.js +6 -6
  49. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  50. package/.next/server/app/api/thursday/skills/route.js +6 -6
  51. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  52. package/.next/server/app/api/thursday/tool-call/route.js +8 -8
  53. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  54. package/.next/server/app/api/workspace/route.js +3 -2
  55. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  56. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  57. package/.next/server/app/artifact/[...path]/page.js +2 -2
  58. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  59. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  61. package/.next/server/app/page/server-reference-manifest.json +40 -40
  62. package/.next/server/app/page.js +3 -3
  63. package/.next/server/app/page.js.nft.json +1 -1
  64. package/.next/server/app/page_client-reference-manifest.js +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__026u77e._.js +5 -0
  66. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +1 -1
  67. package/.next/server/chunks/{[root-of-the-server]__02xjoto._.js → [root-of-the-server]__050zri4._.js} +1 -1
  68. package/.next/server/chunks/[root-of-the-server]__0bg9710._.js +3 -0
  69. package/.next/server/chunks/[root-of-the-server]__0in-d5s._.js +3 -0
  70. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +1 -1
  71. package/.next/server/chunks/[root-of-the-server]__0pa9k-f._.js +8 -0
  72. package/.next/server/chunks/[root-of-the-server]__0qi_m0e._.js +8 -0
  73. package/.next/server/chunks/[root-of-the-server]__0tq0m_k._.js +3 -0
  74. package/.next/server/chunks/[root-of-the-server]__0xep6j_._.js +3 -0
  75. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +2 -2
  76. package/.next/server/chunks/[root-of-the-server]__144l7qt._.js +8 -0
  77. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +2 -2
  78. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__1jg8t86._.js +8 -0
  80. package/.next/server/chunks/{[root-of-the-server]__12cabg9._.js → [root-of-the-server]__1lldzy9._.js} +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +4 -3
  82. package/.next/server/chunks/{[root-of-the-server]__01dkp4a._.js → [root-of-the-server]__1peqvsi._.js} +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__1w8gm5h._.js +10 -0
  84. package/.next/server/chunks/[root-of-the-server]__1xt39nn._.js +8 -0
  85. package/.next/server/chunks/[root-of-the-server]__1zb8mfo._.js +8 -0
  86. package/.next/server/chunks/_0-fjql4._.js +11 -6
  87. package/.next/server/chunks/_00xh_q0._.js +4 -6
  88. package/.next/server/chunks/_05h2cii._.js +1 -1
  89. package/.next/server/chunks/{_1_9mjx-._.js → _091bi8d._.js} +1 -1
  90. package/.next/server/chunks/_0fzqt2y._.js +57 -45
  91. package/.next/server/chunks/{_0u4aiy2._.js → _0lr92vd._.js} +1 -1
  92. package/.next/server/chunks/_0mecv0f._.js +11 -6
  93. package/.next/server/chunks/_0mh8j4m._.js +1 -1
  94. package/.next/server/chunks/{_0sbynis._.js → _0si7xzn._.js} +1 -1
  95. package/.next/server/chunks/_0wp4le3._.js +1 -1
  96. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  97. package/.next/server/chunks/_1230wbx._.js +5 -5
  98. package/.next/server/chunks/_15hq89o._.js +1 -1
  99. package/.next/server/chunks/{_0obojvv._.js → _18d7-5p._.js} +1 -1
  100. package/.next/server/chunks/{_03kp0jp._.js → _1c78vw5._.js} +71 -61
  101. package/.next/server/chunks/_1d0j961._.js +2 -2
  102. package/.next/server/chunks/_1j8k_j5._.js +1 -1
  103. package/.next/server/chunks/_1krd9rf._.js +1 -1
  104. package/.next/server/chunks/_1php4eq._.js +1 -1
  105. package/.next/server/chunks/{_03a0897._.js → _1zkfbaa._.js} +1 -1
  106. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  107. package/.next/server/chunks/features_1qf3cei._.js +6 -1
  108. package/.next/server/chunks/features_memory_1uqvksn._.js +2 -2
  109. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_17k8irj._.js} +1 -1
  110. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js → node_modules_consola_dist_chunks_prompt_mjs_1jidylq._.js} +1 -1
  111. package/.next/server/chunks/ssr/[root-of-the-server]__0-nob52._.js +4 -0
  112. package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +3 -0
  113. package/.next/server/chunks/ssr/{[root-of-the-server]__0_69wqr._.js → [root-of-the-server]__0s4wb56._.js} +1 -1
  114. package/.next/server/chunks/ssr/{[root-of-the-server]__1eh7hid._.js → [root-of-the-server]__0s9dq5u._.js} +1 -1
  115. package/.next/server/chunks/ssr/{[root-of-the-server]__0zkv7z_._.js → [root-of-the-server]__0sd3izg._.js} +2 -2
  116. package/.next/server/chunks/ssr/{[root-of-the-server]__1gtlw6v._.js → [root-of-the-server]__1-13tcn._.js} +3 -3
  117. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +13 -9
  118. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  119. package/.next/server/chunks/ssr/{[root-of-the-server]__1cyjywu._.js → [root-of-the-server]__1zidpfl._.js} +1 -1
  120. package/.next/server/chunks/ssr/_001_ms1._.js +6 -1
  121. package/.next/server/chunks/ssr/{_07uchnc._.js → _04z2c7b._.js} +2 -2
  122. package/.next/server/chunks/ssr/{_0kbkmar._.js → _06tssji._.js} +2 -2
  123. package/.next/server/chunks/ssr/_0fk4c7d._.js +3 -0
  124. package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
  125. package/.next/server/chunks/ssr/{_1xvgn77._.js → _0m2lsd1._.js} +2 -2
  126. package/.next/server/chunks/ssr/_0mggveb._.js +3 -0
  127. package/.next/server/chunks/ssr/_0nbpc_8._.js +3 -0
  128. package/.next/server/chunks/ssr/{_0y0tiht._.js → _0rzucul._.js} +3 -3
  129. package/.next/server/chunks/ssr/{_0mb8zc8._.js → _0s99bgb._.js} +2 -2
  130. package/.next/server/chunks/ssr/_0zcjrbg._.js +3 -0
  131. package/.next/server/chunks/ssr/_1-teee5._.js +4 -4
  132. package/.next/server/chunks/ssr/{_0c-ql2m._.js → _11j92tu._.js} +2 -2
  133. package/.next/server/chunks/ssr/_13fke0x._.js +1 -1
  134. package/.next/server/chunks/ssr/{_01_6m32._.js → _1ebxla1._.js} +2 -2
  135. package/.next/server/chunks/ssr/_1jdgjv9._.js +3 -0
  136. package/.next/server/chunks/ssr/{_0u2jv_x._.js → _1lz_cq2._.js} +60 -50
  137. package/.next/server/chunks/ssr/{_20kd_n0._.js → _1nj4niv._.js} +3 -3
  138. package/.next/server/chunks/ssr/_1ovmc9s._.js +3 -0
  139. package/.next/server/chunks/ssr/_1sji22n._.js +1 -1
  140. package/.next/server/chunks/ssr/_1vyd1ji._.js +1 -1
  141. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  142. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  143. package/.next/server/chunks/ssr/{node_modules_1-hc6uv._.js → node_modules_0-dr_o7._.js} +1 -1
  144. package/.next/server/chunks/ssr/{node_modules_124bue5._.js → node_modules_04sksn9._.js} +1 -1
  145. package/.next/server/chunks/ssr/{node_modules_1uuj9x1._.js → node_modules_05-n6dz._.js} +1 -1
  146. package/.next/server/chunks/ssr/{node_modules_1i7u1ia._.js → node_modules_056qgf-._.js} +1 -1
  147. package/.next/server/chunks/ssr/{node_modules_0lm7_vm._.js → node_modules_07dsgkx._.js} +1 -1
  148. package/.next/server/chunks/ssr/{node_modules_1h4a4pp._.js → node_modules_0bvopy2._.js} +1 -1
  149. package/.next/server/chunks/ssr/{node_modules_1bc7gu8._.js → node_modules_0fkeku3._.js} +1 -1
  150. package/.next/server/chunks/ssr/{node_modules_0h50b8n._.js → node_modules_0ifp389._.js} +1 -1
  151. package/.next/server/chunks/ssr/{node_modules_0zp9r92._.js → node_modules_0iur59s._.js} +1 -1
  152. package/.next/server/chunks/ssr/{node_modules_063xwpp._.js → node_modules_0m383r2._.js} +1 -1
  153. package/.next/server/chunks/ssr/{node_modules_1bgb6-f._.js → node_modules_0u9da1h._.js} +1 -1
  154. package/.next/server/chunks/ssr/{node_modules_0bvraul._.js → node_modules_0zeg3x_._.js} +1 -1
  155. package/.next/server/chunks/ssr/{node_modules_1t9cyee._.js → node_modules_1-y7nni._.js} +1 -1
  156. package/.next/server/chunks/ssr/{node_modules_1_n3ztw._.js → node_modules_16mkxuo._.js} +1 -1
  157. package/.next/server/chunks/ssr/{node_modules_0eu1va6._.js → node_modules_1a0kmjl._.js} +1 -1
  158. package/.next/server/chunks/ssr/{node_modules_0z5tsu8._.js → node_modules_1chvq5f._.js} +1 -1
  159. package/.next/server/chunks/ssr/{node_modules_0h7u60n._.js → node_modules_1j8j4pt._.js} +1 -1
  160. package/.next/server/chunks/ssr/{node_modules_0rjg6t1._.js → node_modules_1k6n600._.js} +1 -1
  161. package/.next/server/chunks/ssr/{node_modules_1xx28h1._.js → node_modules_1k7z2j6._.js} +1 -1
  162. package/.next/server/chunks/ssr/{node_modules_0h5hnr1._.js → node_modules_1k8j8n0._.js} +1 -1
  163. package/.next/server/chunks/ssr/{node_modules_0mwym0g._.js → node_modules_1lz0oxc._.js} +1 -1
  164. package/.next/server/chunks/ssr/{node_modules_1vqb6po._.js → node_modules_1t77e4n._.js} +1 -1
  165. package/.next/server/chunks/ssr/{node_modules_1l0qtdu._.js → node_modules_1uyk91w._.js} +1 -1
  166. package/.next/server/chunks/ssr/{node_modules_0y_cded._.js → node_modules_1x0ztf4._.js} +1 -1
  167. package/.next/server/chunks/ssr/{node_modules_113tz0l._.js → node_modules_1y79i83._.js} +1 -1
  168. package/.next/server/chunks/ssr/{node_modules_1juq3hs._.js → node_modules_20c28js._.js} +1 -1
  169. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0d3skpl._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0bkwd0u._.js} +1 -1
  170. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0i44kk5._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0p9a9su._.js} +1 -1
  171. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_0qe30a_._.js → node_modules_@vercel_oidc_dist_1o553h-._.js} +1 -1
  172. package/.next/server/middleware-build-manifest.js +3 -3
  173. package/.next/server/pages/404.html +1 -1
  174. package/.next/server/pages/500.html +1 -1
  175. package/.next/server/server-reference-manifest.js +1 -1
  176. package/.next/server/server-reference-manifest.json +41 -41
  177. package/.next/static/chunks/{17donp69y4do7.js → 0n3jle9kbqfr2.js} +8 -8
  178. package/.next/static/chunks/{3ok9_x74rip3v.js → 11r2yp_yh9_qn.js} +1 -1
  179. package/.next/static/chunks/{26-pr2c9ro222.js → 126fj4pga_72k.js} +1 -1
  180. package/.next/static/chunks/{2a0k6jlq0_v1u.js → 159udiexf6dom.js} +2 -2
  181. package/.next/static/chunks/18gi7o2ibrgh8.js +3 -0
  182. package/.next/static/chunks/{0x79tx04dxbpp.js → 1q9z1tohil7xn.js} +1 -1
  183. package/.next/static/chunks/{3cfi_q6d_wtci.js → 1sjgfets224re.js} +1 -1
  184. package/.next/static/chunks/{3agpgkwaa321p.js → 1wszsr83t01gh.js} +1 -1
  185. package/.next/static/chunks/22_zkas21rpna.js +1 -0
  186. package/.next/static/chunks/{1ekc7ul556kbl.js → 2szintyibu0s5.js} +1 -1
  187. package/.next/static/chunks/{1okqiww3pnwka.js → 3-b6b7entqazs.js} +1 -1
  188. package/.next/static/chunks/3cq5g494yuke8.css +1 -0
  189. package/.next/static/chunks/{2fzs9fm8yarvq.js → 3dmbhsv2u_8kj.js} +1 -1
  190. package/.next/static/chunks/{1grhjo7a5a0_-.js → 3i0a5px-oufgc.js} +1 -1
  191. package/.next/static/chunks/{280t0_e99i276.js → 3wexk9qo8zuj3.js} +3 -3
  192. package/.next/static/chunks/3xre8bu_pk1p_.js +1 -0
  193. package/.next/static/chunks/3zv6t3i77xhr_.js +1 -0
  194. package/.next/static/chunks/{28cizcdaabd4h.js → 408ueu4hfvcg7.js} +1 -1
  195. package/.next/static/chunks/{3xpbkl66ssib5.js → 435atsnhs7ksu.js} +8 -8
  196. package/README.md +46 -23
  197. package/app/api/events/app-event.ts +0 -2
  198. package/bin/port.mjs +65 -0
  199. package/bin/thursday.mjs +2 -33
  200. package/docs/how-it-works.md +11 -7
  201. package/docs/images/face-dark.png +0 -0
  202. package/docs/images/face-light.png +0 -0
  203. package/docs/images/keeps-going.png +0 -0
  204. package/features/ai/chatgpt.ts +42 -20
  205. package/features/ai/load-tools.ts +20 -14
  206. package/features/ai/prompts/bot.prompt.ts +91 -89
  207. package/features/ai/prompts/thursday.prompt.ts +48 -41
  208. package/features/ai/tools/bot.tool.ts +3 -3
  209. package/features/ai/tools/memory.tool.ts +65 -62
  210. package/features/ai/tools/skills.tool.ts +25 -8
  211. package/features/ai/tools/tool-name.ts +0 -1
  212. package/features/ai/tools/workspace.tool.ts +3 -0
  213. package/features/bot/bot.action.ts +6 -1
  214. package/features/bot/bot.run.ts +195 -80
  215. package/features/bot/bot.runner.ts +82 -39
  216. package/features/bot/bot.schema.ts +25 -0
  217. package/features/bot/components/bot-room.tsx +57 -4
  218. package/features/bot/components/bot-tool.tsx +0 -1
  219. package/features/bot/task.query.ts +132 -10
  220. package/features/bot/task.store.ts +26 -9
  221. package/features/memory/components/memory-edit.tsx +7 -5
  222. package/features/thursday/components/thursday.tsx +0 -3
  223. package/features/thursday/tool-line.ts +0 -1
  224. package/features/thursday/use-thursday.ts +1 -1
  225. package/features/workspace/workspace.ts +27 -20
  226. package/package.json +1 -1
  227. package/skills/browser/SKILL.md +6 -5
  228. package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
  229. package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
  230. package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
  231. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
  232. package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
  233. package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
  234. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
  235. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
  236. package/.next/server/chunks/_1zjrdag._.js +0 -3
  237. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
  238. package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +0 -3
  239. package/.next/server/chunks/ssr/_0mo2w06._.js +0 -3
  240. package/.next/server/chunks/ssr/_13z3f85._.js +0 -3
  241. package/.next/server/chunks/ssr/_14_lw10._.js +0 -3
  242. package/.next/server/chunks/ssr/_1eoqg_g._.js +0 -3
  243. package/.next/server/chunks/ssr/_1spzm8z._.js +0 -3
  244. package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +0 -3
  245. package/.next/static/chunks/0tmgrnlje14-s.js +0 -3
  246. package/.next/static/chunks/0xzgvc0fkdb6t.js +0 -1
  247. package/.next/static/chunks/14hgtsjg-kael.css +0 -1
  248. package/.next/static/chunks/2hw2f6zmt7fmt.js +0 -1
  249. package/.next/static/chunks/3111pj5wnjgos.js +0 -1
  250. package/features/memory/components/memory-view.tsx +0 -84
  251. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_buildManifest.js +0 -0
  252. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_clientMiddlewareManifest.js +0 -0
  253. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → G6-gNoyGDEcgAsq-L4PE1}/_ssgManifest.js +0 -0
@@ -33,12 +33,22 @@ import { desktopNotify } from "@/lib/desktop-notify";
33
33
  import { logger } from "@/lib/logger";
34
34
  import { publicError } from "@/lib/public-error";
35
35
  import { createKeyedLock } from "@/lib/queue";
36
- import { resumeThread, runBot, type TaskEvent } from "./bot.run";
37
- import { TASK_CONTINUE, type TaskPending, type TaskStatus } from "./bot.schema";
36
+ import {
37
+ createBotDesks,
38
+ resumeThread,
39
+ runBot,
40
+ type TaskEvent,
41
+ } from "./bot.run";
42
+ import {
43
+ TASK_CONTINUE,
44
+ type TaskPending,
45
+ type TaskSpeaker,
46
+ type TaskStatus,
47
+ tagSpeaker,
48
+ } from "./bot.schema";
38
49
  import {
39
50
  addTaskUsage,
40
51
  countStopsSinceSpoken,
41
- deleteFinishedTasks,
42
52
  deleteMessages,
43
53
  deleteTask,
44
54
  findTask,
@@ -49,6 +59,7 @@ import {
49
59
  listRunningTaskIds,
50
60
  listTaskFolders,
51
61
  listThread,
62
+ stopNote,
52
63
  type TaskMessageInput,
53
64
  updateTask,
54
65
  upsertMessage,
@@ -99,24 +110,28 @@ const pausing = ((globalThis as Pinned).__botPausing ??= {
99
110
  current: Promise.resolve(),
100
111
  });
101
112
 
102
- /** Opens a job. The opening message — who is who, the job, the call it came from (bot.prompt buildTaskOpening) — is the thread's first row. */
113
+ /** Opens a job. The opening message — who is on it, the job, the call it came from (bot.prompt buildTaskOpening) — is the thread's first row. */
103
114
  export async function startTask(input: {
104
115
  bot: string;
105
116
  request: string;
106
117
  label: string;
107
118
  callId?: string | null;
119
+ /** Who handed it over: Thursday during a call, or the user on screen. */
120
+ from: TaskSpeaker;
108
121
  }) {
109
- const conversation = input.callId
110
- ? await listCallTurns(input.callId, OPENING_TURNS)
122
+ const { from, ...row } = input;
123
+ const conversation = row.callId
124
+ ? await listCallTurns(row.callId, OPENING_TURNS)
111
125
  : [];
112
126
  const opening = buildTaskOpening({
113
- bot: input.bot,
114
- request: input.request,
127
+ bot: row.bot,
128
+ request: row.request,
115
129
  conversation,
130
+ from,
116
131
  });
117
- const task = await insertTask({ ...input, opening });
132
+ const task = await insertTask({ ...row, opening });
118
133
  launch(task.id, {
119
- bot: input.bot,
134
+ bot: row.bot,
120
135
  messages: [{ role: "user", content: opening }],
121
136
  });
122
137
  return task.id;
@@ -128,14 +143,25 @@ export async function startTask(input: {
128
143
  * before its next step; the step in flight finishes), or a follow-up to a
129
144
  * finished one. Only when the run has already ended does the running case
130
145
  * fall through to stop, append, relaunch.
146
+ *
147
+ * @param from Who said it. The words go on the thread tagged with it (bot.schema
148
+ * tagSpeaker): Thursday passing something on from a call and the user typing on
149
+ * screen reach the bot down this one pipe, and a go-ahead means something
150
+ * different from each. Unset for words that already carry their tags — the notes
151
+ * a finished run left unread (drive).
131
152
  */
132
- export async function answerTask(id: string, answer: string) {
153
+ export async function answerTask(
154
+ id: string,
155
+ answer: string,
156
+ from?: TaskSpeaker,
157
+ ) {
158
+ const said = from ? tagSpeaker(from, answer) : answer;
133
159
  return taskLock(id, async () => {
134
160
  const task = await findTask(id);
135
161
  if (!task) publicError("No such job.");
136
162
 
137
163
  const live = running.get(id);
138
- if (live && task.status === "running" && live.note(answer)) {
164
+ if (live && task.status === "running" && live.note(said)) {
139
165
  logger.debug(`task ${id}: interjection queued`);
140
166
  return;
141
167
  }
@@ -147,7 +173,7 @@ export async function answerTask(id: string, answer: string) {
147
173
  forgetResume(id);
148
174
 
149
175
  const thread = await listThread(id);
150
- const { reply, as } = joinThread(task, answer, thread);
176
+ const { reply, as } = joinThread(task, said, thread);
151
177
  logger.debug(`task ${id}: ${as}`);
152
178
 
153
179
  await upsertMessage(id, (await lastSeq(id)) + 1, {
@@ -253,7 +279,8 @@ export async function cancelTask(id: string) {
253
279
  // write "Cancelled." over the thing it was run for.
254
280
  if (task.endedAt) publicError("That job has already ended.");
255
281
  forgetResume(id);
256
- running.get(id)?.stop.abort();
282
+ const live = running.get(id);
283
+ live?.stop.abort();
257
284
  await updateTask(id, {
258
285
  status: "failed",
259
286
  outcome: "Cancelled.",
@@ -264,24 +291,27 @@ export async function cancelTask(id: string) {
264
291
  });
265
292
  // Cancel is a real end, so the browser session closes here; the abort in
266
293
  // answerTask is followed by a relaunch and must not close it.
267
- void closeJobShell(id);
294
+ await live?.done;
295
+ await closeJobShell(id);
268
296
  });
269
297
  }
270
298
 
271
299
  /** Deletes the job, stopping it first if it is still running. */
272
300
  export async function removeTask(id: string) {
273
- return taskLock(id, async () => {
274
- const task = await findTask(id);
275
- forgetResume(id);
276
- running.get(id)?.stop.abort();
277
- running.delete(id);
278
- void closeJobShell(id);
279
- const gone = await deleteTask(id);
280
- // The working folder's lifetime is the row's: with the job gone, what it was
281
- // working with is nobody's (workspace.ts jobScratch).
282
- if (gone && task) await removeJobScratch(id, task.label);
283
- return gone;
284
- });
301
+ return taskLock(id, () => removeLockedTask(id));
302
+ }
303
+
304
+ /** The caller holds taskLock, so a follow-up cannot race deletion or browser cleanup. */
305
+ async function removeLockedTask(id: string) {
306
+ const task = await findTask(id);
307
+ forgetResume(id);
308
+ const live = running.get(id);
309
+ live?.stop.abort();
310
+ await live?.done;
311
+ await closeJobShell(id);
312
+ const gone = await deleteTask(id);
313
+ if (gone && task) await removeJobScratch(id, task.label);
314
+ return gone;
285
315
  }
286
316
 
287
317
  /**
@@ -290,9 +320,16 @@ export async function removeTask(id: string) {
290
320
  * and only one of the two is the database's.
291
321
  */
292
322
  export async function removeFinishedTasks(): Promise<number> {
293
- const removed = await deleteFinishedTasks();
294
- for (const task of removed) await removeJobScratch(task.id, task.label);
295
- return removed.length;
323
+ let removed = 0;
324
+ for (const task of await listTaskFolders()) {
325
+ if (task.status !== "done" && task.status !== "failed") continue;
326
+ await taskLock(task.id, async () => {
327
+ const current = await findTask(task.id);
328
+ if (current?.status !== "done" && current?.status !== "failed") return;
329
+ if (await removeLockedTask(task.id)) removed += 1;
330
+ });
331
+ }
332
+ return removed;
296
333
  }
297
334
 
298
335
  /**
@@ -323,6 +360,7 @@ export async function sweepJobFiles(): Promise<string[]> {
323
360
  await taskLock(task.id, async () => {
324
361
  const now = await findTask(task.id);
325
362
  if (!now || !stale(now)) return;
363
+ await closeHiddenBrowser(task.id);
326
364
  await removeJobScratch(task.id, task.label);
327
365
  removed.push(folder);
328
366
  });
@@ -465,7 +503,7 @@ async function parkTask(
465
503
  const inMs =
466
504
  auto && stop.retry ? waits[Math.min(stops, waits.length - 1)] : 0;
467
505
 
468
- await writeNote(id, `${stop.why} ${IN_FLIGHT}`).catch((cause) =>
506
+ await writeNote(id, stopNote(stop.why)).catch((cause) =>
469
507
  logger.warn(`task ${id}: stop not written`, cause),
470
508
  );
471
509
  await writeEnding(id, {
@@ -485,10 +523,6 @@ async function parkTask(
485
523
  if (auto) scheduleResume(id, inMs);
486
524
  }
487
525
 
488
- /** Said to a resumed run after why it stopped. */
489
- const IN_FLIGHT =
490
- "Anything that was under way — a command, a page loading, a download — may not have finished: check before relying on it, then carry on.";
491
-
492
526
  const spell = (ms: number) =>
493
527
  ms < 60_000
494
528
  ? `${Math.round(ms / 1000)} seconds`
@@ -634,6 +668,12 @@ class ThreadWriter {
634
668
  if (this.closed) return;
635
669
  const step = this.step(event);
636
670
  switch (event.type) {
671
+ case "request":
672
+ return this.write(++this.seq, event, {
673
+ role: "user",
674
+ content: event.content,
675
+ });
676
+
637
677
  case "text":
638
678
  step.assistant ??= ++this.seq;
639
679
  step.parts.assistant.push({ type: "text", text: event.text });
@@ -729,6 +769,7 @@ async function drive(
729
769
  notes: string[],
730
770
  ) {
731
771
  const thread = new ThreadWriter(id, await lastSeq(id));
772
+ const desks = createBotDesks();
732
773
 
733
774
  let ending: {
734
775
  status: TaskStatus;
@@ -749,6 +790,7 @@ async function drive(
749
790
  await runBot(input, {
750
791
  signal,
751
792
  taskId: id,
793
+ desks,
752
794
  notes: () => notes.splice(0),
753
795
  emit: async (event) => {
754
796
  await thread.on(event);
@@ -811,6 +853,8 @@ async function drive(
811
853
  pending: null,
812
854
  };
813
855
  } finally {
856
+ // A resumed segment cannot overtake a participant still unwinding its tools.
857
+ await desks.settle();
814
858
  thread.close();
815
859
  running.delete(id);
816
860
  }
@@ -863,10 +907,9 @@ async function drive(
863
907
  return;
864
908
  }
865
909
 
866
- // A waiting job keeps its browser: answering resumes into the page it left
867
- // off at. An ended one closes only what nobody can see (closeHiddenBrowser).
868
- if (park || final.status === "waiting") void pruneJobFiles();
869
- else void closeHiddenBrowser(id);
910
+ // Follow-ups can reopen a completed job. Its participants keep their browsers
911
+ // until cancellation, deletion, or the workspace retention sweep.
912
+ void pruneJobFiles();
870
913
 
871
914
  const task = await findTask(id);
872
915
  if (!task) return;
@@ -205,6 +205,29 @@ export const TASK_HISTORY_PAGE = PAGE_SIZE;
205
205
  /** The single option offered when the app stopped a job and asks whether to go on. Button text and spoken word alike. */
206
206
  export const TASK_CONTINUE = "Continue";
207
207
 
208
+ /**
209
+ * Who a person's words to a job came from (bot.runner answerTask). Thursday
210
+ * passing something on from a call and the user typing on screen reach a bot
211
+ * down one pipe, so the words carry it in front of them.
212
+ */
213
+ export type TaskSpeaker = "thursday" | "user";
214
+
215
+ const SPEAKER_TAGS: Record<TaskSpeaker, string> = {
216
+ thursday: "Thursday, on the call:",
217
+ user: "The user, on screen:",
218
+ };
219
+
220
+ export const tagSpeaker = (from: TaskSpeaker, text: string): string =>
221
+ `${SPEAKER_TAGS[from]} ${text}`;
222
+
223
+ /** The words without their tag, as the screen draws a person's line (task.query linesOf). */
224
+ export const untagSpeaker = (text: string): string => {
225
+ for (const tag of Object.values(SPEAKER_TAGS)) {
226
+ if (text.startsWith(`${tag} `)) return text.slice(tag.length + 1);
227
+ }
228
+ return text;
229
+ };
230
+
208
231
  /**
209
232
  * What a `waiting` job waits on (database task.pending). `toolCallId` is the
210
233
  * `ask_thursday` call the answer resolves, null when the app stopped the run and
@@ -254,6 +277,8 @@ export const TaskLineSchema = z.discriminatedUnion("kind", [
254
277
  LineBase.extend({ kind: z.literal("text"), text: z.string() }),
255
278
  /** Compaction summary (bot.run compact). The model resumes from here; the screen draws it as a divider. */
256
279
  LineBase.extend({ kind: z.literal("note"), text: z.string() }),
280
+ /** Why the app stopped the run (bot.runner parkTask, a run that broke), without what the resumed run is told to check. */
281
+ LineBase.extend({ kind: z.literal("stop"), text: z.string() }),
257
282
  LineBase.extend({
258
283
  kind: z.literal("tool"),
259
284
  callId: z.string(),
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
+ import { format } from "date-fns";
3
4
  import {
4
5
  ArrowUp,
5
6
  Check,
7
+ ChevronDown,
6
8
  ChevronLeft,
7
9
  ChevronRight,
8
10
  ChevronsRight,
@@ -10,6 +12,7 @@ import {
10
12
  History,
11
13
  Loader2,
12
14
  Plus,
15
+ RotateCw,
13
16
  X,
14
17
  } from "lucide-react";
15
18
  import {
@@ -45,7 +48,7 @@ import { TaskReply, useAnswerTask } from "@/features/bot/components/task-reply";
45
48
  import { openSettings } from "@/features/settings/settings.store";
46
49
  import { ThursdayMark } from "@/features/thursday/components/thursday-mark";
47
50
  import { FileViewer } from "@/features/workspace/components/file-view";
48
- import { shortAgo } from "@/lib/date-like";
51
+ import { shortAgo, toDate } from "@/lib/date-like";
49
52
  import { useServerAction } from "@/lib/protocol/use-server-action";
50
53
  import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
51
54
  import {
@@ -1630,6 +1633,8 @@ function Group({ group, task }: { group: ChatterGroup; task: TaskView }) {
1630
1633
  {runs(group.lines).map((run) =>
1631
1634
  run.kind === "tools" ? (
1632
1635
  <Steps key={run.key} lines={run.lines} taskId={task.id} />
1636
+ ) : run.kind === "stops" ? (
1637
+ <Stops key={run.key} lines={run.lines} />
1633
1638
  ) : (
1634
1639
  run.lines.map((line) => (
1635
1640
  <Line key={line.id} line={line} taskId={task.id} />
@@ -1793,17 +1798,65 @@ function Steps({ lines, taskId }: { lines: Chatter[]; taskId: string }) {
1793
1798
  );
1794
1799
  }
1795
1800
 
1796
- /** Splits one speaker's lines into runs of tool calls and runs of everything else. */
1801
+ /**
1802
+ * Where the app stopped the run (bot.runner parkTask): a failed model call, a
1803
+ * restart, a closed browser. Muted and in the bot's turn, since the bot goes on
1804
+ * from here. The same reason in a row is one line with a count; opening it lists
1805
+ * each stop by the time it happened.
1806
+ */
1807
+ function Stops({ lines }: { lines: Chatter[] }) {
1808
+ return (
1809
+ <details className="group min-w-0 px-1 text-muted-foreground">
1810
+ <summary className="flex cursor-pointer list-none items-center gap-1.5 text-[12.5px] leading-relaxed outline-none [&::-webkit-details-marker]:hidden">
1811
+ <RotateCw className="size-3 shrink-0" />
1812
+ <span className="min-w-0 truncate">{leadOf(lines[0].text)}</span>
1813
+ {lines.length > 1 && (
1814
+ <span className="shrink-0 font-mono text-[10px] text-muted-foreground/70">
1815
+ ×{lines.length}
1816
+ </span>
1817
+ )}
1818
+ <ChevronDown className="size-2.5 shrink-0 text-muted-foreground/70 transition-transform group-open:rotate-180" />
1819
+ </summary>
1820
+ <div className="mt-1 space-y-1 pl-4.5 text-[11px] leading-relaxed">
1821
+ {lines.map((line) => (
1822
+ <p key={line.id} className="flex gap-2 break-keep">
1823
+ {line.at && (
1824
+ <span className="shrink-0 font-mono text-muted-foreground/70 tabular-nums">
1825
+ {format(toDate(line.at), "HH:mm:ss")}
1826
+ </span>
1827
+ )}
1828
+ <span className="min-w-0">{line.text}</span>
1829
+ </p>
1830
+ ))}
1831
+ </div>
1832
+ </details>
1833
+ );
1834
+ }
1835
+
1836
+ /** What stopped it, without the words behind it: a stop names those in parentheses (bot.runner parkTask). */
1837
+ const leadOf = (text: string) => text.split(" (")[0].replace(/\.$/, "");
1838
+
1839
+ /** Splits one speaker's lines into runs of tool calls, of stops, and of everything else. */
1797
1840
  type Run =
1798
1841
  | { kind: "tools"; key: string; lines: Chatter[] }
1842
+ | { kind: "stops"; key: string; lines: Chatter[] }
1799
1843
  | { kind: "said"; key: string; lines: Chatter[] };
1800
1844
 
1801
1845
  function runs(lines: Chatter[]): Run[] {
1802
1846
  const out: Run[] = [];
1803
1847
  for (const line of lines) {
1804
- const kind = line.kind === "tool" && line.tool ? "tools" : "said";
1848
+ const kind =
1849
+ line.kind === "tool" && line.tool
1850
+ ? "tools"
1851
+ : line.kind === "stop"
1852
+ ? "stops"
1853
+ : "said";
1805
1854
  const open = out.at(-1);
1806
- if (open && open.kind === kind) open.lines.push(line);
1855
+ // Stops fold only while the reason repeats; another reason is a line of its own
1856
+ const same =
1857
+ open?.kind === kind &&
1858
+ (kind !== "stops" || leadOf(open.lines[0].text) === leadOf(line.text));
1859
+ if (open && same) open.lines.push(line);
1807
1860
  else out.push({ kind, key: line.id, lines: [line] });
1808
1861
  }
1809
1862
  return out;
@@ -64,7 +64,6 @@ const TOOL_ICONS: Record<string, LucideIcon> = {
64
64
  memory_recall: MemoryMark,
65
65
  memory_remember: MemoryMark,
66
66
  memory_forget: MemoryMark,
67
- memory_show: MemoryMark,
68
67
  load_skill: SkillsMark,
69
68
  tool_search: McpMark,
70
69
  tool_call: McpMark,
@@ -25,6 +25,7 @@ import {
25
25
  type TaskPending,
26
26
  type TaskStatus,
27
27
  type TokenUsage,
28
+ untagSpeaker,
28
29
  } from "./bot.schema";
29
30
 
30
31
  // Tasks and their threads. Bots themselves (roster, pinned tools) are bot.query.
@@ -343,21 +344,23 @@ export async function listBorrowedTails(
343
344
  and(
344
345
  eq(taskMessageTable.taskId, taskId),
345
346
  isNotNull(taskMessageTable.parent),
346
- eq(taskMessageTable.role, "assistant"),
347
+ inArray(taskMessageTable.role, ["assistant", "tool"]),
347
348
  ),
348
349
  )
349
350
  .orderBy(taskMessageTable.seq);
350
351
 
351
352
  const calls = new Map<
352
353
  string,
353
- { bot: string; texts: string[]; then: string | null }
354
+ { bot: string; texts: string[]; then: string | null; exchanges: string[] }
354
355
  >();
356
+ const questions = new Map<string, string>();
355
357
  for (const row of rows) {
356
358
  if (!row.parent) continue;
357
359
  const call = calls.get(row.parent) ?? {
358
360
  bot: row.bot ?? "",
359
361
  texts: [],
360
362
  then: null,
363
+ exchanges: [],
361
364
  };
362
365
  const content = row.content as AssistantContent;
363
366
  const parts =
@@ -371,6 +374,21 @@ export async function listBorrowedTails(
371
374
  } else if (part.type === "tool-call") {
372
375
  call.then =
373
376
  `${part.toolName} ${toolLine(part.toolName, part.input)}`.trim();
377
+ if (part.toolName === TOOL_NAMES.ask_back)
378
+ questions.set(
379
+ part.toolCallId,
380
+ String((part.input as { question?: string }).question ?? ""),
381
+ );
382
+ } else if (
383
+ part.type === "tool-result" &&
384
+ part.toolName === TOOL_NAMES.ask_back
385
+ ) {
386
+ const answer = resultParts(part.output, Number.POSITIVE_INFINITY)
387
+ .flatMap((part) => (part.type === "text" ? [part.text] : []))
388
+ .join("\n");
389
+ call.exchanges.push(
390
+ `They asked you: ${questions.get(part.toolCallId) ?? ""}\nYou answered: ${answer}`,
391
+ );
374
392
  }
375
393
  }
376
394
  calls.set(row.parent, call);
@@ -380,6 +398,7 @@ export async function listBorrowedTails(
380
398
  for (const [id, call] of calls) {
381
399
  const tail = [
382
400
  ...call.texts.slice(-TAIL.texts).map((text) => clip(text, TAIL.chars)),
401
+ ...call.exchanges.map((text) => clip(text, TAIL.chars)),
383
402
  call.then ? `Then: ${call.then}` : "",
384
403
  ]
385
404
  .filter(Boolean)
@@ -541,6 +560,63 @@ export async function listThread(taskId: string): Promise<ModelMessage[]> {
541
560
  );
542
561
  }
543
562
 
563
+ /** A participant keeps its own work across every request, including requests from different bots. */
564
+ export async function listBotThread(taskId: string, bot: string) {
565
+ const rows = await database
566
+ .select()
567
+ .from(taskMessageTable)
568
+ .where(
569
+ and(
570
+ eq(taskMessageTable.taskId, taskId),
571
+ isNotNull(taskMessageTable.parent),
572
+ eq(sql`lower(${taskMessageTable.bot})`, bot.toLowerCase()),
573
+ ),
574
+ )
575
+ .orderBy(taskMessageTable.seq);
576
+ // Older runs store their opening only in the caller's tool arguments.
577
+ const requests = await database
578
+ .select({ content: taskMessageTable.content, bot: taskMessageTable.bot })
579
+ .from(taskMessageTable)
580
+ .where(
581
+ and(
582
+ eq(taskMessageTable.taskId, taskId),
583
+ eq(taskMessageTable.role, "assistant"),
584
+ ),
585
+ )
586
+ .orderBy(taskMessageTable.seq);
587
+ const openings = new Map<string, ModelMessage>();
588
+ for (const row of requests) {
589
+ if (!Array.isArray(row.content)) continue;
590
+ for (const part of row.content) {
591
+ if (part.type !== "tool-call" || part.toolName !== TOOL_NAMES.ask_bot)
592
+ continue;
593
+ const input = part.input as { request?: string; context?: string };
594
+ openings.set(part.toolCallId, {
595
+ role: "user",
596
+ content: `${row.bot} → ${bot}:\n\n${input.request ?? ""}\n\n${input.context ?? ""}`,
597
+ });
598
+ }
599
+ }
600
+ const history: { message: ModelMessage; compact: boolean }[] = [];
601
+ const seen = new Set<string>();
602
+ for (const row of rows) {
603
+ if (row.parent && !seen.has(row.parent)) {
604
+ seen.add(row.parent);
605
+ if (row.role !== "user" || row.compact) {
606
+ const opening = openings.get(row.parent);
607
+ if (opening) history.push({ message: opening, compact: false });
608
+ }
609
+ }
610
+ history.push({
611
+ message: { role: row.role, content: row.content } as ModelMessage,
612
+ compact: row.compact,
613
+ });
614
+ }
615
+ const from = history.findLastIndex((row) => row.compact);
616
+ const kept = from > 0 ? [history[0], ...history.slice(from)] : history;
617
+ return kept.map((row) => row.message);
618
+ }
619
+
544
620
  /** One row in the screen's shape; `pending` folds into `ask`. */
545
621
  function viewOf(row: TaskRow, lines: TaskLine[]): Task {
546
622
  // contextTokens and contextBudget pass through in `rest`
@@ -576,10 +652,33 @@ async function withLines(rows: TaskRow[]): Promise<Task[]> {
576
652
 
577
653
  // ask_back lines name no addressee; the task's own bot is the addressee
578
654
  const owners = new Map(rows.map((row) => [row.id, row.bot]));
655
+ const askers = new Map<string, string>();
656
+ for (const message of messages) {
657
+ if (message.role !== "assistant" || !Array.isArray(message.content))
658
+ continue;
659
+ for (const part of message.content) {
660
+ if (
661
+ part.type === "tool-call" &&
662
+ part.toolName === TOOL_NAMES.ask_bot &&
663
+ message.bot
664
+ ) {
665
+ askers.set(`${message.taskId}:${part.toolCallId}`, message.bot);
666
+ }
667
+ }
668
+ }
579
669
  const byTask = new Map<string, TaskLine[]>();
580
670
  for (const message of messages) {
581
671
  const list = byTask.get(message.taskId) ?? [];
582
- list.push(...linesOf(message, owners.get(message.taskId) ?? ""));
672
+ list.push(
673
+ ...linesOf(
674
+ message,
675
+ (message.parent
676
+ ? askers.get(`${message.taskId}:${message.parent}`)
677
+ : null) ??
678
+ owners.get(message.taskId) ??
679
+ "",
680
+ ),
681
+ );
583
682
  byTask.set(message.taskId, list);
584
683
  }
585
684
  return rows.map((row) => viewOf(row, byTask.get(row.id) ?? []));
@@ -688,6 +787,13 @@ export async function readToolResult(
688
787
  return null;
689
788
  }
690
789
 
790
+ /** Said to a resumed run after why it stopped. The screen shows only the why (linesOf). */
791
+ const IN_FLIGHT =
792
+ "Anything that was under way — a command, a page loading, a download — may not have finished: check before relying on it, then carry on.";
793
+
794
+ /** The thread line a stop the app made leaves (bot.runner parkTask). */
795
+ export const stopNote = (why: string) => `${why} ${IN_FLIGHT}`;
796
+
691
797
  type StoredMessage = typeof taskMessageTable.$inferSelect;
692
798
 
693
799
  /**
@@ -707,14 +813,25 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
707
813
  const content = message.content;
708
814
 
709
815
  if (message.role === "user") {
710
- if (message.seq === 0) return [];
816
+ // Participant requests already appear as the caller's ask_bot line.
817
+ if (
818
+ message.seq === 0 ||
819
+ (message.parent && !message.compact && !message.note)
820
+ )
821
+ return [];
711
822
  const text = typeof content === "string" ? content : textOf(content);
712
823
  if (!text) return [];
713
- // The app's own line — a compaction marker, or why a run stopped — never
714
- // the user's words. `compact` is read too: rows written before `note` was.
715
- if (message.note || message.compact)
716
- return [{ ...base, id: id(0), kind: "note", text }];
717
- return [{ ...base, id: id(0), kind: "user", text }];
824
+ // The app's own lines, never the user's words. `compact` alone marks a
825
+ // compaction: rows written before `note` existed carry only that.
826
+ if (message.compact) return [{ ...base, id: id(0), kind: "note", text }];
827
+ if (message.note) {
828
+ const why = text.endsWith(IN_FLIGHT)
829
+ ? text.slice(0, -IN_FLIGHT.length).trimEnd()
830
+ : text;
831
+ return [{ ...base, id: id(0), kind: "stop", text: why }];
832
+ }
833
+ // Tagged with who said it for the bot (bot.schema tagSpeaker); the screen draws the words
834
+ return [{ ...base, id: id(0), kind: "user", text: untagSpeaker(text) }];
718
835
  }
719
836
 
720
837
  if (message.role === "assistant") {
@@ -817,7 +934,12 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
817
934
  const text = resultParts(part.output, RESULT_LINES)
818
935
  .flatMap((result) => (result.type === "text" ? [result.text] : []))
819
936
  .join("\n");
820
- lines.push({ ...base, id: id(index), kind: "user", text });
937
+ lines.push({
938
+ ...base,
939
+ id: id(index),
940
+ kind: "user",
941
+ text: untagSpeaker(text),
942
+ });
821
943
  } else if (
822
944
  part.toolName === TOOL_NAMES.ask_bot ||
823
945
  part.toolName === TOOL_NAMES.ask_back