thursday-agent 0.3.0 → 0.4.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 (197) 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.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  9. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  16. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  17. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  18. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  19. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  20. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  22. package/.next/server/app/api/config/route.js.nft.json +1 -1
  23. package/.next/server/app/api/events/route.js.nft.json +1 -1
  24. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  26. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  27. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  28. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  29. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  30. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  31. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  32. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  33. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  34. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  35. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  36. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  37. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  38. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  39. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  40. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  41. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  42. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  43. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  44. package/.next/server/app/page/server-reference-manifest.json +40 -40
  45. package/.next/server/app/page.js +2 -2
  46. package/.next/server/app/page.js.nft.json +1 -1
  47. package/.next/server/app/page_client-reference-manifest.js +1 -1
  48. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +1 -1
  49. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +4 -4
  50. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +1 -1
  51. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +2 -2
  52. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +2 -2
  53. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +1 -1
  54. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +3 -3
  55. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +4 -3
  56. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +3 -3
  57. package/.next/server/chunks/_0-fjql4._.js +5 -5
  58. package/.next/server/chunks/_00xh_q0._.js +3 -5
  59. package/.next/server/chunks/_05h2cii._.js +1 -1
  60. package/.next/server/chunks/_0fzqt2y._.js +48 -44
  61. package/.next/server/chunks/_0mecv0f._.js +5 -5
  62. package/.next/server/chunks/_0mh8j4m._.js +1 -1
  63. package/.next/server/chunks/_0wp4le3._.js +1 -1
  64. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  65. package/.next/server/chunks/_1230wbx._.js +3 -3
  66. package/.next/server/chunks/_15hq89o._.js +1 -1
  67. package/.next/server/chunks/{_03kp0jp._.js → _1c78vw5._.js} +58 -56
  68. package/.next/server/chunks/_1j8k_j5._.js +1 -1
  69. package/.next/server/chunks/_1krd9rf._.js +1 -1
  70. package/.next/server/chunks/_1php4eq._.js +1 -1
  71. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  72. package/.next/server/chunks/features_1qf3cei._.js +1 -1
  73. package/.next/server/chunks/features_memory_1uqvksn._.js +2 -2
  74. package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +3 -0
  75. package/.next/server/chunks/ssr/{[root-of-the-server]__1eh7hid._.js → [root-of-the-server]__0s9dq5u._.js} +1 -1
  76. package/.next/server/chunks/ssr/{[root-of-the-server]__1cyjywu._.js → [root-of-the-server]__0s9l2sv._.js} +1 -1
  77. package/.next/server/chunks/ssr/{[root-of-the-server]__0zkv7z_._.js → [root-of-the-server]__0xsog8n._.js} +2 -2
  78. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +13 -9
  79. package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +2 -2
  80. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  81. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +1 -1
  82. package/.next/server/chunks/ssr/{[root-of-the-server]__0_69wqr._.js → [root-of-the-server]__202p40z._.js} +1 -1
  83. package/.next/server/chunks/ssr/_001_ms1._.js +1 -1
  84. package/.next/server/chunks/ssr/{_0c-ql2m._.js → _0fz4mf_._.js} +2 -2
  85. package/.next/server/chunks/ssr/{_07uchnc._.js → _0gb4gmf._.js} +2 -2
  86. package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
  87. package/.next/server/chunks/ssr/{_20kd_n0._.js → _0ilna52._.js} +3 -3
  88. package/.next/server/chunks/ssr/{_0mo2w06._.js → _0n3toaw._.js} +2 -2
  89. package/.next/server/chunks/ssr/{_0mb8zc8._.js → _0s99bgb._.js} +2 -2
  90. package/.next/server/chunks/ssr/_0zcjrbg._.js +3 -0
  91. package/.next/server/chunks/ssr/{_13z3f85._.js → _1-_etts._.js} +2 -2
  92. package/.next/server/chunks/ssr/_13fke0x._.js +1 -1
  93. package/.next/server/chunks/ssr/{_01_6m32._.js → _1b2mjwr._.js} +2 -2
  94. package/.next/server/chunks/ssr/{_1eoqg_g._.js → _1f-dg-6._.js} +2 -2
  95. package/.next/server/chunks/ssr/{_0y0tiht._.js → _1fojd41._.js} +3 -3
  96. package/.next/server/chunks/ssr/_1gd8q8l._.js +3 -0
  97. package/.next/server/chunks/ssr/{_1xvgn77._.js → _1h0rw11._.js} +2 -2
  98. package/.next/server/chunks/ssr/_1jhd09t._.js +3 -0
  99. package/.next/server/chunks/ssr/{_0u2jv_x._.js → _1lz_cq2._.js} +46 -44
  100. package/.next/server/chunks/ssr/{_0kbkmar._.js → _1nnsk7z._.js} +2 -2
  101. package/.next/server/chunks/ssr/_1sji22n._.js +1 -1
  102. package/.next/server/chunks/ssr/_1vyd1ji._.js +1 -1
  103. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  104. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  105. package/.next/server/chunks/ssr/{node_modules_0z5tsu8._.js → node_modules_00etblx._.js} +1 -1
  106. package/.next/server/chunks/ssr/{node_modules_1h4a4pp._.js → node_modules_04jf_m8._.js} +1 -1
  107. package/.next/server/chunks/ssr/{node_modules_0h5hnr1._.js → node_modules_08dhkiq._.js} +1 -1
  108. package/.next/server/chunks/ssr/{node_modules_1vqb6po._.js → node_modules_0940ns5._.js} +1 -1
  109. package/.next/server/chunks/ssr/{node_modules_1i7u1ia._.js → node_modules_09q35v_._.js} +1 -1
  110. package/.next/server/chunks/ssr/{node_modules_124bue5._.js → node_modules_0_-_a--._.js} +1 -1
  111. package/.next/server/chunks/ssr/{node_modules_0h7u60n._.js → node_modules_0arus10._.js} +1 -1
  112. package/.next/server/chunks/ssr/{node_modules_0rjg6t1._.js → node_modules_0b__sgl._.js} +1 -1
  113. package/.next/server/chunks/ssr/{node_modules_113tz0l._.js → node_modules_0cxaxk9._.js} +1 -1
  114. package/.next/server/chunks/ssr/{node_modules_0lm7_vm._.js → node_modules_0g7fzk3._.js} +1 -1
  115. package/.next/server/chunks/ssr/{node_modules_0h50b8n._.js → node_modules_0had7xd._.js} +1 -1
  116. package/.next/server/chunks/ssr/{node_modules_1-hc6uv._.js → node_modules_0pvd7q-._.js} +1 -1
  117. package/.next/server/chunks/ssr/{node_modules_1bc7gu8._.js → node_modules_0yugy6m._.js} +1 -1
  118. package/.next/server/chunks/ssr/{node_modules_0y_cded._.js → node_modules_14om4ar._.js} +1 -1
  119. package/.next/server/chunks/ssr/{node_modules_0zp9r92._.js → node_modules_14q6g4n._.js} +1 -1
  120. package/.next/server/chunks/ssr/{node_modules_0eu1va6._.js → node_modules_198nmll._.js} +1 -1
  121. package/.next/server/chunks/ssr/{node_modules_1t9cyee._.js → node_modules_1_pc9kq._.js} +1 -1
  122. package/.next/server/chunks/ssr/{node_modules_063xwpp._.js → node_modules_1aaxuef._.js} +1 -1
  123. package/.next/server/chunks/ssr/{node_modules_0bvraul._.js → node_modules_1cx_ql6._.js} +1 -1
  124. package/.next/server/chunks/ssr/{node_modules_1xx28h1._.js → node_modules_1cz2hle._.js} +1 -1
  125. package/.next/server/chunks/ssr/{node_modules_1l0qtdu._.js → node_modules_1efrqxi._.js} +1 -1
  126. package/.next/server/chunks/ssr/{node_modules_1juq3hs._.js → node_modules_1eg7m6c._.js} +1 -1
  127. package/.next/server/chunks/ssr/{node_modules_1_n3ztw._.js → node_modules_1gdh31p._.js} +1 -1
  128. package/.next/server/chunks/ssr/{node_modules_0mwym0g._.js → node_modules_1hpopmo._.js} +1 -1
  129. package/.next/server/chunks/ssr/{node_modules_1bgb6-f._.js → node_modules_1qpjvy5._.js} +1 -1
  130. package/.next/server/chunks/ssr/{node_modules_1uuj9x1._.js → node_modules_1vh5ntv._.js} +1 -1
  131. 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_1b748ic._.js} +1 -1
  132. 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_1z4hm5e._.js} +1 -1
  133. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_0qe30a_._.js → node_modules_@vercel_oidc_dist_06mk8id._.js} +1 -1
  134. package/.next/server/middleware-build-manifest.js +3 -3
  135. package/.next/server/pages/404.html +1 -1
  136. package/.next/server/pages/500.html +1 -1
  137. package/.next/server/server-reference-manifest.js +1 -1
  138. package/.next/server/server-reference-manifest.json +41 -41
  139. package/.next/static/chunks/00rd4-v8r7axp.js +1 -0
  140. package/.next/static/chunks/{0tmgrnlje14-s.js → 08qe4as2q428z.js} +2 -2
  141. package/.next/static/chunks/{3ok9_x74rip3v.js → 0fl3gwe4-l6tz.js} +1 -1
  142. package/.next/static/chunks/{2a0k6jlq0_v1u.js → 0gnqdjn2lvdad.js} +2 -2
  143. package/.next/static/chunks/{1okqiww3pnwka.js → 0hwap1-p0m5sa.js} +1 -1
  144. package/.next/static/chunks/{1ekc7ul556kbl.js → 0w52rrrmuj9r9.js} +1 -1
  145. package/.next/static/chunks/{3agpgkwaa321p.js → 0wcprt5ot5she.js} +1 -1
  146. package/.next/static/chunks/{3xpbkl66ssib5.js → 15m575979un5f.js} +6 -6
  147. package/.next/static/chunks/{2fzs9fm8yarvq.js → 17p6tk6k88dq6.js} +1 -1
  148. package/.next/static/chunks/{26-pr2c9ro222.js → 1jd09sxcjddu1.js} +1 -1
  149. package/.next/static/chunks/{280t0_e99i276.js → 1w73ece_6bheu.js} +1 -1
  150. package/.next/static/chunks/{1grhjo7a5a0_-.js → 214ya3n5gxeih.js} +1 -1
  151. package/.next/static/chunks/22_zkas21rpna.js +1 -0
  152. package/.next/static/chunks/{0x79tx04dxbpp.js → 2u7xt6mxg44lf.js} +1 -1
  153. package/.next/static/chunks/{17donp69y4do7.js → 39j0fvpybzabl.js} +8 -8
  154. package/.next/static/chunks/{28cizcdaabd4h.js → 3akmqm77muj4-.js} +1 -1
  155. package/.next/static/chunks/3cq5g494yuke8.css +1 -0
  156. package/.next/static/chunks/3woj-xd7ki5_i.js +1 -0
  157. package/.next/static/chunks/{3cfi_q6d_wtci.js → 3zmcsnuo25zvq.js} +1 -1
  158. package/README.md +46 -23
  159. package/app/api/events/app-event.ts +0 -2
  160. package/bin/port.mjs +65 -0
  161. package/bin/thursday.mjs +2 -33
  162. package/docs/how-it-works.md +11 -7
  163. package/docs/images/face-dark.png +0 -0
  164. package/docs/images/face-light.png +0 -0
  165. package/docs/images/keeps-going.png +0 -0
  166. package/features/ai/chatgpt.ts +42 -20
  167. package/features/ai/load-tools.ts +20 -14
  168. package/features/ai/prompts/bot.prompt.ts +90 -88
  169. package/features/ai/prompts/thursday.prompt.ts +48 -41
  170. package/features/ai/tools/memory.tool.ts +65 -62
  171. package/features/ai/tools/tool-name.ts +0 -1
  172. package/features/ai/tools/workspace.tool.ts +3 -0
  173. package/features/bot/bot.action.ts +6 -1
  174. package/features/bot/bot.run.ts +11 -7
  175. package/features/bot/bot.runner.ts +34 -16
  176. package/features/bot/bot.schema.ts +25 -0
  177. package/features/bot/components/bot-room.tsx +57 -4
  178. package/features/bot/components/bot-tool.tsx +0 -1
  179. package/features/bot/task.query.ts +25 -6
  180. package/features/bot/task.store.ts +23 -4
  181. package/features/memory/components/memory-edit.tsx +7 -5
  182. package/features/thursday/components/thursday.tsx +0 -3
  183. package/features/thursday/tool-line.ts +0 -1
  184. package/features/thursday/use-thursday.ts +1 -1
  185. package/package.json +1 -1
  186. package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +0 -3
  187. package/.next/server/chunks/ssr/_14_lw10._.js +0 -3
  188. package/.next/server/chunks/ssr/_1spzm8z._.js +0 -3
  189. package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +0 -3
  190. package/.next/static/chunks/0xzgvc0fkdb6t.js +0 -1
  191. package/.next/static/chunks/14hgtsjg-kael.css +0 -1
  192. package/.next/static/chunks/2hw2f6zmt7fmt.js +0 -1
  193. package/.next/static/chunks/3111pj5wnjgos.js +0 -1
  194. package/features/memory/components/memory-view.tsx +0 -84
  195. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_buildManifest.js +0 -0
  196. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_clientMiddlewareManifest.js +0 -0
  197. /package/.next/static/{x1cCqY0kWpR_VcYx_lWO5 → kaTO0UHqF1bMnWgCDik2f}/_ssgManifest.js +0 -0
@@ -1,6 +1,5 @@
1
1
  import { tool } from "ai";
2
2
  import * as z from "zod";
3
- import { appEvents } from "@/app/api/events/app-event.server";
4
3
  import { MEMORY_CONVERSATION_PAGE, MEMORY_LIMITS } from "@/config";
5
4
  import {
6
5
  callStamp,
@@ -57,9 +56,9 @@ const noConversation = (source: MemorySource | null) =>
57
56
  /**
58
57
  * Said when a note has outgrown the recommended size, and only then — the write
59
58
  * itself always goes through. It names no tool: this one answers the call and a
60
- * bot, and only one of them has a screen to put a note on. How to do it is the
61
- * call prompt's (prompts/thursday.prompt); the ask is the same either way,
62
- * because a bot that turned up a fact can hand the same request back with it.
59
+ * bot, and how to settle it with the user is the call's to say (prompts/thursday.prompt);
60
+ * the ask is the same either way, because a bot that turned up a fact can hand
61
+ * the same request back with it.
63
62
  */
64
63
  const overSize = (count: number) =>
65
64
  count > MEMORY_LIMITS.factsPerNote
@@ -114,7 +113,9 @@ export const createMemoryTools = (
114
113
  .object({
115
114
  text: z
116
115
  .string()
117
- .describe("One statement that stands on its own later."),
116
+ .describe(
117
+ 'One statement that stands on its own later — a date as a date, never "next week".',
118
+ ),
118
119
  replaces: z
119
120
  .number()
120
121
  .int()
@@ -126,7 +127,7 @@ export const createMemoryTools = (
126
127
  .boolean()
127
128
  .nullish()
128
129
  .describe(
129
- `True carries this line into every call's instructions without opening its note — for what to call them, their language and register, a standing rule. At most ${MEMORY_LIMITS.carried}. Null leaves it; false makes a carried line an ordinary fact.`,
130
+ `True carries this line into every call's instructions without opening its note — for what every call needs before any note is opened: what to call them, their language and register. At most ${MEMORY_LIMITS.carried}. Null leaves it; false makes a carried line an ordinary fact.`,
130
131
  ),
131
132
  })
132
133
  .array()
@@ -175,23 +176,23 @@ export const createMemoryTools = (
175
176
  };
176
177
  }
177
178
  // A new path outside the convention goes to inbox, which keeps its own line and takes no aliases
178
- const filed =
179
- known || isMemoryPath(said)
180
- ? { path: target, description, aliases, facts }
181
- : {
182
- path: MEMORY_INBOX,
183
- description: appNoteLine(MEMORY_INBOX),
184
- facts,
185
- };
179
+ const toInbox = !known && !isMemoryPath(said);
180
+ const filed = toInbox
181
+ ? {
182
+ path: MEMORY_INBOX,
183
+ description: appNoteLine(MEMORY_INBOX),
184
+ facts,
185
+ }
186
+ : { path: target, description, aliases, facts };
186
187
 
187
188
  const write = await writeNotes([filed], source, callId);
188
189
 
189
190
  // The write already succeeded; what follows are requests, not failures.
190
- // A path filed elsewhere must be said, or the model reports it saved where it asked.
191
- const elsewhere =
192
- filed.path !== said
193
- ? ` Filed under ${filed.path}: "${said}" is not a path this listing can carry. If that is not where it belongs, write it again under one that is.`
194
- : "";
191
+ // Only a path that landed in inbox is said: one reached by a name the
192
+ // listing carries is where it was asked for, however it was spelled.
193
+ const elsewhere = toInbox
194
+ ? ` Filed under ${filed.path}: "${said}" is not a path this listing can carry. If that is not where it belongs, write it again under one that is.`
195
+ : "";
195
196
 
196
197
  const unnamed = write.unnamed.length
197
198
  ? ` ${write.unnamed[0]} is new and has no line yet — the listing shows its first fact instead. Call again with \`description\` (one line about what it is) and \`aliases\` (the names they say for it).`
@@ -265,59 +266,61 @@ export const createMemoryTools = (
265
266
  },
266
267
  }),
267
268
 
268
- [TOOL_NAMES.memory_show]: tool({
269
- description: `Put a note on the user's screen so they can see it while you
270
- talk about it, and take it away again. They cannot edit it there — they say
271
- what to drop and you drop it.`,
272
- inputSchema: z.object({
273
- path: z
274
- .string()
275
- .nullable()
276
- .describe(
277
- "The note to put on screen, exactly as the listing writes it. Null takes it away.",
278
- ),
279
- }),
280
- execute: async ({ path }) => {
281
- if (!path?.trim()) {
282
- appEvents.emit({ type: "memory-view", path: null });
283
- return { note: "Taken off the screen." };
284
- }
285
- // The screen gets only the path and reads facts over GET, so deletions show up on their own
286
- const { notes } = await readNotes([path], { touch: false });
287
- const note = notes[0];
288
- if (!note) {
289
- return { note: `Nothing on the listing called ${path.trim()}.` };
290
- }
291
- appEvents.emit({ type: "memory-view", path: note.path });
292
- return {
293
- ...note,
294
- note: "It is on their screen now, in this order. Ask what they no longer need, and forget only what they name.",
295
- };
296
- },
297
- }),
298
-
299
269
  [TOOL_NAMES.memory_forget]: tool({
300
- description: "Delete one fact for good.",
270
+ description: "Delete facts for good.",
301
271
  inputSchema: z.object({
302
- factId: z
272
+ factIds: z
303
273
  .number()
304
274
  .int()
305
- .describe("The id that came with the fact when the note was opened."),
275
+ .array()
276
+ .min(1)
277
+ .describe(
278
+ "The ids that came with the facts when their notes were opened — every one to delete, in one call.",
279
+ ),
306
280
  }),
307
- execute: async ({ factId }) => {
308
- const forgotten = await forgetFactById(factId);
309
- if (!forgotten) {
310
- return { note: `No fact with id ${factId}. Nothing was deleted.` };
281
+ execute: async ({ factIds }) => {
282
+ const missing: number[] = [];
283
+ /** Notes that lost their last fact, and went with it (memory.query forgetFactById). */
284
+ const gone: string[] = [];
285
+ /** Notes that still hold something, handed back as they are now. */
286
+ const left = new Set<string>();
287
+ for (const id of new Set(factIds)) {
288
+ const forgotten = await forgetFactById(id);
289
+ if (!forgotten) {
290
+ missing.push(id);
291
+ continue;
292
+ }
293
+ if (forgotten.noteGone) {
294
+ left.delete(forgotten.path);
295
+ gone.push(forgotten.path);
296
+ } else {
297
+ left.add(forgotten.path);
298
+ }
311
299
  }
312
300
 
313
- // The note goes with its last fact; the listing still shows it until the next session, so say so
314
- if (forgotten.noteGone) {
301
+ if (!gone.length && !left.size) {
315
302
  return {
316
- note: `${GONE} That was the last fact under ${forgotten.path}, so the note went with it.`,
303
+ note: `No fact with id ${missing.join(", ")}. Nothing was deleted.`,
317
304
  };
318
305
  }
319
- const { notes } = await readNotes([forgotten.path], { touch: false });
320
- return { ...notes[0], note: GONE };
306
+
307
+ // The notes that went are still on the listing until the next session, so say so
308
+ const said = [
309
+ GONE,
310
+ gone.length === 1
311
+ ? `That was the last fact under ${gone[0]}, so the note went with it.`
312
+ : gone.length
313
+ ? `That was the last of ${gone.join(", ")}, so those notes went with them.`
314
+ : "",
315
+ missing.length
316
+ ? `No fact with id ${missing.join(", ")}, so those were skipped.`
317
+ : "",
318
+ ]
319
+ .filter(Boolean)
320
+ .join(" ");
321
+ if (!left.size) return { note: said };
322
+ const { notes } = await readNotes([...left], { touch: false });
323
+ return { notes: notes.map(withCount), note: said };
321
324
  },
322
325
  }),
323
326
  });
@@ -7,7 +7,6 @@ export const TOOL_NAMES = {
7
7
  memory_recall: "memory_recall",
8
8
  memory_remember: "memory_remember",
9
9
  memory_forget: "memory_forget",
10
- memory_show: "memory_show",
11
10
  memory_conversation: "memory_conversation",
12
11
 
13
12
  bash: "bash",
@@ -18,6 +18,8 @@ export const createWorkspaceTools = (
18
18
  env?: Record<string, string>;
19
19
  /** Attach the shell guide to this run's first `bash` result (bots only). */
20
20
  guide?: boolean;
21
+ /** How long one command may run; unset is the sandbox's own limit (config EXEC_TIMEOUT_MS). */
22
+ timeoutMs?: number;
21
23
  },
22
24
  ): ToolSet => {
23
25
  /** Fold absolute sandbox paths against cwd; they still resolve when handed back. */
@@ -45,6 +47,7 @@ export const createWorkspaceTools = (
45
47
  const result = await sandbox.exec(command, {
46
48
  signal: abortSignal,
47
49
  env: options.env,
50
+ timeoutMs: options.timeoutMs,
48
51
  });
49
52
  if (!owed) return result;
50
53
  owed = false;
@@ -110,7 +110,12 @@ export const startTaskAction = serverAction(
110
110
  const worker = await findBot(bot);
111
111
  if (!worker) publicError(`No bot called "${bot}".`);
112
112
  const label = labelFor(said);
113
- const id = await startTask({ bot: worker.name, request: said, label });
113
+ const id = await startTask({
114
+ bot: worker.name,
115
+ request: said,
116
+ label,
117
+ from: "user",
118
+ });
114
119
  return { id, label, bot: worker.name };
115
120
  },
116
121
  );
@@ -344,6 +344,7 @@ export async function runBot(
344
344
  const summary = await compact(model.model, agentTools, messages, {
345
345
  signal: options.signal,
346
346
  budget,
347
+ instructions: prompt.text,
347
348
  }).finally(quiet.release);
348
349
  logger.debug(
349
350
  `compacted ${messages.length} messages at ${size} tokens (budget ${budget})`,
@@ -701,17 +702,19 @@ async function filesUnder(
701
702
  /**
702
703
  * The model summarizes its own context and continues from the summary. Tools
703
704
  * are passed with calling off: providers refuse a history of tool calls
704
- * without the tools that made them. A context too long to summarise whole —
705
- * the very thing a compaction is for — is tried once more without the tool
706
- * calls and results but the last few. A failure throws with its cause kept:
707
- * the runner parks what a retry or a smaller budget can fix (bot.runner
708
- * parkTask) and fails the rest; the thread stays.
705
+ * without the tools that made them. The run's own instructions go too: the
706
+ * summary is written by the bot the thread belongs to, and the provider's
707
+ * cached prefix, instructions first, still matches the run's. A context too
708
+ * long to summarise whole — the very thing a compaction is for — is tried once
709
+ * more without the tool calls and results but the last few. A failure throws
710
+ * with its cause kept: the runner parks what a retry or a smaller budget can
711
+ * fix (bot.runner parkTask) and fails the rest; the thread stays.
709
712
  */
710
713
  async function compact(
711
714
  model: LanguageModel,
712
715
  tools: ToolSet,
713
716
  messages: ModelMessage[],
714
- options: { signal?: AbortSignal; budget: number },
717
+ options: { signal?: AbortSignal; budget: number; instructions: string },
715
718
  ): Promise<{ text: string; usage: TokenUsage }> {
716
719
  let failure: unknown;
717
720
  try {
@@ -745,10 +748,11 @@ async function summarize(
745
748
  model: LanguageModel,
746
749
  tools: ToolSet,
747
750
  messages: ModelMessage[],
748
- options: { signal?: AbortSignal; budget: number },
751
+ options: { signal?: AbortSignal; budget: number; instructions: string },
749
752
  ): Promise<{ text: string; usage: TokenUsage }> {
750
753
  const { text, usage } = await generateText({
751
754
  model,
755
+ system: options.instructions,
752
756
  tools,
753
757
  toolChoice: "none",
754
758
  messages: [
@@ -34,7 +34,13 @@ import { logger } from "@/lib/logger";
34
34
  import { publicError } from "@/lib/public-error";
35
35
  import { createKeyedLock } from "@/lib/queue";
36
36
  import { resumeThread, runBot, type TaskEvent } from "./bot.run";
37
- import { TASK_CONTINUE, type TaskPending, type TaskStatus } from "./bot.schema";
37
+ import {
38
+ TASK_CONTINUE,
39
+ type TaskPending,
40
+ type TaskSpeaker,
41
+ type TaskStatus,
42
+ tagSpeaker,
43
+ } from "./bot.schema";
38
44
  import {
39
45
  addTaskUsage,
40
46
  countStopsSinceSpoken,
@@ -49,6 +55,7 @@ import {
49
55
  listRunningTaskIds,
50
56
  listTaskFolders,
51
57
  listThread,
58
+ stopNote,
52
59
  type TaskMessageInput,
53
60
  updateTask,
54
61
  upsertMessage,
@@ -99,24 +106,28 @@ const pausing = ((globalThis as Pinned).__botPausing ??= {
99
106
  current: Promise.resolve(),
100
107
  });
101
108
 
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. */
109
+ /** 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
110
  export async function startTask(input: {
104
111
  bot: string;
105
112
  request: string;
106
113
  label: string;
107
114
  callId?: string | null;
115
+ /** Who handed it over: Thursday during a call, or the user on screen. */
116
+ from: TaskSpeaker;
108
117
  }) {
109
- const conversation = input.callId
110
- ? await listCallTurns(input.callId, OPENING_TURNS)
118
+ const { from, ...row } = input;
119
+ const conversation = row.callId
120
+ ? await listCallTurns(row.callId, OPENING_TURNS)
111
121
  : [];
112
122
  const opening = buildTaskOpening({
113
- bot: input.bot,
114
- request: input.request,
123
+ bot: row.bot,
124
+ request: row.request,
115
125
  conversation,
126
+ from,
116
127
  });
117
- const task = await insertTask({ ...input, opening });
128
+ const task = await insertTask({ ...row, opening });
118
129
  launch(task.id, {
119
- bot: input.bot,
130
+ bot: row.bot,
120
131
  messages: [{ role: "user", content: opening }],
121
132
  });
122
133
  return task.id;
@@ -128,14 +139,25 @@ export async function startTask(input: {
128
139
  * before its next step; the step in flight finishes), or a follow-up to a
129
140
  * finished one. Only when the run has already ended does the running case
130
141
  * fall through to stop, append, relaunch.
142
+ *
143
+ * @param from Who said it. The words go on the thread tagged with it (bot.schema
144
+ * tagSpeaker): Thursday passing something on from a call and the user typing on
145
+ * screen reach the bot down this one pipe, and a go-ahead means something
146
+ * different from each. Unset for words that already carry their tags — the notes
147
+ * a finished run left unread (drive).
131
148
  */
132
- export async function answerTask(id: string, answer: string) {
149
+ export async function answerTask(
150
+ id: string,
151
+ answer: string,
152
+ from?: TaskSpeaker,
153
+ ) {
154
+ const said = from ? tagSpeaker(from, answer) : answer;
133
155
  return taskLock(id, async () => {
134
156
  const task = await findTask(id);
135
157
  if (!task) publicError("No such job.");
136
158
 
137
159
  const live = running.get(id);
138
- if (live && task.status === "running" && live.note(answer)) {
160
+ if (live && task.status === "running" && live.note(said)) {
139
161
  logger.debug(`task ${id}: interjection queued`);
140
162
  return;
141
163
  }
@@ -147,7 +169,7 @@ export async function answerTask(id: string, answer: string) {
147
169
  forgetResume(id);
148
170
 
149
171
  const thread = await listThread(id);
150
- const { reply, as } = joinThread(task, answer, thread);
172
+ const { reply, as } = joinThread(task, said, thread);
151
173
  logger.debug(`task ${id}: ${as}`);
152
174
 
153
175
  await upsertMessage(id, (await lastSeq(id)) + 1, {
@@ -465,7 +487,7 @@ async function parkTask(
465
487
  const inMs =
466
488
  auto && stop.retry ? waits[Math.min(stops, waits.length - 1)] : 0;
467
489
 
468
- await writeNote(id, `${stop.why} ${IN_FLIGHT}`).catch((cause) =>
490
+ await writeNote(id, stopNote(stop.why)).catch((cause) =>
469
491
  logger.warn(`task ${id}: stop not written`, cause),
470
492
  );
471
493
  await writeEnding(id, {
@@ -485,10 +507,6 @@ async function parkTask(
485
507
  if (auto) scheduleResume(id, inMs);
486
508
  }
487
509
 
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
510
  const spell = (ms: number) =>
493
511
  ms < 60_000
494
512
  ? `${Math.round(ms / 1000)} seconds`
@@ -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.
@@ -688,6 +689,13 @@ export async function readToolResult(
688
689
  return null;
689
690
  }
690
691
 
692
+ /** Said to a resumed run after why it stopped. The screen shows only the why (linesOf). */
693
+ const IN_FLIGHT =
694
+ "Anything that was under way — a command, a page loading, a download — may not have finished: check before relying on it, then carry on.";
695
+
696
+ /** The thread line a stop the app made leaves (bot.runner parkTask). */
697
+ export const stopNote = (why: string) => `${why} ${IN_FLIGHT}`;
698
+
691
699
  type StoredMessage = typeof taskMessageTable.$inferSelect;
692
700
 
693
701
  /**
@@ -710,11 +718,17 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
710
718
  if (message.seq === 0) return [];
711
719
  const text = typeof content === "string" ? content : textOf(content);
712
720
  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 }];
721
+ // The app's own lines, never the user's words. `compact` alone marks a
722
+ // compaction: rows written before `note` existed carry only that.
723
+ if (message.compact) return [{ ...base, id: id(0), kind: "note", text }];
724
+ if (message.note) {
725
+ const why = text.endsWith(IN_FLIGHT)
726
+ ? text.slice(0, -IN_FLIGHT.length).trimEnd()
727
+ : text;
728
+ return [{ ...base, id: id(0), kind: "stop", text: why }];
729
+ }
730
+ // Tagged with who said it for the bot (bot.schema tagSpeaker); the screen draws the words
731
+ return [{ ...base, id: id(0), kind: "user", text: untagSpeaker(text) }];
718
732
  }
719
733
 
720
734
  if (message.role === "assistant") {
@@ -817,7 +831,12 @@ function linesOf(message: StoredMessage, owner: string): TaskLine[] {
817
831
  const text = resultParts(part.output, RESULT_LINES)
818
832
  .flatMap((result) => (result.type === "text" ? [result.text] : []))
819
833
  .join("\n");
820
- lines.push({ ...base, id: id(index), kind: "user", text });
834
+ lines.push({
835
+ ...base,
836
+ id: id(index),
837
+ kind: "user",
838
+ text: untagSpeaker(text),
839
+ });
821
840
  } else if (
822
841
  part.toolName === TOOL_NAMES.ask_bot ||
823
842
  part.toolName === TOOL_NAMES.ask_back
@@ -24,7 +24,11 @@ import { errorToString } from "@/lib/utils";
24
24
 
25
25
  export type BotRef = { name: string; icon?: BotIcon | null };
26
26
 
27
- /** `note` is a marker, not speech: a compaction summary (bot.run compact). Draw as a divider. */
27
+ /**
28
+ * `note` and `stop` are the app's markers, not speech. `note` is a compaction
29
+ * summary (bot.run compact), drawn as a divider; `stop` is where the app stopped
30
+ * the run (bot.runner parkTask), drawn muted in the bot's turn.
31
+ */
28
32
  export type ChatterKind =
29
33
  | "say"
30
34
  | "ask"
@@ -32,7 +36,8 @@ export type ChatterKind =
32
36
  | "user"
33
37
  | "result"
34
38
  | "error"
35
- | "note";
39
+ | "note"
40
+ | "stop";
36
41
 
37
42
  /** One tool call. `name` picks the renderer (components/bot-tool); `results` arrive after the call. */
38
43
  export type ToolUse = {
@@ -63,6 +68,8 @@ export type Chatter = {
63
68
  tool?: ToolUse;
64
69
  /** Options attached to a question; only on the line where the bot stopped to ask. */
65
70
  options?: string[];
71
+ /** When it was written; only for kind `stop`, whose repeats fold into one line. */
72
+ at?: DateLike;
66
73
  };
67
74
 
68
75
  /** `waiting`: the bot stopped to ask the user something. */
@@ -159,6 +166,16 @@ export function taskFromRow(row: Task, bots?: Bot[]): TaskView {
159
166
  case "note":
160
167
  lines.push({ id: line.id, bot, to, text: line.text, kind: "note" });
161
168
  break;
169
+ case "stop":
170
+ lines.push({
171
+ id: line.id,
172
+ bot,
173
+ to,
174
+ text: line.text,
175
+ kind: "stop",
176
+ at: line.at,
177
+ });
178
+ break;
162
179
  case "tool":
163
180
  openLines.set(line.callId, lines.length);
164
181
  lines.push({
@@ -264,7 +281,8 @@ export function taskFromRow(row: Task, bots?: Bot[]): TaskView {
264
281
  export function lastSaid(task: TaskView): Chatter | null {
265
282
  for (let at = task.lines.length - 1; at >= 0; at--) {
266
283
  const line = task.lines[at];
267
- if (line.kind !== "user" && line.kind !== "note") return line;
284
+ if (line.kind !== "user" && line.kind !== "note" && line.kind !== "stop")
285
+ return line;
268
286
  }
269
287
  return null;
270
288
  }
@@ -412,7 +430,8 @@ export function latestPerBot(list: TaskView[]): BotLine[] {
412
430
  byBot.set(task.bot.name, { bot: task.bot, task, line: null, open: 0 });
413
431
  }
414
432
  for (const line of task.lines) {
415
- if (line.kind === "user" || line.kind === "note") continue;
433
+ if (line.kind === "user" || line.kind === "note" || line.kind === "stop")
434
+ continue;
416
435
  byBot.set(line.bot.name, {
417
436
  bot: line.bot,
418
437
  task,