thursday-agent 0.4.0 → 0.6.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 (657) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +3 -2
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.json +8 -1
  6. package/.next/routes-manifest.json +18 -6
  7. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/server/app/api/artifact/route.js +10 -6
  21. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/memory/files/route.js +6 -5
  23. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  24. package/.next/server/app/api/bot/memory/route.js +8 -8
  25. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  26. package/.next/server/app/api/bot/route.js +8 -8
  27. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  28. package/.next/server/app/api/bot/thread/[id]/route/app-paths-manifest.json +3 -0
  29. package/.next/server/app/api/bot/thread/[id]/route.js +15 -0
  30. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -0
  31. package/.next/server/app/api/bot/thread/[id]/route_client-reference-manifest.js +3 -0
  32. package/.next/server/app/api/bot/thread/route/app-paths-manifest.json +3 -0
  33. package/.next/server/app/api/bot/thread/route.js +15 -0
  34. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -0
  35. package/.next/server/app/api/bot/thread/route_client-reference-manifest.js +3 -0
  36. package/.next/server/app/api/config/route.js +7 -7
  37. package/.next/server/app/api/config/route.js.nft.json +1 -1
  38. package/.next/server/app/api/events/route.js +3 -3
  39. package/.next/server/app/api/events/route.js.nft.json +1 -1
  40. package/.next/server/app/api/file/[...path]/route.js +5 -5
  41. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  42. package/.next/server/app/api/llm-model/catalog/route.js +8 -8
  43. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  44. package/.next/server/app/api/llm-model/credits/route.js +8 -8
  45. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  46. package/.next/server/app/api/llm-model/route.js +8 -8
  47. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  48. package/.next/server/app/api/llm-model/subscription/route.js +8 -8
  49. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  50. package/.next/server/app/api/mcp/[name]/route.js +6 -6
  51. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  52. package/.next/server/app/api/mcp/oauth/callback/route.js +4 -4
  53. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  54. package/.next/server/app/api/mcp/route.js +6 -6
  55. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  56. package/.next/server/app/api/mcp/tools/route.js +6 -6
  57. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  58. package/.next/server/app/api/memory/edit/route.js +13 -13
  59. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  60. package/.next/server/app/api/memory/route.js +7 -6
  61. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  62. package/.next/server/app/api/skills/[source]/[name]/route.js +6 -6
  63. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/skills/route.js +6 -6
  65. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  66. package/.next/server/app/api/thursday/call/route.js +10 -11
  67. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  68. package/.next/server/app/api/thursday/skills/route.js +10 -11
  69. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  70. package/.next/server/app/api/thursday/tool-call/route.js +15 -15
  71. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  72. package/.next/server/app/api/workspace/missing/route/app-paths-manifest.json +3 -0
  73. package/.next/server/app/api/workspace/missing/route/build-manifest.json +12 -0
  74. package/.next/server/app/api/workspace/missing/route/server-reference-manifest.json +4 -0
  75. package/.next/server/app/api/workspace/missing/route.js +12 -0
  76. package/.next/server/app/api/workspace/missing/route.js.map +5 -0
  77. package/.next/server/app/api/workspace/missing/route.js.nft.json +1 -0
  78. package/.next/server/app/api/workspace/missing/route_client-reference-manifest.js +3 -0
  79. package/.next/server/app/api/workspace/route.js +5 -4
  80. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  81. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  82. package/.next/server/app/artifact/[...path]/page.js +4 -3
  83. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  84. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  86. package/.next/server/app/page/server-reference-manifest.json +67 -45
  87. package/.next/server/app/page.js +10 -9
  88. package/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app-paths-manifest.json +3 -2
  91. package/.next/server/chunks/[root-of-the-server]__02846fb._.js +8 -0
  92. package/.next/server/chunks/[root-of-the-server]__049f521._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__05luceg._.js +3 -0
  94. package/.next/server/chunks/[root-of-the-server]__06--3q3._.js +8 -0
  95. package/.next/server/chunks/[root-of-the-server]__07bnah_._.js +3 -0
  96. package/.next/server/chunks/[root-of-the-server]__08eae20._.js +8 -0
  97. package/.next/server/chunks/[root-of-the-server]__0_ue8oe._.js +3 -0
  98. package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +5 -0
  99. package/.next/server/chunks/[root-of-the-server]__0g-ueei._.js +8 -0
  100. package/.next/server/chunks/[root-of-the-server]__0h_x6q6._.js +5 -0
  101. package/.next/server/chunks/{[root-of-the-server]__0pa2n68._.js → [root-of-the-server]__0kxrvxf._.js} +2 -2
  102. package/.next/server/chunks/{[root-of-the-server]__0q0vvt2._.js → [root-of-the-server]__0ov3iir._.js} +2 -2
  103. package/.next/server/chunks/[root-of-the-server]__0qwtlmx._.js +8 -0
  104. package/.next/server/chunks/[root-of-the-server]__0sm5xvm._.js +3 -0
  105. package/.next/server/chunks/[root-of-the-server]__0ye3da2._.js +3 -0
  106. package/.next/server/chunks/[root-of-the-server]__0zp35ul._.js +5 -0
  107. package/.next/server/chunks/[root-of-the-server]__144u9d_._.js +3 -0
  108. package/.next/server/chunks/[root-of-the-server]__18-2tsq._.js +5 -0
  109. package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +3 -0
  110. package/.next/server/chunks/[root-of-the-server]__1e2365e._.js +8 -0
  111. package/.next/server/chunks/[root-of-the-server]__1fix932._.js +8 -0
  112. package/.next/server/chunks/{[root-of-the-server]__0ce8nf8._.js → [root-of-the-server]__1g3t7vn._.js} +3 -3
  113. package/.next/server/chunks/[root-of-the-server]__1kp-83u._.js +5 -0
  114. package/.next/server/chunks/[root-of-the-server]__1kv8p3e._.js +5 -0
  115. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +2 -3
  116. package/.next/server/chunks/_02szwk8._.js +5 -0
  117. package/.next/server/chunks/_05h2cii._.js +1 -1
  118. package/.next/server/chunks/_06c9kie._.js +216 -0
  119. package/.next/server/chunks/{_1x6kd1r._.js → _07y2gj-._.js} +1 -1
  120. package/.next/server/chunks/_0a43a-0._.js +3 -0
  121. package/.next/server/chunks/_0bujjey._.js +5 -0
  122. package/.next/server/chunks/_0c9j6rb._.js +1 -1
  123. package/.next/server/chunks/_0dkd549._.js +3 -0
  124. package/.next/server/chunks/_0ej1kvv._.js +3 -0
  125. package/.next/server/chunks/_0f6ts7z._.js +3 -0
  126. package/.next/server/chunks/_0gm9hyv._.js +55 -0
  127. package/.next/server/chunks/_0i55en5._.js +25 -0
  128. package/.next/server/chunks/_0k50n27._.js +18 -0
  129. package/.next/server/chunks/_0khihjk._.js +3 -3
  130. package/.next/server/chunks/_0n74zsf._.js +5 -5
  131. package/.next/server/chunks/_0nvm9ef._.js +3 -0
  132. package/.next/server/chunks/_0p56fn0._.js +3 -0
  133. package/.next/server/chunks/_0recufk._.js +1 -1
  134. package/.next/server/chunks/_0xbkz10._.js +3 -0
  135. package/.next/server/chunks/_10tkr26._.js +3 -0
  136. package/.next/server/chunks/{_0ioz3nd._.js → _11u2kp7._.js} +1 -1
  137. package/.next/server/chunks/_11ujpya._.js +3 -0
  138. package/.next/server/chunks/_127nx_p._.js +5 -0
  139. package/.next/server/chunks/_17l5ygt._.js +1 -1
  140. package/.next/server/chunks/_19uauj6._.js +3 -0
  141. package/.next/server/chunks/_1_5mnq4._.js +5 -5
  142. package/.next/server/chunks/_1abl6t9._.js +55 -0
  143. package/.next/server/chunks/_1d0j961._.js +3 -3
  144. package/.next/server/chunks/_1dbixxz._.js +3 -0
  145. package/.next/server/chunks/_1dc_d1q._.js +98 -0
  146. package/.next/server/chunks/_1drgfx4._.js +5 -5
  147. package/.next/server/chunks/_1e48czp._.js +3 -3
  148. package/.next/server/chunks/_1f31u5x._.js +5 -0
  149. package/.next/server/chunks/_1kg70wc._.js +3 -3
  150. package/.next/server/chunks/_1klbcft._.js +5 -0
  151. package/.next/server/chunks/_1krd9rf._.js +4 -1
  152. package/.next/server/chunks/_1kwmzab._.js +3 -0
  153. package/.next/server/chunks/{_1pf4qe-._.js → _1oxj9fa._.js} +1 -1
  154. package/.next/server/chunks/{_1f46eie._.js → _1pcmdbb._.js} +1 -1
  155. package/.next/server/chunks/_1qgix-j._.js +3 -0
  156. package/.next/server/chunks/{lib_logger_ts_1exwjut._.js → _1rpchta._.js} +5 -5
  157. package/.next/server/chunks/_1vsej-s._.js +5 -0
  158. package/.next/server/chunks/_1yx0p_s._.js +5 -0
  159. package/.next/server/chunks/{_0rnf-6n._.js → _20fsr_z._.js} +6 -6
  160. package/.next/server/chunks/_2128hkj._.js +5 -0
  161. package/.next/server/chunks/_next-internal_server_app_api_bot_thread_[id]_route_actions_1fstjn2.js +3 -0
  162. package/.next/server/chunks/_next-internal_server_app_api_bot_thread_route_actions_003ccso.js +3 -0
  163. package/.next/server/chunks/_next-internal_server_app_api_workspace_missing_route_actions_1fidi6p.js +3 -0
  164. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  165. package/.next/server/chunks/features_0-cnt0l._.js +4 -0
  166. package/.next/server/chunks/features_0hdaci0._.js +4 -0
  167. package/.next/server/chunks/features_1e39j7-._.js +8 -0
  168. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +1 -1
  169. package/.next/server/chunks/features_memory_1uqvksn._.js +1 -1
  170. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_0a7-pbw._.js} +1 -1
  171. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js → node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js} +1 -1
  172. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js → node_modules_consola_dist_chunks_prompt_mjs_107pxfn._.js} +1 -1
  173. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js → node_modules_consola_dist_chunks_prompt_mjs_15hbel8._.js} +1 -1
  174. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_18i-23e._.js +3 -0
  175. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1bewgf3._.js +3 -0
  176. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1kipdeo._.js +3 -0
  177. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1nlww1m._.js +3 -0
  178. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1q77nfx._.js +3 -0
  179. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xqdzx_._.js +3 -0
  180. package/.next/server/chunks/{node_modules_zod_v4_1801ax7._.js → node_modules_zod_v4_0lkux5-._.js} +2 -2
  181. package/.next/server/chunks/{node_modules_zod_v4_1cha_bw._.js → node_modules_zod_v4_1jmr9cq._.js} +2 -2
  182. package/.next/server/chunks/{node_modules_zod_v4_0ubdflj._.js → node_modules_zod_v4_1yrxmek._.js} +2 -2
  183. package/.next/server/chunks/node_modules_zod_v4_classic_errors_0tp0y-q.js +3 -0
  184. package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
  185. package/.next/server/chunks/ssr/[root-of-the-server]__0bxsfyn._.js +101 -0
  186. package/.next/server/chunks/ssr/[root-of-the-server]__0lyi8i2._.js +1 -1
  187. package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
  188. package/.next/server/chunks/ssr/[root-of-the-server]__172cq4x._.js +3 -0
  189. package/.next/server/chunks/ssr/[root-of-the-server]__18dthbp._.js +3 -0
  190. package/.next/server/chunks/ssr/[root-of-the-server]__18e68w8._.js +3 -0
  191. package/.next/server/chunks/ssr/[root-of-the-server]__1fll8dc._.js +3 -0
  192. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  193. package/.next/server/chunks/ssr/[root-of-the-server]__1nygfik._.js +5 -0
  194. package/.next/server/chunks/ssr/[root-of-the-server]__1sdphb0._.js +8 -0
  195. package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +100 -0
  196. package/.next/server/chunks/ssr/_00rehl_._.js +3 -0
  197. package/.next/server/chunks/ssr/{_1lz_cq2._.js → _02oc_h5._.js} +76 -89
  198. package/.next/server/chunks/ssr/_03_928f._.js +3 -0
  199. package/.next/server/chunks/ssr/_05yghx6._.js +3 -0
  200. package/.next/server/chunks/ssr/_08c4z-9._.js +3 -0
  201. package/.next/server/chunks/ssr/_0_qfdlw._.js +4 -0
  202. package/.next/server/chunks/ssr/_0c9e5w6._.js +9 -0
  203. package/.next/server/chunks/ssr/{_0s99bgb._.js → _0dh2e1j._.js} +9 -3
  204. package/.next/server/chunks/ssr/_0dr-ddg._.js +3 -0
  205. package/.next/server/chunks/ssr/_0eaog6_._.js +3 -0
  206. package/.next/server/chunks/ssr/_0evkpto._.js +3 -0
  207. package/.next/server/chunks/ssr/_0g_rlog._.js +3 -0
  208. package/.next/server/chunks/ssr/_0geg212._.js +3 -0
  209. package/.next/server/chunks/ssr/{_1gd8q8l._.js → _0hxt2ul._.js} +2 -2
  210. package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
  211. package/.next/server/chunks/ssr/_0lu1n_x._.js +1 -1
  212. package/.next/server/chunks/ssr/{_15j2_8f._.js → _0wzaht-._.js} +2 -2
  213. package/.next/server/chunks/ssr/_0z-g0uy._.js +3 -0
  214. package/.next/server/chunks/ssr/_123ttsf._.js +3 -0
  215. package/.next/server/chunks/ssr/_123y-ah._.js +3 -0
  216. package/.next/server/chunks/ssr/_12w42gi._.js +3 -0
  217. package/.next/server/chunks/ssr/_161kqav._.js +3 -0
  218. package/.next/server/chunks/ssr/{_0ap2fzn._.js → _16c6_xm._.js} +2 -2
  219. package/.next/server/chunks/ssr/_18opah7._.js +3 -0
  220. package/.next/server/chunks/ssr/_18xqmny._.js +3 -0
  221. package/.next/server/chunks/ssr/_1_krfph._.js +3 -0
  222. package/.next/server/chunks/ssr/{_1-_etts._.js → _1anqots._.js} +2 -2
  223. package/.next/server/chunks/ssr/_1ey7qn-._.js +70 -0
  224. package/.next/server/chunks/ssr/_1g9vs7q._.js +3 -0
  225. package/.next/server/chunks/ssr/_1jmz33w._.js +3 -0
  226. package/.next/server/chunks/ssr/{_0n3toaw._.js → _1k0spz7._.js} +2 -2
  227. package/.next/server/chunks/ssr/_1rgtxhr._.js +5 -0
  228. package/.next/server/chunks/ssr/_1slp25y._.js +3 -0
  229. package/.next/server/chunks/ssr/{_1fojd41._.js → _1x9pskj._.js} +3 -3
  230. package/.next/server/chunks/ssr/_1y6b5mr._.js +101 -0
  231. package/.next/server/chunks/ssr/_1ztk7p6._.js +7 -0
  232. package/.next/server/chunks/ssr/components_ui_tabs_tsx_0ej40b4._.js +3 -0
  233. package/.next/server/chunks/ssr/features_bot_thread_query_ts_023eklt._.js +4 -0
  234. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  235. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  236. package/.next/server/chunks/ssr/{_1-teee5._.js → lib_logger_ts_0-olsze._.js} +2 -7
  237. package/.next/server/chunks/ssr/{node_modules_0cxaxk9._.js → node_modules_00_51_v._.js} +1 -1
  238. package/.next/server/chunks/ssr/{node_modules_04jf_m8._.js → node_modules_022sipm._.js} +1 -1
  239. package/.next/server/chunks/ssr/{node_modules_0g7fzk3._.js → node_modules_0332gw4._.js} +1 -1
  240. package/.next/server/chunks/ssr/{node_modules_1cx_ql6._.js → node_modules_04vxchl._.js} +1 -1
  241. package/.next/server/chunks/ssr/{node_modules_1vh5ntv._.js → node_modules_0746rzi._.js} +1 -1
  242. package/.next/server/chunks/ssr/{node_modules_0yugy6m._.js → node_modules_0d5uzof._.js} +1 -1
  243. package/.next/server/chunks/ssr/{node_modules_09q35v_._.js → node_modules_0exkwcm._.js} +1 -1
  244. package/.next/server/chunks/ssr/{node_modules_08dhkiq._.js → node_modules_0fr-ml8._.js} +1 -1
  245. package/.next/server/chunks/ssr/{node_modules_14q6g4n._.js → node_modules_0mxvg8h._.js} +1 -1
  246. package/.next/server/chunks/ssr/{node_modules_0arus10._.js → node_modules_0qapgjr._.js} +1 -1
  247. package/.next/server/chunks/ssr/{node_modules_1eg7m6c._.js → node_modules_0qbfyjh._.js} +1 -1
  248. package/.next/server/chunks/ssr/{node_modules_1cz2hle._.js → node_modules_0t-9nup._.js} +1 -1
  249. package/.next/server/chunks/ssr/{node_modules_17bwo43._.js → node_modules_0tusx4d._.js} +3 -3
  250. package/.next/server/chunks/ssr/{node_modules_1efrqxi._.js → node_modules_0vsxbzp._.js} +1 -1
  251. package/.next/server/chunks/ssr/{node_modules_0_-_a--._.js → node_modules_0vv0-fg._.js} +1 -1
  252. package/.next/server/chunks/ssr/{node_modules_1gdh31p._.js → node_modules_0wsvp00._.js} +1 -1
  253. package/.next/server/chunks/ssr/{node_modules_1hpopmo._.js → node_modules_1-urho9._.js} +1 -1
  254. package/.next/server/chunks/ssr/{node_modules_1_pc9kq._.js → node_modules_16pi_dd._.js} +1 -1
  255. package/.next/server/chunks/ssr/{node_modules_00etblx._.js → node_modules_1d53yiq._.js} +1 -1
  256. package/.next/server/chunks/ssr/{node_modules_1aaxuef._.js → node_modules_1g1eihe._.js} +1 -1
  257. package/.next/server/chunks/ssr/{node_modules_198nmll._.js → node_modules_1gpednh._.js} +1 -1
  258. package/.next/server/chunks/ssr/{node_modules_0pvd7q-._.js → node_modules_1gu1pph._.js} +1 -1
  259. package/.next/server/chunks/ssr/{node_modules_0b__sgl._.js → node_modules_1he1w8_._.js} +1 -1
  260. package/.next/server/chunks/ssr/{node_modules_1qpjvy5._.js → node_modules_1lnjapm._.js} +1 -1
  261. package/.next/server/chunks/ssr/{node_modules_0940ns5._.js → node_modules_1lynj9m._.js} +1 -1
  262. package/.next/server/chunks/ssr/{node_modules_0x3-ply._.js → node_modules_1seuurl._.js} +2 -2
  263. package/.next/server/chunks/ssr/{node_modules_14om4ar._.js → node_modules_1u6w1pb._.js} +1 -1
  264. package/.next/server/chunks/ssr/{node_modules_0had7xd._.js → node_modules_1xn6saw._.js} +1 -1
  265. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1b748ic._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_067_exa._.js} +1 -1
  266. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1z4hm5e._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0nrrdaq._.js} +1 -1
  267. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_06mk8id._.js → node_modules_@vercel_oidc_dist_21a5haa._.js} +1 -1
  268. package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_trash_mjs_0px2gzr._.js +3 -0
  269. package/.next/server/middleware-build-manifest.js +3 -3
  270. package/.next/server/pages/404.html +1 -1
  271. package/.next/server/pages/500.html +1 -1
  272. package/.next/server/server-reference-manifest.js +1 -1
  273. package/.next/server/server-reference-manifest.json +68 -46
  274. package/.next/static/chunks/0dr16v67vj4n7.js +2 -0
  275. package/.next/static/chunks/0g4b348l0pve7.js +1 -0
  276. package/.next/static/chunks/0joan-69kbspd.js +1 -0
  277. package/.next/static/chunks/0vlgzqo9ka90v.js +1 -0
  278. package/.next/static/chunks/0zrupxgci5gqc.js +1 -0
  279. package/.next/static/chunks/10k2xvap03z20.css +1 -0
  280. package/.next/static/chunks/18mkhstgiqcbt.js +1 -0
  281. package/.next/static/chunks/{15m575979un5f.js → 1i-u2wzub_to1.js} +2 -2
  282. package/.next/static/chunks/1kp5nfp06nj9h.js +5 -0
  283. package/.next/static/chunks/1ohslk4568tm3.js +1 -0
  284. package/.next/static/chunks/1sbmwswc9k2-1.js +1 -0
  285. package/.next/static/chunks/1t9qms5pwwa3n.js +1 -0
  286. package/.next/static/chunks/1vj_5bnbxx84e.js +1 -0
  287. package/.next/static/chunks/1vqezbhjm3goq.js +1 -0
  288. package/.next/static/chunks/{3akmqm77muj4-.js → 1wlo979mx-y3w.js} +1 -1
  289. package/.next/static/chunks/26ql4y-43xwhp.js +1 -0
  290. package/.next/static/chunks/{0fl3gwe4-l6tz.js → 2_pnccjp6j1ca.js} +1 -1
  291. package/.next/static/chunks/2a51-ywz9b2a3.js +1 -0
  292. package/.next/static/chunks/2fvdgxih7x7hr.js +1 -0
  293. package/.next/static/chunks/2jlz8zp4jax_m.js +18 -0
  294. package/.next/static/chunks/2nvepds1fu2ux.js +1 -0
  295. package/.next/static/chunks/3addj3i4-izma.js +1 -0
  296. package/.next/static/chunks/3bfwpww3m3x4u.js +1 -0
  297. package/.next/static/chunks/3gfh7nxwusbt0.js +1 -0
  298. package/.next/static/chunks/3gwa44cvz4qoi.js +1 -0
  299. package/.next/static/chunks/{38gweqdo4jpfj.js → 3izg0a3k87q1-.js} +5 -5
  300. package/.next/static/chunks/3zk6q3-3f9swq.js +99 -0
  301. package/.next/static/chunks/3zkg5iup3v4a2.js +12 -0
  302. package/.next/static/chunks/405m0bqtoihev.js +1 -0
  303. package/.next/static/chunks/42wcg3wfg_m_p.js +1 -0
  304. package/README.md +27 -216
  305. package/app/api/events/app-event.ts +6 -4
  306. package/app/api/query-key.ts +25 -16
  307. package/bin/database.mjs +37 -0
  308. package/bin/thursday.mjs +44 -28
  309. package/components/ui/custom-icon.tsx +0 -60
  310. package/components/ui/radio-group.tsx +37 -0
  311. package/components/ui/shiny-text.tsx +33 -6
  312. package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/migration.sql +80 -24
  313. package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/snapshot.json +689 -110
  314. package/database/migrations/20260916160713_thread_cancelled/migration.sql +2 -0
  315. package/database/migrations/{20260911030320_opposite_jack_power → 20260916160713_thread_cancelled}/snapshot.json +706 -84
  316. package/docs/how-it-works.md +39 -36
  317. package/docs/images/keeps-going.png +0 -0
  318. package/docs/images/two-minds.png +0 -0
  319. package/docs/live-calls.md +231 -0
  320. package/docs/thread-rooms.md +71 -0
  321. package/features/ai/chatgpt.ts +1 -2
  322. package/features/ai/live.schema.ts +128 -0
  323. package/features/ai/load-tools.ts +148 -77
  324. package/features/ai/model.schema.ts +2 -120
  325. package/features/ai/model.ts +52 -88
  326. package/features/ai/prompts/bot.prompt.ts +91 -214
  327. package/features/ai/prompts/live.prompt.ts +265 -0
  328. package/features/ai/prompts/prompt-helper.ts +91 -32
  329. package/features/ai/prompts/thursday.prompt.ts +125 -172
  330. package/features/ai/tools/bot.tool.ts +38 -145
  331. package/features/ai/tools/call.tool.ts +1 -1
  332. package/features/ai/tools/connected.ts +5 -3
  333. package/features/ai/tools/mcp.tool.ts +7 -4
  334. package/features/ai/tools/memory.tool.ts +35 -31
  335. package/features/ai/tools/skills.tool.ts +36 -14
  336. package/features/ai/tools/studio.tool.ts +26 -21
  337. package/features/ai/tools/tool-name.ts +2 -5
  338. package/features/ai/tools/workspace.tool.ts +33 -10
  339. package/features/artifact/artifact.action.ts +2 -2
  340. package/features/artifact/artifact.query.ts +112 -61
  341. package/features/artifact/artifact.schema.ts +8 -6
  342. package/features/artifact/components/artifact-setting.tsx +83 -33
  343. package/features/bot/bot.action.ts +43 -36
  344. package/features/bot/bot.memory.ts +137 -20
  345. package/features/bot/bot.query.ts +28 -11
  346. package/features/bot/bot.run.ts +299 -636
  347. package/features/bot/bot.runner.ts +468 -844
  348. package/features/bot/bot.schema.ts +93 -82
  349. package/features/bot/bot.seed.ts +141 -64
  350. package/features/bot/components/bot-mark.tsx +324 -232
  351. package/features/bot/components/bot-room.tsx +1613 -697
  352. package/features/bot/components/bot-roster.tsx +8 -13
  353. package/features/bot/components/bot-setting.tsx +58 -42
  354. package/features/bot/components/bot-tool.tsx +55 -87
  355. package/features/bot/components/mark-palette.tsx +35 -32
  356. package/features/bot/components/path-chips.tsx +33 -3
  357. package/features/bot/components/thread-badge.tsx +34 -0
  358. package/features/bot/components/thread-reply.tsx +639 -0
  359. package/features/bot/components/thread-setting.tsx +674 -0
  360. package/features/bot/mark.const.ts +79 -31
  361. package/features/bot/room.query.ts +889 -0
  362. package/features/bot/room.schema.ts +64 -0
  363. package/features/bot/seed-bots.ts +2 -1
  364. package/features/bot/thread.query.ts +971 -0
  365. package/features/bot/thread.store.ts +587 -0
  366. package/features/config/components/voice-key.tsx +113 -88
  367. package/features/config/config.const.ts +5 -9
  368. package/features/connectors/mcp.schema.ts +0 -2
  369. package/features/intro/components/intro.tsx +169 -139
  370. package/features/memory/components/memory-setting.tsx +2 -7
  371. package/features/memory/memory.schema.ts +14 -23
  372. package/features/settings/components/setting-ui.tsx +0 -44
  373. package/features/settings/components/settings.tsx +12 -12
  374. package/features/settings/settings.alert.ts +3 -3
  375. package/features/settings/settings.store.ts +1 -1
  376. package/features/skills/skills.schema.ts +0 -2
  377. package/features/thursday/components/ascii-field.tsx +24 -44
  378. package/features/thursday/components/ascii-orb.tsx +359 -358
  379. package/features/thursday/components/call-log.tsx +46 -44
  380. package/features/thursday/components/face.tsx +35 -10
  381. package/features/thursday/components/tab-state.tsx +3 -3
  382. package/features/thursday/components/thursday-mark.tsx +1 -0
  383. package/features/thursday/components/thursday-setting.tsx +631 -332
  384. package/features/thursday/components/thursday.tsx +106 -41
  385. package/features/thursday/components/voice-picker.tsx +255 -0
  386. package/features/thursday/thursday.action.ts +94 -68
  387. package/features/thursday/thursday.query.ts +49 -30
  388. package/features/thursday/thursday.schema.ts +27 -30
  389. package/features/thursday/thursday.store.ts +11 -7
  390. package/features/thursday/tool-call.ts +3 -3
  391. package/features/thursday/tool-line.ts +5 -3
  392. package/features/thursday/use-thursday.ts +725 -450
  393. package/features/workspace/workspace.query.ts +17 -0
  394. package/features/workspace/workspace.ts +103 -50
  395. package/lib/live/live.schema.ts +47 -0
  396. package/lib/live/live.server.ts +155 -0
  397. package/lib/live/live.session.ts +574 -0
  398. package/lib/{realtime/realtime.tap.ts → live/live.tap.ts} +41 -12
  399. package/lib/live/live.transport.ts +152 -0
  400. package/lib/protocol/use-server-pages.ts +9 -5
  401. package/lib/utils.ts +21 -20
  402. package/package.json +2 -2
  403. package/public/voices/alloy.ogg +0 -0
  404. package/public/voices/ash.ogg +0 -0
  405. package/public/voices/ballad.ogg +0 -0
  406. package/public/voices/beacon.ogg +0 -0
  407. package/public/voices/bossa.ogg +0 -0
  408. package/public/voices/cedar.ogg +0 -0
  409. package/public/voices/cinder.ogg +0 -0
  410. package/public/voices/coral.ogg +0 -0
  411. package/public/voices/delta.ogg +0 -0
  412. package/public/voices/echo.ogg +0 -0
  413. package/public/voices/gleam.ogg +0 -0
  414. package/public/voices/marin.ogg +0 -0
  415. package/public/voices/meridian.ogg +0 -0
  416. package/public/voices/quartz.ogg +0 -0
  417. package/public/voices/ripple.ogg +0 -0
  418. package/public/voices/sage.ogg +0 -0
  419. package/public/voices/shimmer.ogg +0 -0
  420. package/public/voices/stone.ogg +0 -0
  421. package/public/voices/tempo.ogg +0 -0
  422. package/public/voices/verse.ogg +0 -0
  423. package/public/voices/vesper.ogg +0 -0
  424. package/public/voices/willow.ogg +0 -0
  425. package/scripts/artifact-paths.test.mts +25 -0
  426. package/scripts/bot-context.test.mts +1408 -0
  427. package/scripts/live-history.test.mts +115 -0
  428. package/scripts/live-session.test.mts +948 -0
  429. package/server.js +1 -1
  430. package/skills/browser/SKILL.md +17 -6
  431. package/skills/computer/SKILL.md +1 -1
  432. package/skills/find-skills/SKILL.md +3 -8
  433. package/skills/interactive-page/SKILL.md +12 -7
  434. package/skills/interactive-page/references/diagram.md +89 -0
  435. package/skills/interactive-page/scripts/archify/LICENSE +22 -0
  436. package/skills/interactive-page/scripts/archify/README.md +25 -0
  437. package/skills/interactive-page/scripts/archify/assets/JetBrainsMono-OFL.txt +93 -0
  438. package/skills/interactive-page/scripts/archify/assets/template.html +14934 -0
  439. package/skills/interactive-page/scripts/archify/bin/archify.mjs +2126 -0
  440. package/skills/interactive-page/scripts/archify/bin/open-artifact.mjs +86 -0
  441. package/skills/interactive-page/scripts/archify/bin/preview.mjs +653 -0
  442. package/skills/interactive-page/scripts/archify/bin/visual-check.mjs +829 -0
  443. package/skills/interactive-page/scripts/archify/delta/architecture-delta.mjs +1223 -0
  444. package/skills/interactive-page/scripts/archify/examples/agent-run.lifecycle.json +60 -0
  445. package/skills/interactive-page/scripts/archify/examples/agent-tool-call.workflow.json +94 -0
  446. package/skills/interactive-page/scripts/archify/examples/async-job-roundtrip.sequence.json +61 -0
  447. package/skills/interactive-page/scripts/archify/examples/cache-miss-request.sequence.json +82 -0
  448. package/skills/interactive-page/scripts/archify/examples/checkout-platform.base.architecture.json +31 -0
  449. package/skills/interactive-page/scripts/archify/examples/checkout-platform.head.architecture.json +31 -0
  450. package/skills/interactive-page/scripts/archify/examples/deployment-release.lifecycle.json +49 -0
  451. package/skills/interactive-page/scripts/archify/examples/event-stream.dataflow.json +57 -0
  452. package/skills/interactive-page/scripts/archify/examples/incident-response.workflow.json +64 -0
  453. package/skills/interactive-page/scripts/archify/examples/product-analytics.dataflow.json +76 -0
  454. package/skills/interactive-page/scripts/archify/examples/production-deployment.architecture.json +71 -0
  455. package/skills/interactive-page/scripts/archify/examples/release-delivery.workflow.json +62 -0
  456. package/skills/interactive-page/scripts/archify/examples/web-app.architecture.json +46 -0
  457. package/skills/interactive-page/scripts/archify/migrations/workflow-v2.mjs +279 -0
  458. package/skills/interactive-page/scripts/archify/recipes/scenarios.mjs +284 -0
  459. package/skills/interactive-page/scripts/archify/references/authoring-contract.md +243 -0
  460. package/skills/interactive-page/scripts/archify/references/delivery-contract.md +120 -0
  461. package/skills/interactive-page/scripts/archify/references/viewer-runtime.md +45 -0
  462. package/skills/interactive-page/scripts/archify/renderers/architecture/grid.mjs +62 -0
  463. package/skills/interactive-page/scripts/archify/renderers/architecture/render-architecture.mjs +1078 -0
  464. package/skills/interactive-page/scripts/archify/renderers/dataflow/README.md +104 -0
  465. package/skills/interactive-page/scripts/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  466. package/skills/interactive-page/scripts/archify/renderers/lifecycle/README.md +115 -0
  467. package/skills/interactive-page/scripts/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  468. package/skills/interactive-page/scripts/archify/renderers/sequence/README.md +114 -0
  469. package/skills/interactive-page/scripts/archify/renderers/sequence/render-sequence.mjs +464 -0
  470. package/skills/interactive-page/scripts/archify/renderers/shared/brand-marks.mjs +563 -0
  471. package/skills/interactive-page/scripts/archify/renderers/shared/cli.mjs +218 -0
  472. package/skills/interactive-page/scripts/archify/renderers/shared/desktop-readability.mjs +26 -0
  473. package/skills/interactive-page/scripts/archify/renderers/shared/diagnostics.mjs +127 -0
  474. package/skills/interactive-page/scripts/archify/renderers/shared/engineering-profiles.mjs +157 -0
  475. package/skills/interactive-page/scripts/archify/renderers/shared/generated-brand-marks.mjs +3 -0
  476. package/skills/interactive-page/scripts/archify/renderers/shared/generated-validators.mjs +13 -0
  477. package/skills/interactive-page/scripts/archify/renderers/shared/geometry.mjs +1423 -0
  478. package/skills/interactive-page/scripts/archify/renderers/shared/i18n.mjs +595 -0
  479. package/skills/interactive-page/scripts/archify/renderers/shared/layout-report.mjs +40 -0
  480. package/skills/interactive-page/scripts/archify/renderers/shared/legend.mjs +217 -0
  481. package/skills/interactive-page/scripts/archify/renderers/shared/output-path.mjs +340 -0
  482. package/skills/interactive-page/scripts/archify/renderers/shared/repository-evidence.mjs +238 -0
  483. package/skills/interactive-page/scripts/archify/renderers/shared/repository-location.mjs +58 -0
  484. package/skills/interactive-page/scripts/archify/renderers/shared/text-fit.mjs +49 -0
  485. package/skills/interactive-page/scripts/archify/renderers/shared/utils.mjs +232 -0
  486. package/skills/interactive-page/scripts/archify/renderers/shared/validator.mjs +86 -0
  487. package/skills/interactive-page/scripts/archify/renderers/workflow/README.md +223 -0
  488. package/skills/interactive-page/scripts/archify/renderers/workflow/render-workflow.mjs +35 -0
  489. package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  490. package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  491. package/skills/interactive-page/scripts/archify/schemas/README.md +211 -0
  492. package/skills/interactive-page/scripts/archify/schemas/architecture.schema.json +178 -0
  493. package/skills/interactive-page/scripts/archify/schemas/common.schema.json +115 -0
  494. package/skills/interactive-page/scripts/archify/schemas/dataflow.schema.json +243 -0
  495. package/skills/interactive-page/scripts/archify/schemas/lifecycle.schema.json +266 -0
  496. package/skills/interactive-page/scripts/archify/schemas/sequence.schema.json +223 -0
  497. package/skills/interactive-page/scripts/archify/schemas/workflow.schema.json +428 -0
  498. package/skills/interactive-page/scripts/archify/scripts/check-render-output.mjs +916 -0
  499. package/skills/interactive-page/scripts/archify/scripts/render-examples.mjs +26 -0
  500. package/skills/interactive-page/scripts/bundle-artifact.sh +9 -8
  501. package/skills/skill-creator/SKILL.md +4 -5
  502. package/.next/server/app/api/bot/task/[id]/route/app-paths-manifest.json +0 -3
  503. package/.next/server/app/api/bot/task/[id]/route.js +0 -16
  504. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +0 -1
  505. package/.next/server/app/api/bot/task/[id]/route_client-reference-manifest.js +0 -3
  506. package/.next/server/app/api/bot/task/route/app-paths-manifest.json +0 -3
  507. package/.next/server/app/api/bot/task/route.js +0 -16
  508. package/.next/server/app/api/bot/task/route.js.nft.json +0 -1
  509. package/.next/server/app/api/bot/task/route_client-reference-manifest.js +0 -3
  510. package/.next/server/chunks/[root-of-the-server]__01dkp4a._.js +0 -3
  511. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +0 -3
  512. package/.next/server/chunks/[root-of-the-server]__02xjoto._.js +0 -3
  513. package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
  514. package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
  515. package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +0 -3
  516. package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
  517. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
  518. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +0 -3
  519. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +0 -5
  520. package/.next/server/chunks/[root-of-the-server]__11n256_._.js +0 -5
  521. package/.next/server/chunks/[root-of-the-server]__12cabg9._.js +0 -3
  522. package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
  523. package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
  524. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +0 -5
  525. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +0 -3
  526. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
  527. package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +0 -3
  528. package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +0 -3
  529. package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +0 -3
  530. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
  531. package/.next/server/chunks/_0-73of8._.js +0 -55
  532. package/.next/server/chunks/_0-fjql4._.js +0 -26
  533. package/.next/server/chunks/_00xh_q0._.js +0 -25
  534. package/.next/server/chunks/_03a0897._.js +0 -3
  535. package/.next/server/chunks/_051qh8u._.js +0 -3
  536. package/.next/server/chunks/_05sovvj._.js +0 -3
  537. package/.next/server/chunks/_0fzqt2y._.js +0 -134
  538. package/.next/server/chunks/_0mecv0f._.js +0 -26
  539. package/.next/server/chunks/_0mh8j4m._.js +0 -3
  540. package/.next/server/chunks/_0obojvv._.js +0 -3
  541. package/.next/server/chunks/_0sb7tlz._.js +0 -3
  542. package/.next/server/chunks/_0sbynis._.js +0 -3
  543. package/.next/server/chunks/_0u10wmw._.js +0 -55
  544. package/.next/server/chunks/_0u4aiy2._.js +0 -3
  545. package/.next/server/chunks/_0wp4le3._.js +0 -3
  546. package/.next/server/chunks/_0y5fg-x._.js +0 -3
  547. package/.next/server/chunks/_1230wbx._.js +0 -10
  548. package/.next/server/chunks/_12nheja._.js +0 -3
  549. package/.next/server/chunks/_13510xh._.js +0 -3
  550. package/.next/server/chunks/_15hq89o._.js +0 -3
  551. package/.next/server/chunks/_18-1wcc._.js +0 -55
  552. package/.next/server/chunks/_1_9mjx-._.js +0 -3
  553. package/.next/server/chunks/_1c78vw5._.js +0 -229
  554. package/.next/server/chunks/_1h5_8sk._.js +0 -3
  555. package/.next/server/chunks/_1hdnxb7._.js +0 -3
  556. package/.next/server/chunks/_1j8k_j5._.js +0 -3
  557. package/.next/server/chunks/_1nula3v._.js +0 -3
  558. package/.next/server/chunks/_1php4eq._.js +0 -3
  559. package/.next/server/chunks/_1yuv4vp._.js +0 -3
  560. package/.next/server/chunks/_1zjrdag._.js +0 -3
  561. package/.next/server/chunks/_next-internal_server_app_api_bot_task_[id]_route_actions_1_k_8u3.js +0 -3
  562. package/.next/server/chunks/_next-internal_server_app_api_bot_task_route_actions_0xln6ra.js +0 -3
  563. package/.next/server/chunks/features_0rpfxbk._.js +0 -3
  564. package/.next/server/chunks/features_1qf3cei._.js +0 -3
  565. package/.next/server/chunks/features_bot_0zt92oh._.js +0 -3
  566. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js +0 -3
  567. package/.next/server/chunks/ssr/[externals]__0dizgek._.js +0 -3
  568. package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +0 -3
  569. package/.next/server/chunks/ssr/[root-of-the-server]__0mz33a7._.js +0 -3
  570. package/.next/server/chunks/ssr/[root-of-the-server]__0s9dq5u._.js +0 -3
  571. package/.next/server/chunks/ssr/[root-of-the-server]__0s9l2sv._.js +0 -3
  572. package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +0 -3
  573. package/.next/server/chunks/ssr/[root-of-the-server]__0xsog8n._.js +0 -3
  574. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +0 -61
  575. package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +0 -43
  576. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
  577. package/.next/server/chunks/ssr/[root-of-the-server]__202p40z._.js +0 -3
  578. package/.next/server/chunks/ssr/_001_ms1._.js +0 -28
  579. package/.next/server/chunks/ssr/_01n3o0m._.js +0 -3
  580. package/.next/server/chunks/ssr/_0fz4mf_._.js +0 -3
  581. package/.next/server/chunks/ssr/_0gb4gmf._.js +0 -3
  582. package/.next/server/chunks/ssr/_0h42fze._.js +0 -3
  583. package/.next/server/chunks/ssr/_0h7zdni._.js +0 -3
  584. package/.next/server/chunks/ssr/_0ilna52._.js +0 -42
  585. package/.next/server/chunks/ssr/_0q0zan6._.js +0 -70
  586. package/.next/server/chunks/ssr/_0zcjrbg._.js +0 -3
  587. package/.next/server/chunks/ssr/_0zhoc3r._.js +0 -3
  588. package/.next/server/chunks/ssr/_13fke0x._.js +0 -3
  589. package/.next/server/chunks/ssr/_18cymkq._.js +0 -3
  590. package/.next/server/chunks/ssr/_1b2mjwr._.js +0 -3
  591. package/.next/server/chunks/ssr/_1f-dg-6._.js +0 -3
  592. package/.next/server/chunks/ssr/_1h0rw11._.js +0 -3
  593. package/.next/server/chunks/ssr/_1jhd09t._.js +0 -3
  594. package/.next/server/chunks/ssr/_1nnsk7z._.js +0 -3
  595. package/.next/server/chunks/ssr/_1ov37if._.js +0 -3
  596. package/.next/server/chunks/ssr/_1sji22n._.js +0 -3
  597. package/.next/server/chunks/ssr/_1vi_uo7._.js +0 -3
  598. package/.next/server/chunks/ssr/_1vyd1ji._.js +0 -3
  599. package/.next/server/chunks/ssr/_1xx4zlb._.js +0 -3
  600. package/.next/server/chunks/ssr/components_ui_1y1l7yj._.js +0 -3
  601. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
  602. package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +0 -27
  603. package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +0 -27
  604. package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0npjf8-._.js +0 -3
  605. package/.next/static/chunks/00rd4-v8r7axp.js +0 -1
  606. package/.next/static/chunks/08qe4as2q428z.js +0 -3
  607. package/.next/static/chunks/0gnqdjn2lvdad.js +0 -2
  608. package/.next/static/chunks/0hwap1-p0m5sa.js +0 -1
  609. package/.next/static/chunks/0r5qcctujhxli.js +0 -1
  610. package/.next/static/chunks/0w52rrrmuj9r9.js +0 -1
  611. package/.next/static/chunks/0wcprt5ot5she.js +0 -1
  612. package/.next/static/chunks/10teww7wt-f6h.js +0 -1
  613. package/.next/static/chunks/17p6tk6k88dq6.js +0 -1
  614. package/.next/static/chunks/18a96rkhi4_yd.js +0 -1
  615. package/.next/static/chunks/1dkjg87pxk0n9.js +0 -28
  616. package/.next/static/chunks/1jd09sxcjddu1.js +0 -1
  617. package/.next/static/chunks/1w73ece_6bheu.js +0 -18
  618. package/.next/static/chunks/214ya3n5gxeih.js +0 -1
  619. package/.next/static/chunks/22_zkas21rpna.js +0 -1
  620. package/.next/static/chunks/2fh4-3z_q8b_w.js +0 -1
  621. package/.next/static/chunks/2h0sd2vumhb15.js +0 -1
  622. package/.next/static/chunks/2t4vhi3m8ahya.js +0 -1
  623. package/.next/static/chunks/2tigdtz3dpcy8.js +0 -1
  624. package/.next/static/chunks/2u7xt6mxg44lf.js +0 -1
  625. package/.next/static/chunks/3786h5vayt80-.js +0 -28
  626. package/.next/static/chunks/39j0fvpybzabl.js +0 -40
  627. package/.next/static/chunks/3cq5g494yuke8.css +0 -1
  628. package/.next/static/chunks/3hibbjnt85-k6.js +0 -1
  629. package/.next/static/chunks/3woj-xd7ki5_i.js +0 -1
  630. package/.next/static/chunks/3xk7yqgomx7yq.js +0 -1
  631. package/.next/static/chunks/3zmcsnuo25zvq.js +0 -1
  632. package/database/migrations/20260911030320_opposite_jack_power/migration.sql +0 -1
  633. package/docs/images/task-thread.png +0 -0
  634. package/features/bot/components/task-badge.tsx +0 -42
  635. package/features/bot/components/task-reply.tsx +0 -328
  636. package/features/bot/components/task-setting.tsx +0 -524
  637. package/features/bot/task.query.ts +0 -976
  638. package/features/bot/task.store.ts +0 -489
  639. package/lib/realtime/client-secret.ts +0 -86
  640. package/lib/realtime/open-session.ts +0 -31
  641. package/lib/realtime/openai.ts +0 -35
  642. package/lib/realtime/realtime.audio.ts +0 -196
  643. package/lib/realtime/realtime.driver.ts +0 -489
  644. package/lib/realtime/realtime.protocol.ts +0 -393
  645. package/lib/realtime/realtime.schema.ts +0 -37
  646. package/lib/realtime/realtime.session.ts +0 -106
  647. package/lib/realtime/realtime.transport.ts +0 -384
  648. package/lib/realtime/xai.ts +0 -39
  649. /package/.next/server/app/api/bot/{task → thread}/[id]/route/build-manifest.json +0 -0
  650. /package/.next/server/app/api/bot/{task → thread}/[id]/route/server-reference-manifest.json +0 -0
  651. /package/.next/server/app/api/bot/{task → thread}/[id]/route.js.map +0 -0
  652. /package/.next/server/app/api/bot/{task → thread}/route/build-manifest.json +0 -0
  653. /package/.next/server/app/api/bot/{task → thread}/route/server-reference-manifest.json +0 -0
  654. /package/.next/server/app/api/bot/{task → thread}/route.js.map +0 -0
  655. /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_buildManifest.js +0 -0
  656. /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_clientMiddlewareManifest.js +0 -0
  657. /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_ssgManifest.js +0 -0
@@ -4,57 +4,51 @@ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { useAppEvent } from "@/app/api/events/app-event.client";
5
5
  import { queryKey } from "@/app/api/query-key";
6
6
  import { toast } from "@/components/ui/toast";
7
+ import { CALL_END, CALL_IDLE, CALL_RELAY } from "@/config";
7
8
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
8
- import {
9
- type Bot,
10
- isAppStop,
11
- TASK_CONTINUE,
12
- type Task,
13
- } from "@/features/bot/bot.schema";
14
- import { MARK_BANDS } from "@/features/bot/mark.const";
15
- import { botTasks, screenActs } from "@/features/bot/task.store";
9
+ import { acceptThreadRelaysAction } from "@/features/bot/bot.action";
10
+ import { type Bot, isAppStop, type Thread } from "@/features/bot/bot.schema";
11
+ import { botThreads, screenActs } from "@/features/bot/thread.store";
16
12
  import { runRemoteTool } from "@/features/thursday/tool-call";
17
13
  import { isCombo, useHotkey } from "@/hooks/use-hotkey";
18
14
  import { useWakeWord } from "@/hooks/use-wake-word";
19
15
  import { toDate } from "@/lib/date-like";
20
- import { unwrapResult } from "@/lib/protocol/result";
16
+ import type { LiveClose } from "@/lib/live/live.schema";
17
+ import {
18
+ type LiveActivity,
19
+ type LiveSession,
20
+ type LiveToolCall,
21
+ openLiveSession,
22
+ } from "@/lib/live/live.session";
23
+ import {
24
+ type AudioTap,
25
+ createAudioTap,
26
+ SPECTRUM_BANDS,
27
+ } from "@/lib/live/live.tap";
28
+ import { type Result, unwrapResult } from "@/lib/protocol/result";
21
29
  import {
22
30
  revalidate,
23
31
  revalidateAll,
24
32
  useServerRoute,
25
33
  } from "@/lib/protocol/use-server-route";
26
34
  import { createOutbox, type Outbox } from "@/lib/queue";
27
- import { openRealtimeSession } from "@/lib/realtime/open-session";
28
- import type {
29
- RealtimeActivity,
30
- RealtimeSession,
31
- RealtimeToolCall,
32
- } from "@/lib/realtime/realtime.session";
33
- import { type AudioTap, createAudioTap } from "@/lib/realtime/realtime.tap";
34
35
  import { errorToString } from "@/lib/utils";
35
36
  import {
36
37
  endCallAction,
37
38
  openCallAction,
39
+ saveThoughtAction,
38
40
  saveTurnsAction,
39
41
  } from "./thursday.action";
40
- import type {
41
- CallMessage,
42
- CallStatus,
43
- CallTurn,
44
- LiveStatus,
45
- } from "./thursday.schema";
42
+ import type { CallMessage, CallStatus, LiveStatus } from "./thursday.schema";
46
43
  import { thursdaySettings, useThursdayStore } from "./thursday.store";
47
44
  import { toolBot, toolLine } from "./tool-line";
48
45
 
49
46
  /**
50
- * One live call, plus the task inbox the app watches even with no call open.
51
- * The server picks the provider (openCallAction) and runs the tools (tool-call);
52
- * the page itself only hangs up.
47
+ * One live call, plus the thread inbox the app watches even with no call open.
48
+ * The server opens the Live session (openCallAction) and runs the tools
49
+ * (tool-call); the page itself only hangs up.
53
50
  */
54
51
 
55
- /** Time for a tool result to reach the model before the socket closes. */
56
- const HANG_UP_DELAY_MS = 400;
57
-
58
52
  /** Plays when the line opens. */
59
53
  const CONNECTED_SOUND = "/sounds/start_voice.ogg";
60
54
 
@@ -69,6 +63,13 @@ const TOOL_LINGER_MS = 2500;
69
63
  /** A relay from a bot carries more to read than a tool line. */
70
64
  const RELAY_LINGER_MS = 5000;
71
65
 
66
+ /**
67
+ * Once the backend's turn is over, how long the activity line keeps saying it is
68
+ * working while it waits for her voice. Her first word is what normally ends it;
69
+ * this is only for a turn that never reaches one.
70
+ */
71
+ const THINKING_TAIL_MS = 6000;
72
+
72
73
  /**
73
74
  * The face lags the activity line: only a tool held longer than this switches
74
75
  * to the working face. Shorter tools show on the line only.
@@ -81,92 +82,88 @@ const FACE_TOOL_MS = 800;
81
82
  */
82
83
  const FACE_SETTLE_MS = 600;
83
84
 
84
- /** Scrollback for the side-by-side layout; the call view shows at most three. */
85
- const KEEP_MESSAGES = 24;
86
-
87
- /** Safety net only; the event stream revalidates tasks as they change. */
88
- const TASK_POLL_FALLBACK_MS = 30_000;
89
-
90
- /** Notify once about failing saves, then stay quiet for the rest of the call. */
91
- const SAVE_FAILURE_LIMIT = 3;
92
-
93
85
  /**
94
- * With no user speech, agent speech, or tool for this long, the model is asked
95
- * to say goodbye and call end_call.
86
+ * How long the face says a call failed to open or dropped before it rests: long
87
+ * enough for the orb to spell its ERROR out. The toast carries the reason.
96
88
  */
97
- const IDLE_HANG_UP_MS = 60_000;
98
-
99
- /** If end_call does not follow the goodbye, the page hangs up. */
100
- const IDLE_GRACE_MS = 15_000;
89
+ const FAILED_FACE_MS = 6000;
101
90
 
102
- /** The countdown shows on screen inside this window. */
103
- const IDLE_WARN_MS = 15_000;
91
+ /** Scrollback for the side-by-side layout; the call view shows at most three. */
92
+ const KEEP_MESSAGES = 24;
104
93
 
105
- /**
106
- * The user finished a turn and nothing came back — no words, no tool — for this
107
- * long. The line is up and the model is not on it, so there is no goodbye to
108
- * ask for: the page hangs up. Only armed while an answer is owed, so a quiet
109
- * line still ends the slow way (IDLE_HANG_UP_MS).
110
- */
111
- const AGENT_SILENT_MS = 30_000;
94
+ /** Safety net only; the event stream revalidates threads as they change. */
95
+ const THREAD_POLL_FALLBACK_MS = 30_000;
112
96
 
113
- const IDLE_LINE =
114
- "The user has said nothing for a minute. Say a one-line goodbye, then call end_call.";
97
+ /** Failed saves of one kind before the call stops saving that kind and says so, once. */
98
+ const SAVE_FAILURE_LIMIT = 3;
115
99
 
116
100
  /**
117
- * Upper bound on the mic staying muted for a relayed line the model never
118
- * voices (the response can be rejected).
101
+ * Put in as trusted behaviour just ahead of the first relay of a call. The relay
102
+ * itself carries facts only, since the backend reads relays too and routes answers
103
+ * by them.
119
104
  */
120
- const RELAY_MIC_MS = 15_000;
105
+ const RELAY_NOTE =
106
+ "An update from background work follows. If you have already told the user what it says, it need not be said again.";
121
107
 
122
- /** A line queued for the model. */
123
- type Said = {
124
- line: string;
125
- /** Shown on the activity line when the line is sent. */
126
- show?: ToolRun;
127
- };
108
+ const IDLE_LINE = `The user has said nothing for ${Math.round(CALL_IDLE.hangUpMs / 1000)} seconds. Say a one-line goodbye and end the call.`;
128
109
 
129
110
  /**
130
- * A tool the model is using, as the activity line draws it. `line` is the
131
- * human-readable sentence (tool-line), null when none exists; `name` is the
111
+ * What the activity line draws: a tool the model is using, or a relay. `line` is
112
+ * the human-readable sentence (tool-line), null when none exists; `name` is the
132
113
  * tool that actually ran.
133
114
  */
134
- export type ToolRun = {
115
+ export type ActivityLine = {
135
116
  name: string;
136
117
  line: string | null;
137
118
  done: boolean;
138
- /** A relay from a bot (answer or question) rather than a tool; `name` is the task label. */
139
- kind?: "tool" | "relay";
119
+ /** The tool call this is, so one call finishing never ends another's line. */
120
+ id?: string;
121
+ /** A relay from a bot (answer or question) rather than a tool; `name` is the thread label. */
122
+ kind?: "relay";
140
123
  /** The bot this names, when it names one: the row draws its face instead of a glyph. */
141
124
  bot?: string | null;
142
125
  };
143
126
 
144
127
  export function useThursday() {
145
- const session = useRef<RealtimeSession | null>(null);
128
+ const session = useRef<LiveSession | null>(null);
146
129
  // created on the first call: `new Audio()` cannot run during SSR
147
130
  const tap = useRef<AudioTap | null>(null);
148
131
  /** Created inside the call-starting click; hang-up may come from a tool or a disconnect with no gesture. */
149
132
  const farewell = useRef<HTMLAudioElement | null>(null);
150
133
 
151
134
  const [status, setStatus] = useState<CallStatus>("idle");
135
+ /** A call just failed to open or dropped; the face says so for FAILED_FACE_MS. */
136
+ const [failed, setFailed] = useState(false);
137
+ const failedFor = useRef<ReturnType<typeof setTimeout> | null>(null);
138
+ const showFailed = useCallback((on: boolean) => {
139
+ if (failedFor.current) clearTimeout(failedFor.current);
140
+ failedFor.current = on
141
+ ? setTimeout(() => setFailed(false), FAILED_FACE_MS)
142
+ : null;
143
+ setFailed(on);
144
+ }, []);
152
145
  const [messages, setMessages] = useState<CallMessage[]>([]);
153
- const [tool, setTool] = useState<ToolRun | null>(null);
146
+ const [tool, setTool] = useState<ActivityLine | null>(null);
154
147
  /** When the line opened (ms). */
155
148
  const [since, setSince] = useState<number | null>(null);
156
- /** Seconds until idle hang-up; set only inside IDLE_WARN_MS. */
149
+ /** Seconds until idle hang-up; set only inside CALL_IDLE.warnMs. */
157
150
  const [idleLeft, setIdleLeft] = useState<number | null>(null);
151
+ /** When the backend picked this turn up (ms); the activity line counts from it. */
152
+ const [thinkingSince, setThinkingSince] = useState<number | null>(null);
153
+ /** The same value where callbacks can read it, and the timer that ends it. */
154
+ const thinking = useRef<number | null>(null);
155
+ const thinkTail = useRef<ReturnType<typeof setTimeout> | null>(null);
158
156
  /**
159
- * Mic is muted while a tool runs and while a relayed line is being read.
160
- * The effect below applies it to the session.
157
+ * A relayed update is out and has not been voiced yet: nothing else goes in
158
+ * until her voice has started and stopped.
161
159
  */
162
- const [micOff, setMicOff] = useState(false);
163
- /** The two mute reasons, and whether the relayed line has produced audio yet. */
164
- const mic = useRef<{
165
- tool: boolean;
166
- reading: boolean;
160
+ const reading = useRef<{
161
+ on: boolean;
167
162
  spoke: boolean;
168
163
  giveUp: ReturnType<typeof setTimeout> | null;
169
- }>({ tool: false, reading: false, spoke: false, giveUp: null });
164
+ }>({ on: false, spoke: false, giveUp: null });
165
+ /** What `session.closed` confirmed for the call being ended; recorded on its row. */
166
+ const finalized = useRef<LiveClose | null>(null);
170
167
  /**
171
168
  * Last activity time, whether the goodbye was requested, and when an answer
172
169
  * started being owed (null once she answers or works).
@@ -178,30 +175,53 @@ export function useThursday() {
178
175
  grace: ReturnType<typeof setTimeout> | null;
179
176
  }>({ since: 0, asked: false, owed: null, grace: null });
180
177
  const linger = useRef<ReturnType<typeof setTimeout> | null>(null);
178
+ /** Whether the line holds a relay, so only a relay is finished by her voice. */
179
+ const relayOpen = useRef(false);
181
180
  // Every server-run tool in this call starts with this signal, so one abort
182
- // reaches running browser commands. Delegated tasks are server-owned and outlive the call
181
+ // reaches running browser commands. Delegated threads are server-owned and outlive the call
183
182
  const working = useRef<AbortController | null>(null);
184
183
  /** Row the turns are saved to; a ref so long-lived callbacks see it. */
185
184
  const callId = useRef<string | null>(null);
186
185
  /**
187
- * Relays sent this call, keyed by relayKey (id + updatedAt): an answered task
188
- * can stop to ask again, so the id alone would silence its second question.
186
+ * Open work put to her while this page has been open, by item key (openWork):
187
+ * each goes in once a call. A key carries its job's last change, so a job that
188
+ * asks or ends again is new work.
189
189
  */
190
- const relayed = useRef(new Set<string>());
191
- /** When this call was placed. An ending from before it is already in her prompt. */
192
- const placedAt = useRef(0);
190
+ const told = useRef(new Set<string>());
193
191
  /**
194
- * Lines for the model, including ones raised before the line opens. Held
195
- * until the session exists, which is also what lets a call-back queue its
196
- * line first.
192
+ * Keys put in this call that her voice has not carried yet: rejected, cut short
193
+ * by a hang-up, or never spoken. Hanging up takes them out of `told`, so the
194
+ * next call puts them in again; what she voiced stays out.
197
195
  */
198
- const outboxRef = useRef<Outbox<Said> | null>(null);
199
- const outbox = (outboxRef.current ??= createOutbox<Said>());
196
+ const unvoiced = useRef(new Set<string>());
197
+ /** The keys of the update on the line now, carried once her voice starts and stops on it. */
198
+ const onLine = useRef<string[]>([]);
200
199
  /**
201
- * Sends what piled up while she was reading the last line, as one line. Set
202
- * for the length of a session; null between calls.
200
+ * When her voice was last heard: for letting a goodbye finish (CALL_END), and for
201
+ * telling words she already answered from words still owed an answer.
203
202
  */
204
- const resumeRelay = useRef<(() => void) | null>(null);
203
+ const voiced = useRef(0);
204
+ /** The relay note went in on this call (RELAY_NOTE). */
205
+ const relayNoted = useRef(false);
206
+ /** The hang-up waiting on her goodbye once the backend called end_call. */
207
+ const leaving = useRef<ReturnType<typeof setInterval> | null>(null);
208
+ /** When either side's words were last transcribed: the quiet clock for relays. */
209
+ const heard = useRef(0);
210
+ /**
211
+ * The call-back placed this call and its first relay has not gone in yet: that one
212
+ * goes in once she has voiced the opening rather than waiting for a quiet line.
213
+ */
214
+ const rang = useRef(false);
215
+ /** Backend work or a call tool is running, as the last activity said. */
216
+ const acting = useRef(false);
217
+ /** The inbox as last fetched, read by the relay clock. */
218
+ const latest = useRef<Thread[] | undefined>(undefined);
219
+ /**
220
+ * Context she need not say (screen acts), including what arrives before the
221
+ * line opens. Held until the session exists.
222
+ */
223
+ const outboxRef = useRef<Outbox<string> | null>(null);
224
+ const outbox = (outboxRef.current ??= createOutbox<string>());
205
225
  /** Wants a call, including while connecting. */
206
226
  const calling = useRef(false);
207
227
  /**
@@ -210,10 +230,18 @@ export function useThursday() {
210
230
  * in `call` or the hang-up press is swallowed too.
211
231
  */
212
232
  const opening = useRef(false);
233
+ const ending = useRef(false);
234
+ /**
235
+ * Which attempt owns the line. A hang-up during startup moves it on, so an
236
+ * attempt still connecting cannot come up after a newer call has been placed.
237
+ */
238
+ const attempt = useRef(0);
213
239
 
214
- const showTool = useCallback((call: RealtimeToolCall) => {
240
+ const showTool = useCallback((call: LiveToolCall) => {
215
241
  if (linger.current) clearTimeout(linger.current);
242
+ relayOpen.current = false;
216
243
  setTool({
244
+ id: call.id,
217
245
  name: call.name,
218
246
  line: toolLine(call.name, call.arguments),
219
247
  bot: toolBot(call.name, call.arguments),
@@ -221,21 +249,22 @@ export function useThursday() {
221
249
  });
222
250
  }, []);
223
251
 
224
- /** A relay takes the tool line and stays long enough to read. */
225
- const showRelay = useCallback((run: ToolRun) => {
252
+ /**
253
+ * A relay takes the tool line and runs there like a tool does: while it goes in,
254
+ * and while she voices it. Her voice finishing it is what stops the motion
255
+ * (doneReading); after that it stays long enough to read.
256
+ */
257
+ const showRelay = useCallback((run: ActivityLine) => {
226
258
  if (linger.current) clearTimeout(linger.current);
227
- setTool(run);
228
- linger.current = setTimeout(
229
- () => setTool((open) => (open?.kind === "relay" ? null : open)),
230
- RELAY_LINGER_MS,
231
- );
259
+ relayOpen.current = true;
260
+ setTool({ ...run, done: false });
232
261
  }, []);
233
262
 
234
263
  /** Tool finished; lingers, then clears. */
235
- const hideTool = useCallback((name: string) => {
264
+ const hideTool = useCallback((id: string) => {
236
265
  setTool((open) =>
237
266
  // a second tool already took the line; leave it
238
- open?.name === name ? { ...open, done: true } : open,
267
+ open?.id === id ? { ...open, done: true } : open,
239
268
  );
240
269
  if (linger.current) clearTimeout(linger.current);
241
270
  linger.current = setTimeout(
@@ -244,36 +273,144 @@ export function useThursday() {
244
273
  );
245
274
  }, []);
246
275
 
247
- /** The effect below is the only place that touches the session mute. */
248
- const syncMic = useCallback(() => {
249
- setMicOff(mic.current.tool || mic.current.reading);
276
+ /** The relayed update was voiced, or waited on long enough: the next can go in. */
277
+ const doneReading = useCallback(() => {
278
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
279
+ // Her voice started and stopped on it: carried
280
+ if (reading.current.spoke) {
281
+ for (const key of onLine.current) unvoiced.current.delete(key);
282
+ }
283
+ onLine.current = [];
284
+ reading.current = { on: false, spoke: false, giveUp: null };
285
+ if (!relayOpen.current) return;
286
+ relayOpen.current = false;
287
+ setTool((open) =>
288
+ open?.kind === "relay" ? { ...open, done: true } : open,
289
+ );
290
+ if (linger.current) clearTimeout(linger.current);
291
+ linger.current = setTimeout(
292
+ () => setTool((open) => (open?.kind === "relay" ? null : open)),
293
+ RELAY_LINGER_MS,
294
+ );
250
295
  }, []);
251
296
 
252
- const openMic = useCallback(() => {
253
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
254
- mic.current.giveUp = null;
255
- mic.current.reading = false;
256
- mic.current.spoke = false;
257
- syncMic();
258
- }, [syncMic]);
259
-
260
- /** A relayed line is on the wire; the mic stays muted until it is read. */
297
+ /** An update is on the wire: nothing else goes in until she has voiced it. */
261
298
  const readAloud = useCallback(() => {
262
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
263
- mic.current.reading = true;
264
- mic.current.spoke = false;
265
- mic.current.giveUp = setTimeout(() => {
266
- openMic();
267
- // a line she never voiced must not strand the ones queued behind it
268
- resumeRelay.current?.();
269
- }, RELAY_MIC_MS);
270
- syncMic();
271
- }, [openMic, syncMic]);
272
-
273
- // muting at the track level also clears the browser's recording indicator (realtime.audio setMuted)
274
- useEffect(() => {
275
- session.current?.mute(micOff);
276
- }, [micOff]);
299
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
300
+ reading.current = {
301
+ on: true,
302
+ spoke: false,
303
+ giveUp: setTimeout(doneReading, CALL_RELAY.readMs),
304
+ };
305
+ }, [doneReading]);
306
+
307
+ const setThinking = useCallback((at: number | null) => {
308
+ thinking.current = at;
309
+ setThinkingSince(at);
310
+ }, []);
311
+
312
+ /**
313
+ * What the activity line says about the backend. It starts when the backend takes the
314
+ * turn and ends on her first word, not on the response: a turn that calls two
315
+ * tools settles for a moment between them, and the wait before she speaks is
316
+ * still the same stretch of work. Both would otherwise read as the call stalling.
317
+ */
318
+ const holdThinking = useCallback(
319
+ (busy: boolean, speaking: boolean) => {
320
+ const dropTail = () => {
321
+ if (thinkTail.current) clearTimeout(thinkTail.current);
322
+ thinkTail.current = null;
323
+ };
324
+ // Her voice always wins: it is the answer the work was for
325
+ if (speaking) {
326
+ dropTail();
327
+ if (thinking.current !== null) setThinking(null);
328
+ return;
329
+ }
330
+ if (busy) {
331
+ dropTail();
332
+ if (thinking.current === null) setThinking(Date.now());
333
+ return;
334
+ }
335
+ if (thinking.current !== null && !thinkTail.current) {
336
+ thinkTail.current = setTimeout(() => {
337
+ thinkTail.current = null;
338
+ setThinking(null);
339
+ }, THINKING_TAIL_MS);
340
+ }
341
+ },
342
+ [setThinking],
343
+ );
344
+
345
+ /**
346
+ * Puts to her what background work still waits on the user, once neither
347
+ * side's words have been transcribed for CALL_RELAY.quietMs and nothing is
348
+ * running: questions not answered, jobs ended or stopped and not yet seen,
349
+ * and progress from jobs still running. Live never speaks unprompted, so
350
+ * nothing reaches the user unless this puts it in. Handling an item — an
351
+ * answer, `thread` `seen`, a click on screen — takes it off the list. Each
352
+ * item goes in once a call, and once her voice has carried it, not on later
353
+ * calls either (`told`, `unvoiced`). One append holds one kind, so an ending
354
+ * is never lost among questions. Nothing goes in once the call is ending.
355
+ */
356
+ const relayOpenWork = useCallback(() => {
357
+ const live = session.current;
358
+ const threads = latest.current;
359
+ if (!live || !threads || reading.current.on || acting.current) return;
360
+ if (leaving.current || idle.current.asked) return;
361
+ // On a call-back, why she called goes in once the opening is voiced: it is the
362
+ // first thing they ask, and a quiet line would come too late
363
+ const rung = rang.current;
364
+ rang.current = false;
365
+ if (!rung && Date.now() - heard.current < CALL_RELAY.quietMs) return;
366
+ const open = openWork(threads).filter(
367
+ (item) => !told.current.has(item.key),
368
+ );
369
+ const first = open[0];
370
+ if (!first) return;
371
+ const due = open
372
+ .filter((item) => item.kind === first.kind)
373
+ .slice(0, CALL_RELAY.perTurn);
374
+ const lines = due.map((item) => item.line);
375
+
376
+ // Told as it goes out, even if Live refuses it: a refusal is tried again on the next
377
+ // call (unvoiced), never in a loop on this one
378
+ for (const item of due) {
379
+ told.current.add(item.key);
380
+ unvoiced.current.add(item.key);
381
+ }
382
+ readAloud();
383
+ onLine.current = due.map((item) => item.key);
384
+ // On the line before it goes out, so it runs there for the whole wait
385
+ showRelay((due.at(-1) ?? first).show);
386
+ // Appends go out in order, so the note is in her context before the first update
387
+ if (!relayNoted.current) {
388
+ relayNoted.current = true;
389
+ void live.append("instructions", RELAY_NOTE);
390
+ }
391
+ void live
392
+ .append(
393
+ "commentary",
394
+ lines.length === 1
395
+ ? lines.join("")
396
+ : `[${lines.length} updates.]\n\n${lines.join("\n\n")}`,
397
+ )
398
+ .then((delivered) => {
399
+ if (!delivered) {
400
+ doneReading();
401
+ return;
402
+ }
403
+ const ids = due.flatMap((item) => item.relayIds);
404
+ if (ids.length) return acceptThreadRelaysAction(ids).then(unwrapResult);
405
+ })
406
+ .catch((cause) =>
407
+ toast.add({
408
+ type: "error",
409
+ title: "Could not record relay delivery",
410
+ description: errorToString(cause),
411
+ }),
412
+ );
413
+ }, [readAloud, doneReading, showRelay]);
277
414
 
278
415
  /**
279
416
  * One timer; when it fires it wears the last wanted face, so a tool shorter
@@ -335,24 +472,22 @@ export function useThursday() {
335
472
  [restFace, wearFace],
336
473
  );
337
474
 
338
- const onCall = status !== "idle" && status !== "connecting";
475
+ const onCall =
476
+ status !== "idle" && status !== "connecting" && status !== "ending";
339
477
 
340
478
  // Inbox, with or without a call. Revalidated on server events; polling is the safety net
341
- const { data: tasks } = useServerRoute<Task[]>(queryKey.tasks, {
342
- refreshInterval: TASK_POLL_FALLBACK_MS,
479
+ const { data: threads } = useServerRoute<Thread[]>(queryKey.threads, {
480
+ refreshInterval: THREAD_POLL_FALLBACK_MS,
343
481
  });
344
482
 
345
483
  // Server event stream (app/api/events): signals revalidate their key
346
- const boot = useRef<string | null>(null);
347
484
  useAppEvent({
348
- hello: (event) => {
485
+ hello: () => {
349
486
  // Sent on every connect, so this is also every reconnect: nothing that
350
- // changed while the stream was down raised a signal anybody heard, and a
351
- // changed `boot` means the server restarted on top of that
352
- boot.current = event.boot;
487
+ // changed while the stream was down raised a signal anybody heard
353
488
  void revalidateAll();
354
489
  },
355
- tasks: () => void revalidate(queryKey.tasks),
490
+ threads: () => void revalidate(queryKey.threads),
356
491
  memory: () => void revalidate(queryKey.memory),
357
492
  mcp: () => {
358
493
  void revalidate(queryKey.mcp);
@@ -368,51 +503,34 @@ export function useThursday() {
368
503
  // faces come from the bot list
369
504
  const { data: bots } = useServerRoute<Bot[]>(queryKey.bot);
370
505
  useEffect(() => {
371
- if (tasks) botTasks.sync(tasks, bots);
372
- }, [tasks, bots]);
373
-
374
- // A question is relayed on every call, because it still needs an answer. An
375
- // ending is relayed only on the call it happened during: one from before is
376
- // already in her prompt, on the `delegate` line that opened it. Telling is not
377
- // seeing — the badge clears when the user opens the job. Sending while the
378
- // line is still opening is fine: the outbox holds the line. `status` is in
379
- // deps because the guard is a ref; without it a list that arrived before the
380
- // line opened would wait for the next tasks change
506
+ if (threads) botThreads.sync(threads, bots);
507
+ }, [threads, bots]);
508
+
381
509
  useEffect(() => {
382
- if (!tasks || !calling.current) return;
383
- for (const task of tasks) {
384
- if (task.status === "running") continue;
385
- // A stop the app picks back up by itself is not news: it runs again in a moment
386
- if (task.ask?.auto) continue;
387
- if (
388
- task.status !== "waiting" &&
389
- toDate(task.updatedAt).getTime() < placedAt.current
390
- )
391
- continue;
392
- const key = relayKey(task);
393
- if (relayed.current.has(key)) continue;
394
- relayed.current.add(key);
395
- outbox.send({ line: relayLine(task), show: relayShow(task) });
396
- }
397
- }, [tasks, outbox, status]);
510
+ latest.current = threads;
511
+ }, [threads]);
398
512
 
399
- // The user acted on a task on screen; the model may have just read that question and must not ask again
513
+ // The user acted on a thread on screen; she may have just read that question and must not ask again
400
514
  useEffect(
401
515
  () =>
402
516
  screenActs.subscribe((act) => {
403
517
  if (!calling.current) return;
404
- outbox.send({
405
- line:
406
- act.kind === "answered"
407
- ? `The user answered task "${act.label}" on screen: ${act.answer}. The bot is going on with that — do not ask again.`
408
- : `The user stopped task "${act.label}" on screen. It is not running any more — do not wait for it or say anything more about it.`,
409
- });
518
+ outbox.send(
519
+ act.kind === "answered"
520
+ ? `The user sent a message on screen to ${act.recipient ?? "the coordinator"} in thread "${act.label}" (${act.id})${act.replyTo ? `, replying to ${act.replyTo}` : ""}: ${act.answer}. It has reached that participant.`
521
+ : `The user stopped thread "${act.label}" on screen. It is no longer running.`,
522
+ );
410
523
  }),
411
524
  [outbox],
412
525
  );
413
526
 
414
527
  /** Closes the session and resets state. Turns were saved during the call. */
415
528
  const hangUp = useCallback(async () => {
529
+ if (ending.current) return;
530
+ ending.current = true;
531
+ if (leaving.current) clearInterval(leaving.current);
532
+ leaving.current = null;
533
+ attempt.current += 1;
416
534
  const live = session.current;
417
535
  const call = callId.current;
418
536
 
@@ -420,16 +538,16 @@ export function useThursday() {
420
538
  callId.current = null;
421
539
  calling.current = false;
422
540
  opening.current = false;
423
- relayed.current.clear();
424
- // unsent lines go: a question is relayed again next call, an ending is in her prompt
541
+ relayNoted.current = false;
542
+ rang.current = false;
543
+ // What she did not voice goes in again next call; unsent context goes with the session
544
+ for (const key of unvoiced.current) told.current.delete(key);
545
+ unvoiced.current.clear();
425
546
  outbox.close();
426
547
  outbox.clear();
427
- // holds the closed session; the next call sets its own
428
- resumeRelay.current = null;
429
548
  working.current?.abort();
430
549
  working.current = null;
431
- mic.current.tool = false;
432
- openMic();
550
+ doneReading();
433
551
  if (linger.current) clearTimeout(linger.current);
434
552
  // a pending face timer must not fire after the call
435
553
  restFace();
@@ -437,28 +555,38 @@ export function useThursday() {
437
555
  idle.current = { since: 0, asked: false, owed: null, grace: null };
438
556
  setIdleLeft(null);
439
557
  setSince(null);
440
- setStatus("idle");
558
+ setStatus(live ? "ending" : "idle");
441
559
  setMessages([]);
442
560
  setTool(null);
443
- // the room is not cleared: tasks outlive the call
444
-
445
- // closing finalizes a mid-speech turn, so its save is queued first
446
- live?.close();
447
- if (call) void endCallAction(call);
561
+ setThinking(null);
562
+ if (thinkTail.current) clearTimeout(thinkTail.current);
563
+ thinkTail.current = null;
564
+ relayOpen.current = false;
565
+ // the room is not cleared: threads outlive the call
566
+
567
+ // Waits (bounded) for session.closed, which saves the last turns and says
568
+ // what was billed; the row is ended with that, after the saves it queued
569
+ await live?.close();
570
+ const close = finalized.current;
571
+ finalized.current = null;
572
+ ending.current = false;
573
+ setStatus("idle");
574
+ if (call) void endCallAction(call, close);
448
575
  // only when there was a line to close
449
576
  if (live && farewell.current) {
450
577
  farewell.current.currentTime = 0;
451
578
  void farewell.current.play().catch(() => {});
452
579
  }
453
- }, [outbox, restFace, openMic]);
580
+ }, [outbox, restFace, doneReading, setThinking]);
454
581
 
455
582
  // Unmount during a call must release the mic and stop tools
456
583
  useEffect(() => {
457
584
  return () => {
458
- session.current?.close();
585
+ attempt.current += 1;
586
+ void session.current?.close();
459
587
  session.current = null;
460
588
  // Closing only the session would leave the row live (thursday.query
461
- // isAnyCallLive) and finished tasks unannounced (bot.runner); end the row too
589
+ // isAnyCallLive) and finished threads unannounced (bot.runner); end the row too
462
590
  const call = callId.current;
463
591
  callId.current = null;
464
592
  calling.current = false;
@@ -469,27 +597,48 @@ export function useThursday() {
469
597
  // clear timers so nothing sets state on an unmounted tree
470
598
  if (linger.current) clearTimeout(linger.current);
471
599
  if (faceIn.current) clearTimeout(faceIn.current);
472
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
600
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
601
+ if (thinkTail.current) clearTimeout(thinkTail.current);
473
602
  if (idle.current.grace) clearTimeout(idle.current.grace);
603
+ if (failedFor.current) clearTimeout(failedFor.current);
604
+ if (leaving.current) clearInterval(leaving.current);
474
605
  };
475
606
  }, []);
476
607
 
477
608
  /**
478
- * Rewinds the idle clock. User activity also cancels a requested goodbye;
479
- * agent activity does not, so the goodbye cannot cancel itself.
609
+ * The backend ended the call: hang up once her goodbye is over. She says it while
610
+ * the backend works, so it may be done already, still going, or yet to start;
611
+ * CALL_END bounds each case. The tool's own result always gets `quietMs` to go out.
480
612
  */
481
- const stir = useCallback((who: "user" | "agent") => {
613
+ const leave = useCallback(() => {
614
+ if (leaving.current) return;
615
+ const asked = Date.now();
616
+ leaving.current = setInterval(() => {
617
+ const now = Date.now();
618
+ // A goodbye heard just before the backend ended the call counts as said
619
+ const said = voiced.current > asked - CALL_END.unsaidMs;
620
+ const over = said
621
+ ? now - Math.max(voiced.current, asked) >= CALL_END.quietMs
622
+ : now - asked >= CALL_END.unsaidMs;
623
+ if (over || now - asked >= CALL_END.maxMs) void hangUp();
624
+ }, 100);
625
+ }, [hangUp]);
626
+
627
+ /**
628
+ * Rewinds the idle clock. `user` is new words transcribed from them, first seen at
629
+ * `said`: an answer is owed from then, and a requested goodbye is off. Transcripts
630
+ * lag the audio, so her voice heard since `said` already answered them. `agent` is
631
+ * her voice or backend work, which settles what is owed but cannot cancel its own
632
+ * goodbye.
633
+ */
634
+ const stir = useCallback((who: "user" | "agent", said = 0) => {
482
635
  const now = Date.now();
483
636
  idle.current.since = now;
484
637
  if (who !== "user") {
485
- // Words or a tool: she is on the line, so nothing is owed.
486
638
  idle.current.owed = null;
487
639
  return;
488
640
  }
489
- // Rewound by anything the user does, transcript deltas included, so a long
490
- // turn is never mistaken for a line that stopped answering; it runs from the
491
- // moment they stopped rather than from the moment they started.
492
- idle.current.owed = now;
641
+ if (voiced.current < said) idle.current.owed ??= now;
493
642
  if (idle.current.grace) clearTimeout(idle.current.grace);
494
643
  idle.current.grace = null;
495
644
  idle.current.asked = false;
@@ -503,7 +652,7 @@ export function useThursday() {
503
652
  const live = session.current;
504
653
  if (!live) return;
505
654
  const owed = idle.current.owed;
506
- if (owed !== null && Date.now() - owed >= AGENT_SILENT_MS) {
655
+ if (owed !== null && Date.now() - owed >= CALL_IDLE.agentSilentMs) {
507
656
  // Nothing to say goodbye with; the model is what would have said it.
508
657
  toast.add({
509
658
  type: "error",
@@ -513,225 +662,300 @@ export function useThursday() {
513
662
  void hangUp();
514
663
  return;
515
664
  }
516
- const left = IDLE_HANG_UP_MS - (Date.now() - idle.current.since);
665
+ const left = CALL_IDLE.hangUpMs - (Date.now() - idle.current.since);
517
666
  setIdleLeft(
518
- left <= IDLE_WARN_MS && !idle.current.asked
667
+ left <= CALL_IDLE.warnMs && !idle.current.asked
519
668
  ? Math.max(0, Math.ceil(left / 1000))
520
669
  : null,
521
670
  );
522
671
  if (left > 0 || idle.current.asked) return;
523
672
 
524
673
  idle.current.asked = true;
525
- live.say(IDLE_LINE);
526
- idle.current.grace = setTimeout(() => void hangUp(), IDLE_GRACE_MS);
674
+ void live.append("instructions", IDLE_LINE);
675
+ idle.current.grace = setTimeout(() => void hangUp(), CALL_IDLE.graceMs);
527
676
  }, 1000);
528
677
  return () => clearInterval(tick);
529
678
  }, [onCall, stir, hangUp]);
530
679
 
531
- const call = useCallback(async () => {
532
- // Guard with a ref, not `status`: three entry points (face, wake word,
533
- // hotkey) can fire in one frame and both see a stale "idle", opening two sessions
534
- if (opening.current) return;
535
- // with a line open this press hangs up; `calling` covers the gap before re-render
536
- if (calling.current || status !== "idle") return hangUp();
537
-
538
- setStatus("connecting");
539
- // from here on this is a call; the outbox holds lines until the session exists
540
- opening.current = true;
541
- calling.current = true;
542
- placedAt.current = Date.now();
543
- outbox.clear();
544
- try {
545
- const handshake = unwrapResult(await openCallAction(thursdaySettings()));
546
- callId.current = handshake.callId;
547
-
548
- const stop = new AbortController();
549
- working.current = stop;
550
- const saving = { failures: 0 };
551
-
552
- tap.current ??= createAudioTap();
553
- // Inside the gesture: an AudioContext created elsewhere starts suspended.
554
- // Calls from the wake word or a call-back have no gesture; armAudioUnlock handles those
555
- armAudioUnlock(tap.current.open().context);
556
- const chime = new Audio(CONNECTED_SOUND);
557
- farewell.current ??= new Audio(HUNG_UP_SOUND);
558
-
559
- // one turn per seq; the session reports turns one at a time
560
- const turns: CallMessage[] = [];
561
-
562
- const live = await openRealtimeSession({
563
- provider: handshake.provider,
564
- credential: handshake.credential,
565
- setup: handshake.session,
566
- audio: tap.current,
567
- on: {
568
- // the session calls tools; the page forwards them to the server
569
- runTool: async (call) => {
570
- // end_call is the page's only tool. Answer without awaiting: the socket
571
- // goes down with the call, so this reply must go first
572
- if (call.name === TOOL_NAMES.end_call) {
573
- setTimeout(() => void hangUp(), HANG_UP_DELAY_MS);
574
- return "Ending the call.";
575
- }
576
- showTool(call);
577
- try {
578
- return await runRemoteTool(handshake.callId, call, stop.signal);
579
- } finally {
580
- hideTool(call.name);
581
- }
582
- },
583
- turn: (turn) => {
584
- stir(turn.role === "user" ? "user" : "agent");
585
- // tool turns are saved but not shown
586
- if (turn.role !== "tool") {
587
- turns[turn.seq] = {
588
- id: turn.id,
589
- role: turn.role,
590
- text: turn.text,
591
- };
592
- // holes are turns whose text has not arrived
593
- setMessages(turns.filter(Boolean).slice(-KEEP_MESSAGES));
594
- }
595
- if (turn.done) {
596
- const { id, role, tool, text, seq } = turn;
597
- persistTurn(
598
- handshake.callId,
599
- { id, role, tool, text, seq },
600
- saving,
601
- );
680
+ // The relay clock: open work goes in only while the line is quiet
681
+ useEffect(() => {
682
+ if (!onCall) return;
683
+ const tick = setInterval(relayOpenWork, 1000);
684
+ return () => clearInterval(tick);
685
+ }, [onCall, relayOpenWork]);
686
+
687
+ const call = useCallback(
688
+ async (placedBy?: "callBack") => {
689
+ // Compared exactly: a face tap hands its click event in here
690
+ const calledBack = placedBy === "callBack";
691
+ // Guard with a ref, not `status`: three entry points (face, wake word,
692
+ // hotkey) can fire in one frame and both see a stale "idle", opening two sessions
693
+ if (opening.current || ending.current) return;
694
+ // with a line open this press hangs up; `calling` covers the gap before re-render
695
+ if (calling.current || status !== "idle") return hangUp();
696
+
697
+ setStatus("connecting");
698
+ showFailed(false);
699
+ // from here on this is a call; the outbox holds updates until the session exists
700
+ opening.current = true;
701
+ calling.current = true;
702
+ finalized.current = null;
703
+ relayNoted.current = false;
704
+ outbox.clear();
705
+ attempt.current += 1;
706
+ const mine = attempt.current;
707
+ const current = () => calling.current && attempt.current === mine;
708
+ try {
709
+ // Inside the gesture, before anything awaits: an AudioContext created later
710
+ // starts suspended. Calls from the wake word or a call-back have no gesture;
711
+ // armAudioUnlock handles those
712
+ tap.current ??= createAudioTap();
713
+ armAudioUnlock(tap.current.open().context);
714
+ const chime = new Audio(CONNECTED_SOUND);
715
+ farewell.current ??= new Audio(HUNG_UP_SOUND);
716
+
717
+ const settings = thursdaySettings();
718
+ const stop = new AbortController();
719
+ working.current = stop;
720
+ const saving = { failures: 0 };
721
+ const thinkingSaves = { failures: 0 };
722
+ /** Filled in by the handshake, read by callbacks that only run after it. */
723
+ const line = { callId: "", opening: null as string | null };
724
+
725
+ // one turn per id; the session reports display groups one at a time
726
+ const turns = new Map<string, CallMessage & { seq: number }>();
727
+ /** When the page first saw each of the user's display groups. */
728
+ const userSaid = new Map<string, number>();
729
+
730
+ const live = await openLiveSession({
731
+ initialize: async (sdp) => {
732
+ const handshake = unwrapResult(
733
+ await openCallAction(settings, sdp, calledBack),
734
+ );
735
+ if (!current()) {
736
+ void endCallAction(handshake.callId);
737
+ throw new Error("The call closed during startup.");
602
738
  }
739
+ callId.current = handshake.callId;
740
+ line.callId = handshake.callId;
741
+ line.opening = handshake.opening;
742
+ return handshake.sdp;
603
743
  },
604
- // session facts drive the face through showFace; the tool line is drawn by the tool itself
605
- activity: (activity) => {
606
- showFace(statusOf(activity));
607
- if (activity.hearing) stir("user");
608
- else if (activity.speaking || activity.tools.length) stir("agent");
609
- // Tool mute is the live fact. Relay mute ends once playback has started and
610
- // stopped: turn text is finalized before audio (realtime.session), so playback is what counts
611
- mic.current.tool = activity.tools.length > 0;
612
- if (mic.current.reading) {
613
- if (activity.speaking) mic.current.spoke = true;
614
- else if (mic.current.spoke) {
615
- mic.current.reading = false;
616
- // she has finished reading; whatever landed meanwhile goes now
617
- resumeRelay.current?.();
744
+ audio: tap.current,
745
+ on: {
746
+ // the backend calls tools; the page forwards them to the server
747
+ runTool: async (call) => {
748
+ // end_call is the page's only tool. The line goes down once her
749
+ // goodbye is over (leave), so this reply reaches the model first
750
+ if (call.name === TOOL_NAMES.end_call) {
751
+ leave();
752
+ return "Ending the call.";
618
753
  }
619
- }
620
- if (!mic.current.reading) openMic();
621
- else syncMic();
622
- },
623
- warn: (description) =>
624
- toast.add({ type: "warning", title: "Call warning", description }),
625
- failed: (description) => {
626
- toast.add({ type: "error", title: "Call failed", description });
627
- void hangUp();
754
+ showTool(call);
755
+ try {
756
+ return await runRemoteTool(line.callId, call, stop.signal);
757
+ } finally {
758
+ hideTool(call.id);
759
+ }
760
+ },
761
+ reasoning: (part) =>
762
+ persist(
763
+ thinkingSaves,
764
+ () =>
765
+ saveThoughtAction(line.callId, {
766
+ ...part,
767
+ seq: Math.max(0, Math.round(part.seq)),
768
+ }),
769
+ "The backend's thinking is not being saved",
770
+ ),
771
+ turn: (turn) => {
772
+ // New words, not a checkpoint of the same ones: a blank fragment is not words
773
+ const fresh =
774
+ turn.role !== "tool" &&
775
+ Boolean(turn.text.trim()) &&
776
+ turn.text !== turns.get(turn.id)?.text;
777
+ // New words from either side restart the relay's quiet clock
778
+ if (fresh) heard.current = Date.now();
779
+ // Her voice and backend work rewind the idle clock from activity, where an
780
+ // update she reads out is told apart; transcripts lag the audio and cannot
781
+ if (fresh && turn.role === "user") {
782
+ const said = userSaid.get(turn.id) ?? Date.now();
783
+ userSaid.set(turn.id, said);
784
+ stir("user", said);
785
+ }
786
+ // Tool turns are saved but not shown. Hanging up clears the screen
787
+ // before `close()` checkpoints open groups, so only the live call draws.
788
+ if (turn.role !== "tool" && callId.current === line.callId) {
789
+ turns.set(turn.id, {
790
+ seq: turn.seq,
791
+ id: turn.id,
792
+ role: turn.role,
793
+ text: turn.text,
794
+ });
795
+ setMessages(
796
+ [...turns.values()]
797
+ .sort((a, b) => a.seq - b.seq)
798
+ .slice(-KEEP_MESSAGES),
799
+ );
800
+ }
801
+ if (turn.done) {
802
+ persist(saving, () =>
803
+ saveTurnsAction(line.callId, [
804
+ {
805
+ id: turn.id,
806
+ role: turn.role,
807
+ tool: turn.tool,
808
+ text: turn.text,
809
+ // Live orders by audio milliseconds; the row keeps a whole number
810
+ seq: Math.max(0, Math.round(turn.seq)),
811
+ fragments: turn.fragments ?? null,
812
+ },
813
+ ]),
814
+ );
815
+ }
816
+ },
817
+ // session facts drive the face through showFace; the tool line is drawn by the tool itself
818
+ activity: (activity) => {
819
+ showFace(statusOf(activity));
820
+ const busy = activity.working || activity.tools.length > 0;
821
+ acting.current = busy;
822
+ // From the moment the backend picks the turn up until her first word
823
+ holdThinking(busy, activity.speaking);
824
+ if (activity.speaking) voiced.current = Date.now();
825
+ // Sound on the mic alone does not rewind the clock: a noisy room would
826
+ // keep a call open forever. Her words and backend work do, except an
827
+ // update she voices on her own: waiting results must not hold a call open.
828
+ if (
829
+ (activity.speaking && !reading.current.on) ||
830
+ activity.working ||
831
+ activity.tools.length
832
+ )
833
+ stir("agent");
834
+ // An update counts as voiced once her voice has started and stopped
835
+ if (reading.current.on) {
836
+ if (activity.speaking) reading.current.spoke = true;
837
+ else if (reading.current.spoke) doneReading();
838
+ }
839
+ },
840
+ finalized: (close) => {
841
+ finalized.current = close;
842
+ },
843
+ warn: (description) =>
844
+ toast.add({
845
+ type: "warning",
846
+ title: "Call warning",
847
+ description,
848
+ }),
849
+ failed: (description) => {
850
+ toast.add({ type: "error", title: "Call failed", description });
851
+ showFailed(true);
852
+ void hangUp();
853
+ },
628
854
  },
629
- },
630
- });
631
-
632
- /**
633
- * One line at a time. Three jobs landing together used to be three
634
- * system items and three turns in a row, which the user hears
635
- * as her talking to herself; the box stays closed while she reads one,
636
- * and whatever arrived meanwhile goes out merged, as one thing to say.
637
- */
638
- const flush = (queued: Said[]) => {
639
- if (!queued.length) return;
640
- // not the user's turn while the model reads this
641
- readAloud();
642
- live.say(queued.length === 1 ? queued[0].line : mergedRelay(queued));
643
- const last = queued.at(-1)?.show;
644
- if (last) showRelay(last);
645
- outbox.close();
646
- };
647
-
648
- resumeRelay.current = () => {
649
- const held = outbox.clear();
650
- outbox.open((one) => flush([one]));
651
- flush(held);
652
- };
653
-
654
- // line open: flush held lines first
655
- outbox.open((one) => flush([one]));
855
+ });
656
856
 
657
- // An opening — a first call, or memory to tidy — makes the model speak first, on the same path as a relay
658
- if (handshake.opening) {
659
- readAloud();
660
- live.say(handshake.opening);
857
+ if (!current()) {
858
+ // Hung up while the line was going up; hangUp already ended the row
859
+ await live.close();
860
+ return;
861
+ }
862
+
863
+ /** An opening that never landed leaves nothing to wait for. */
864
+ const unless = (delivered: boolean) => {
865
+ if (!delivered) doneReading();
866
+ };
867
+
868
+ session.current = live;
869
+ opening.current = false;
870
+ rang.current = calledBack;
871
+ // Context is not gated: held lines first, then each as it comes
872
+ outbox.open((text) => void live.append("thinking", text));
873
+ // The quiet clock starts with the line, so nothing is put to her the moment it opens
874
+ heard.current = Date.now();
875
+
876
+ if (line.opening) {
877
+ // The greeting goes first; open work waits until she has said it
878
+ readAloud();
879
+ void live.append("instructions", line.opening).then(unless);
880
+ }
881
+
882
+ wearFace("listening");
883
+ setSince(Date.now());
884
+ // a rejected chime is not worth a message
885
+ void chime.play().catch(() => {});
886
+ } catch (cause) {
887
+ // Hanging up while the line was going up is not a failure to report
888
+ if (current()) {
889
+ toast.add({
890
+ type: "error",
891
+ title: "Could not start the call",
892
+ description: errorToString(cause),
893
+ });
894
+ showFailed(true);
895
+ }
896
+ if (attempt.current !== mine) return;
897
+ // a call that never opened still has a row; close it
898
+ if (callId.current) void endCallAction(callId.current);
899
+ callId.current = null;
900
+ opening.current = false;
901
+ calling.current = false;
902
+ working.current = null;
903
+ outbox.clear();
904
+ setStatus("idle");
661
905
  }
662
-
663
- session.current = live;
664
- opening.current = false;
665
- wearFace("listening");
666
- setSince(Date.now());
667
- // a rejected chime is not worth a message
668
- void chime.play().catch(() => {});
669
- } catch (cause) {
670
- toast.add({
671
- type: "error",
672
- title: "Could not start the call",
673
- description: errorToString(cause),
674
- });
675
- // a call that never opened still has a row; close it
676
- if (callId.current) void endCallAction(callId.current);
677
- callId.current = null;
678
- opening.current = false;
679
- calling.current = false;
680
- outbox.clear();
681
- // nothing was delivered; forget so the next call resends
682
- relayed.current.clear();
683
- setStatus("idle");
684
- }
685
- }, [
686
- status,
687
- hangUp,
688
- showTool,
689
- hideTool,
690
- showRelay,
691
- showFace,
692
- wearFace,
693
- stir,
694
- outbox,
695
- readAloud,
696
- openMic,
697
- syncMic,
698
- ]);
906
+ },
907
+ [
908
+ status,
909
+ hangUp,
910
+ showTool,
911
+ hideTool,
912
+ showFace,
913
+ wearFace,
914
+ stir,
915
+ outbox,
916
+ readAloud,
917
+ doneReading,
918
+ holdThinking,
919
+ showFailed,
920
+ leave,
921
+ ],
922
+ );
699
923
 
700
924
  /**
701
- * Call-back: opens a call when a task is waiting on the user and no line is
702
- * open. Tried once per task (`woke`), or a failed open would redial forever;
703
- * tasks present at app start count as already tried.
925
+ * Call-back: opens a call when a thread is waiting on the user and no line is
926
+ * open. Tried once per thread (`woke`), or a failed open would redial forever;
927
+ * threads present at app start count as already tried.
704
928
  */
705
929
  const callBack = useThursdayStore((state) => state.callBack);
706
930
  const woke = useRef<Set<string> | null>(null);
707
931
  useEffect(() => {
708
- if (!tasks) return;
709
- const wants = tasks.filter(
710
- (task) =>
711
- !task.seen &&
712
- !task.ask?.auto &&
713
- (asksSomething(task) ||
714
- (callBack === "any" && task.status !== "running")),
932
+ if (!threads) return;
933
+ const wants = threads.filter(
934
+ (thread) =>
935
+ !thread.seen &&
936
+ !thread.ask?.auto &&
937
+ (asksSomething(thread) ||
938
+ (callBack === "any" && thread.status !== "running")),
715
939
  );
716
940
 
717
941
  if (!woke.current) {
718
- woke.current = new Set(wants.map((task) => task.id));
942
+ woke.current = new Set(wants.map((thread) => thread.id));
719
943
  return;
720
944
  }
721
945
 
722
- // forget tasks that stopped waiting; a later stop counts as fresh
723
- const open = new Set(wants.map((task) => task.id));
946
+ // forget threads that stopped waiting; a later stop counts as fresh
947
+ const open = new Set(wants.map((thread) => thread.id));
724
948
  for (const id of woke.current) {
725
949
  if (!open.has(id)) woke.current.delete(id);
726
950
  }
727
951
 
728
952
  if (callBack === "off" || status !== "idle") return;
729
- const fresh = wants.filter((task) => !woke.current?.has(task.id));
953
+ const fresh = wants.filter((thread) => !woke.current?.has(thread.id));
730
954
  if (!fresh.length) return;
731
955
 
732
- for (const task of wants) woke.current.add(task.id);
733
- void call();
734
- }, [tasks, callBack, status, call]);
956
+ for (const thread of wants) woke.current.add(thread.id);
957
+ void call("callBack");
958
+ }, [threads, callBack, status, call]);
735
959
 
736
960
  /** Read by the face once per animation frame, outside React state. */
737
961
  const getSpectrum = useCallback(() => tap.current?.read() ?? EMPTY_BANDS, []);
@@ -748,12 +972,15 @@ export function useThursday() {
748
972
  enabled: status === "idle" && wake.enabled && !wakeBlocked,
749
973
  phrases: [wake.phrase],
750
974
  onWake: () => void call(),
751
- onError: () => setWakeBlocked(true),
975
+ onError: (reason) => {
976
+ setWakeBlocked(true);
977
+ toast.add({ type: "error", title: "Wake word off", description: reason });
978
+ },
752
979
  });
753
980
 
754
981
  // Hotkey presses `call` like the face tap, so it hangs up during a call; disabled while the line goes up or down
755
982
  const hotkey = useThursdayStore((state) => state.hotkey);
756
- const busy = status === "connecting";
983
+ const busy = status === "connecting" || status === "ending";
757
984
  useHotkey({
758
985
  enabled: hotkey.enabled && !busy,
759
986
  combo: hotkey.enabled && isCombo(hotkey.combo) ? hotkey.combo : null,
@@ -762,12 +989,14 @@ export function useThursday() {
762
989
 
763
990
  return {
764
991
  status,
992
+ /** A call just failed to open or dropped; true for a few seconds after. */
993
+ failed,
765
994
  messages,
766
995
  tool,
996
+ /** When the backend picked the turn up (ms); null when it is not working. */
997
+ thinkingSince,
767
998
  /** Seconds until idle hang-up; null outside the warning window. */
768
999
  idleLeft,
769
- /** Mic muted: a tool is running or a relay is being read. */
770
- micOff,
771
1000
  /** When the line opened (ms); null without a call. */
772
1001
  since,
773
1002
  call,
@@ -780,11 +1009,7 @@ export function useThursday() {
780
1009
  };
781
1010
  }
782
1011
 
783
- const EMPTY_BANDS = new Array<number>(MARK_BANDS).fill(0);
784
-
785
- /** Relay dedup key: task id plus its last state change. */
786
- const relayKey = (task: Task) =>
787
- `${task.id}@${toDate(task.updatedAt).getTime()}`;
1012
+ const EMPTY_BANDS = new Array<number>(SPECTRUM_BANDS).fill(0);
788
1013
 
789
1014
  /**
790
1015
  * Calls opened without a gesture (wake word, call-back) get a suspended
@@ -816,34 +1041,32 @@ function armAudioUnlock(context: AudioContext) {
816
1041
  });
817
1042
  }
818
1043
 
819
- /** Tools win over speech; delegate gets its own face. */
820
- function statusOf(activity: RealtimeActivity): LiveStatus {
821
- if (activity.tools.length) {
1044
+ /** Voice wins over work; delegate gets its own face. */
1045
+ function statusOf(activity: LiveActivity): LiveStatus {
1046
+ if (activity.speaking) return "speaking";
1047
+ if (activity.working || activity.tools.length) {
822
1048
  return activity.tools.includes(TOOL_NAMES.delegate)
823
1049
  ? "delegating"
824
1050
  : "working";
825
1051
  }
826
- return activity.speaking ? "speaking" : "listening";
1052
+ return "listening";
827
1053
  }
828
1054
 
829
- /**
830
- * Fire-and-forget save: one toast after SAVE_FAILURE_LIMIT failures, then
831
- * silence.
832
- */
833
- function persistTurn(
834
- callId: string,
835
- turn: CallTurn,
1055
+ /** Fire-and-forget save; after SAVE_FAILURE_LIMIT failures of this kind it stops and says so once. */
1056
+ function persist(
836
1057
  saving: { failures: number },
1058
+ save: () => Promise<Result<unknown>>,
1059
+ stopped = "This call is not being saved",
837
1060
  ) {
838
1061
  if (saving.failures >= SAVE_FAILURE_LIMIT) return;
839
- void saveTurnsAction(callId, [turn])
1062
+ void save()
840
1063
  .then((result) => unwrapResult(result))
841
1064
  .catch((cause) => {
842
1065
  saving.failures += 1;
843
1066
  if (saving.failures !== SAVE_FAILURE_LIMIT) return;
844
1067
  toast.add({
845
1068
  type: "warning",
846
- title: "This call is not being saved",
1069
+ title: stopped,
847
1070
  description: errorToString(cause),
848
1071
  });
849
1072
  });
@@ -853,58 +1076,110 @@ function persistTurn(
853
1076
  * Waiting on a real question. A job the app stopped waits in the inbox
854
1077
  * instead of ringing.
855
1078
  */
856
- function asksSomething(task: Task) {
857
- return task.status === "waiting" && !isAppStop(task.ask);
858
- }
1079
+ const asksSomething = (thread: Thread) => thread.room.questions.length > 0;
1080
+
1081
+ /** One piece of background work waiting on the user, as the relay clock puts it to her. */
1082
+ type OpenWork = {
1083
+ key: string;
1084
+ /** What it is, so one append holds one kind. */
1085
+ kind: "question" | "ending" | "progress";
1086
+ /** The relay text. */
1087
+ line: string;
1088
+ /** Relay rows it covers, accepted once it lands. */
1089
+ relayIds: number[];
1090
+ /** The same item on the activity line, so the user sees where her words came from. */
1091
+ show: ActivityLine;
1092
+ };
859
1093
 
860
- /**
861
- * Sent as a system entry (realtime.driver say); the first sentence names
862
- * sender and recipient or the model answers it as user speech.
863
- */
864
- function relayLine(task: Task) {
865
- // The label travels with the answer: what the user says next about this job
866
- // is a word to it, and the task tool takes it by that name
867
- const from = `[${task.bot} → you, about "${task.label}" — not the user speaking; they have not heard this. Anything more on this job goes to the task tool as "${task.label}", never a new job.]`;
868
- switch (task.status) {
869
- case "waiting": {
870
- // a stop is told, not read as a question
871
- const options = task.ask?.options ?? [];
872
- const said = task.ask?.question ?? task.outcome ?? "";
873
- if (isAppStop(task.ask)) {
874
- return `${from} It stopped before finishing. Where it got to: ${said} Ask whether to keep going; if yes, send "${TASK_CONTINUE}" with the task tool.`;
875
- }
876
- const list = options.length ? ` Options: ${options.join(" / ")}.` : "";
877
- return `${from} It asks: ${said}${list} Answer with the task tool if you can; otherwise ask the user and send back what they say.`;
878
- }
879
- case "failed":
880
- return `${from} It could not finish: ${task.outcome ?? ""}`;
881
- default:
882
- return `${from} Done. Its answer: ${task.outcome ?? ""}`;
883
- }
884
- }
1094
+ const OPEN_RANK = {
1095
+ question: 0,
1096
+ stopped: 1,
1097
+ done: 2,
1098
+ progress: 3,
1099
+ } as const;
885
1100
 
886
1101
  /**
887
- * Lines that piled up while she was reading, as one system entry. The header
888
- * is what stops it becoming one turn per job: they arrived together and the
889
- * user has heard none of them.
1102
+ * Everything in the inbox still waiting on the user, most pressing first:
1103
+ * questions, then jobs stopped or finished and not yet seen, then
1104
+ * progress from jobs still running. Sent as commentary; the bracket carries
1105
+ * facts only — who, which thread, where an answer goes — because the backend
1106
+ * reads relays too and routes answers by them.
890
1107
  */
891
- function mergedRelay(queued: Said[]) {
892
- return `[${queued.length} jobs came back at once — not the user speaking; they have heard none of this. Tell them in one turn, not ${queued.length}.]
1108
+ function openWork(threads: Thread[]): OpenWork[] {
1109
+ const items: (OpenWork & { rank: number })[] = [];
1110
+ for (const thread of threads) {
1111
+ const { relays, questions } = thread.room;
1112
+ const changed = toDate(thread.updatedAt).getTime();
1113
+ const bracket = (from: string, kind: string, tail = "") =>
1114
+ `[${from} → Thursday, thread "${thread.label}" (${thread.id}), ${kind}.${tail}]`;
1115
+ const show = (bot: string, line: string): ActivityLine => ({
1116
+ kind: "relay",
1117
+ name: thread.label,
1118
+ line,
1119
+ done: true,
1120
+ bot,
1121
+ });
893
1122
 
894
- ${queued.map((one) => one.line).join("\n\n")}`;
895
- }
1123
+ for (const question of questions) {
1124
+ const options = question.options?.length
1125
+ ? ` Options: ${question.options.join(" / ")}.`
1126
+ : "";
1127
+ items.push({
1128
+ rank: OPEN_RANK.question,
1129
+ key: `question:${question.id}`,
1130
+ kind: "question",
1131
+ line: `${bracket(question.bot, "question", ` Its answer goes to thread ${thread.id}, recipient ${question.bot}, replyTo ${question.id}.`)}\n${question.text}${options}`,
1132
+ relayIds: relays
1133
+ .filter((relay) => relay.messageId === question.id)
1134
+ .map((relay) => relay.id),
1135
+ show: show(question.bot, `${question.bot}: question`),
1136
+ });
1137
+ }
896
1138
 
897
- /** The same relay on the activity line, so the user sees where the model's line came from. */
898
- function relayShow(task: Task): ToolRun {
899
- const line =
900
- task.status === "waiting"
901
- ? isAppStop(task.ask)
902
- ? `${task.bot} stopped`
903
- : `${task.bot} is asking`
904
- : task.status === "failed"
905
- ? `${task.bot} could not finish`
906
- : `Answer from ${task.bot}`;
907
- return { kind: "relay", name: task.label, line, done: true, bot: task.bot };
908
- }
1139
+ // Relay rows that belong to no open question
1140
+ const loose = relays.filter(
1141
+ (relay) => !questions.some((question) => question.id === relay.messageId),
1142
+ );
1143
+ // A cancel is the user's own and already seen; nothing about it is news
1144
+ const ended = thread.status === "done";
1145
+ // A stop the app picks back up by itself is not news: it runs again in a moment
1146
+ const stopped =
1147
+ thread.status === "waiting" && isAppStop(thread.ask) && !thread.ask?.auto;
1148
+
1149
+ if (ended || stopped) {
1150
+ if (thread.seen) continue;
1151
+ const kind = stopped ? "stopped" : "done";
1152
+ const said =
1153
+ kind === "stopped"
1154
+ ? `It stopped before finishing. Where it got to: ${thread.ask?.question ?? thread.outcome ?? ""}`
1155
+ : `Done. Its answer: ${thread.outcome ?? ""}`;
1156
+ items.push({
1157
+ rank: OPEN_RANK[kind],
1158
+ key: `${kind}:${thread.id}@${changed}`,
1159
+ kind: "ending",
1160
+ line: `${bracket(thread.bot, kind)}\n${said}`,
1161
+ // Its ending says what its progress messages said
1162
+ relayIds: loose.map((relay) => relay.id),
1163
+ show: show(
1164
+ thread.bot,
1165
+ kind === "stopped"
1166
+ ? `${thread.bot} stopped`
1167
+ : `Answer from ${thread.bot}`,
1168
+ ),
1169
+ });
1170
+ continue;
1171
+ }
909
1172
 
910
- export type { CallMessage, CallStatus };
1173
+ for (const relay of loose) {
1174
+ items.push({
1175
+ rank: OPEN_RANK.progress,
1176
+ key: `progress:${relay.id}`,
1177
+ kind: "progress",
1178
+ line: `${bracket(relay.bot, relay.kind)}\n${relay.text}`,
1179
+ relayIds: [relay.id],
1180
+ show: show(relay.bot, `${relay.bot}: ${relay.kind}`),
1181
+ });
1182
+ }
1183
+ }
1184
+ return items.sort((a, b) => a.rank - b.rank);
1185
+ }