@maudecode/talaria-web 1.18.0 → 1.20.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 (774) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.js +16 -16
  3. package/dist/api/auth-raw.js.map +1 -1
  4. package/dist/api/automation-raw.d.ts +2 -2
  5. package/dist/api/automation-raw.js +93 -38
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +99 -17
  8. package/dist/api/automation-router.js +22 -14
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1433 -37
  11. package/dist/api/chat-router.js +205 -90
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +6 -2
  14. package/dist/api/git-router.js +30 -30
  15. package/dist/api/git-router.js.map +1 -1
  16. package/dist/api/raw-routes.d.ts +3 -3
  17. package/dist/api/raw-routes.js +76 -62
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +5 -5
  20. package/dist/api/router.js +22 -22
  21. package/dist/api/router.js.map +1 -1
  22. package/dist/api/session-visibility.d.ts +4 -4
  23. package/dist/api/session-visibility.js +26 -16
  24. package/dist/api/session-visibility.js.map +1 -1
  25. package/dist/api/sessions-router.d.ts +2635 -151
  26. package/dist/api/sessions-router.js +193 -177
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +450 -11
  29. package/dist/api/settings-router.js +102 -49
  30. package/dist/api/settings-router.js.map +1 -1
  31. package/dist/api/sse-routes.d.ts +2 -2
  32. package/dist/api/sse-routes.js +102 -80
  33. package/dist/api/sse-routes.js.map +1 -1
  34. package/dist/api/tools-raw.d.ts +5 -0
  35. package/dist/api/tools-raw.js +58 -33
  36. package/dist/api/tools-raw.js.map +1 -1
  37. package/dist/api/tools-router.d.ts +963 -109
  38. package/dist/api/tools-router.js +426 -72
  39. package/dist/api/tools-router.js.map +1 -1
  40. package/dist/api/transcribe-stream.d.ts +16 -0
  41. package/dist/api/transcribe-stream.js +172 -0
  42. package/dist/api/transcribe-stream.js.map +1 -0
  43. package/dist/app.d.ts +4 -1
  44. package/dist/app.js +80 -57
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/crypto.d.ts +3 -0
  47. package/dist/auth/crypto.js +18 -0
  48. package/dist/auth/crypto.js.map +1 -1
  49. package/dist/auth/gate.js +14 -9
  50. package/dist/auth/gate.js.map +1 -1
  51. package/dist/auth/passkeys.d.ts +15 -14
  52. package/dist/auth/passkeys.js +89 -91
  53. package/dist/auth/passkeys.js.map +1 -1
  54. package/dist/auth/store.d.ts +17 -7
  55. package/dist/auth/store.js +93 -52
  56. package/dist/auth/store.js.map +1 -1
  57. package/dist/bin/talaria-web.js +12 -81
  58. package/dist/bin/talaria-web.js.map +1 -1
  59. package/dist/cli/ctl.js +8 -2
  60. package/dist/cli/ctl.js.map +1 -1
  61. package/dist/cli/dotenv.d.ts +5 -0
  62. package/dist/cli/dotenv.js +14 -2
  63. package/dist/cli/dotenv.js.map +1 -1
  64. package/dist/cli/launcher.js +5 -5
  65. package/dist/cli/launcher.js.map +1 -1
  66. package/dist/cli/main.d.ts +1 -0
  67. package/dist/cli/main.js +90 -0
  68. package/dist/cli/main.js.map +1 -0
  69. package/dist/cli/supervise.d.ts +24 -0
  70. package/dist/cli/supervise.js +147 -6
  71. package/dist/cli/supervise.js.map +1 -1
  72. package/dist/config/agent-config.d.ts +37 -11
  73. package/dist/config/agent-config.js +146 -64
  74. package/dist/config/agent-config.js.map +1 -1
  75. package/dist/config.d.ts +2 -0
  76. package/dist/config.js +14 -5
  77. package/dist/config.js.map +1 -1
  78. package/dist/fs/atomic.d.ts +4 -0
  79. package/dist/fs/atomic.js +19 -2
  80. package/dist/fs/atomic.js.map +1 -1
  81. package/dist/fs/stat.d.ts +7 -0
  82. package/dist/fs/stat.js +27 -0
  83. package/dist/fs/stat.js.map +1 -0
  84. package/dist/http/context.d.ts +28 -9
  85. package/dist/http/context.js +77 -25
  86. package/dist/http/context.js.map +1 -1
  87. package/dist/onboarding.d.ts +2 -2
  88. package/dist/onboarding.js +10 -8
  89. package/dist/onboarding.js.map +1 -1
  90. package/dist/profiles/profiles.d.ts +12 -4
  91. package/dist/profiles/profiles.js +78 -30
  92. package/dist/profiles/profiles.js.map +1 -1
  93. package/dist/projects.d.ts +15 -6
  94. package/dist/projects.js +55 -47
  95. package/dist/projects.js.map +1 -1
  96. package/dist/providers/catalog.d.ts +85 -14
  97. package/dist/providers/catalog.js +502 -146
  98. package/dist/providers/catalog.js.map +1 -1
  99. package/dist/providers/env-file.d.ts +10 -3
  100. package/dist/providers/env-file.js +24 -75
  101. package/dist/providers/env-file.js.map +1 -1
  102. package/dist/providers/runtime-env.d.ts +4 -2
  103. package/dist/providers/runtime-env.js +9 -6
  104. package/dist/providers/runtime-env.js.map +1 -1
  105. package/dist/providers/tables.d.ts +3 -1
  106. package/dist/providers/tables.js +4 -2
  107. package/dist/providers/tables.js.map +1 -1
  108. package/dist/redact.js +56 -23
  109. package/dist/redact.js.map +1 -1
  110. package/dist/release.d.ts +7 -0
  111. package/dist/release.js +19 -3
  112. package/dist/release.js.map +1 -1
  113. package/dist/runtime.d.ts +5 -1
  114. package/dist/runtime.js +303 -96
  115. package/dist/runtime.js.map +1 -1
  116. package/dist/server.js +31 -18
  117. package/dist/server.js.map +1 -1
  118. package/dist/sessions/anchor.d.ts +11 -4
  119. package/dist/sessions/anchor.js +33 -37
  120. package/dist/sessions/anchor.js.map +1 -1
  121. package/dist/sessions/background-events.d.ts +2 -0
  122. package/dist/sessions/background-events.js +10 -0
  123. package/dist/sessions/background-events.js.map +1 -0
  124. package/dist/sessions/background-tasks.d.ts +49 -23
  125. package/dist/sessions/background-tasks.js +161 -89
  126. package/dist/sessions/background-tasks.js.map +1 -1
  127. package/dist/sessions/background-updates.d.ts +15 -8
  128. package/dist/sessions/background-updates.js +101 -25
  129. package/dist/sessions/background-updates.js.map +1 -1
  130. package/dist/sessions/cli-sessions.d.ts +23 -6
  131. package/dist/sessions/cli-sessions.js +84 -84
  132. package/dist/sessions/cli-sessions.js.map +1 -1
  133. package/dist/sessions/completions.d.ts +38 -2
  134. package/dist/sessions/completions.js +348 -77
  135. package/dist/sessions/completions.js.map +1 -1
  136. package/dist/sessions/compress.d.ts +8 -0
  137. package/dist/sessions/compress.js +2 -2
  138. package/dist/sessions/compress.js.map +1 -1
  139. package/dist/sessions/drafts.d.ts +14 -10
  140. package/dist/sessions/drafts.js +38 -43
  141. package/dist/sessions/drafts.js.map +1 -1
  142. package/dist/sessions/export.d.ts +2 -0
  143. package/dist/sessions/export.js +6 -2
  144. package/dist/sessions/export.js.map +1 -1
  145. package/dist/sessions/gateway-sessions.d.ts +86 -0
  146. package/dist/sessions/gateway-sessions.js +914 -0
  147. package/dist/sessions/gateway-sessions.js.map +1 -0
  148. package/dist/sessions/gateway-watcher.d.ts +10 -11
  149. package/dist/sessions/gateway-watcher.js +25 -21
  150. package/dist/sessions/gateway-watcher.js.map +1 -1
  151. package/dist/sessions/goals.d.ts +31 -0
  152. package/dist/sessions/goals.js +75 -0
  153. package/dist/sessions/goals.js.map +1 -0
  154. package/dist/sessions/journal.d.ts +5 -79
  155. package/dist/sessions/journal.js +15 -606
  156. package/dist/sessions/journal.js.map +1 -1
  157. package/dist/sessions/list.d.ts +34 -18
  158. package/dist/sessions/list.js +161 -147
  159. package/dist/sessions/list.js.map +1 -1
  160. package/dist/sessions/merge.d.ts +77 -13
  161. package/dist/sessions/merge.js +315 -65
  162. package/dist/sessions/merge.js.map +1 -1
  163. package/dist/sessions/notices.d.ts +22 -0
  164. package/dist/sessions/notices.js +72 -0
  165. package/dist/sessions/notices.js.map +1 -0
  166. package/dist/sessions/pending.d.ts +10 -4
  167. package/dist/sessions/pending.js +83 -7
  168. package/dist/sessions/pending.js.map +1 -1
  169. package/dist/sessions/prefix-index.d.ts +85 -0
  170. package/dist/sessions/prefix-index.js +243 -0
  171. package/dist/sessions/prefix-index.js.map +1 -0
  172. package/dist/sessions/recovery.d.ts +4 -7
  173. package/dist/sessions/recovery.js +100 -283
  174. package/dist/sessions/recovery.js.map +1 -1
  175. package/dist/sessions/relay.d.ts +20 -20
  176. package/dist/sessions/relay.js +205 -127
  177. package/dist/sessions/relay.js.map +1 -1
  178. package/dist/sessions/service.d.ts +251 -78
  179. package/dist/sessions/service.js +1510 -564
  180. package/dist/sessions/service.js.map +1 -1
  181. package/dist/sessions/session.d.ts +20 -2
  182. package/dist/sessions/session.js +33 -5
  183. package/dist/sessions/session.js.map +1 -1
  184. package/dist/sessions/shares.d.ts +7 -6
  185. package/dist/sessions/shares.js +22 -45
  186. package/dist/sessions/shares.js.map +1 -1
  187. package/dist/sessions/source-kind.js +1 -0
  188. package/dist/sessions/source-kind.js.map +1 -1
  189. package/dist/sessions/state-db-identity.d.ts +18 -0
  190. package/dist/sessions/state-db-identity.js +67 -0
  191. package/dist/sessions/state-db-identity.js.map +1 -0
  192. package/dist/sessions/state-db-worker.js +24 -5
  193. package/dist/sessions/state-db-worker.js.map +1 -1
  194. package/dist/sessions/state-db.d.ts +157 -21
  195. package/dist/sessions/state-db.js +1145 -204
  196. package/dist/sessions/state-db.js.map +1 -1
  197. package/dist/sessions/state-saved.d.ts +1 -1
  198. package/dist/sessions/state-saved.js +35 -11
  199. package/dist/sessions/state-saved.js.map +1 -1
  200. package/dist/sessions/storage/backend.d.ts +171 -0
  201. package/dist/sessions/storage/backend.js +19 -0
  202. package/dist/sessions/storage/backend.js.map +1 -0
  203. package/dist/sessions/storage/conformance.d.ts +20 -0
  204. package/dist/sessions/storage/conformance.js +237 -0
  205. package/dist/sessions/storage/conformance.js.map +1 -0
  206. package/dist/sessions/storage/document.d.ts +16 -0
  207. package/dist/sessions/storage/document.js +44 -0
  208. package/dist/sessions/storage/document.js.map +1 -0
  209. package/dist/sessions/storage/json-documents.d.ts +25 -0
  210. package/dist/sessions/storage/json-documents.js +277 -0
  211. package/dist/sessions/storage/json-documents.js.map +1 -0
  212. package/dist/sessions/storage/json-files.d.ts +23 -0
  213. package/dist/sessions/storage/json-files.js +244 -0
  214. package/dist/sessions/storage/json-files.js.map +1 -0
  215. package/dist/sessions/storage/json-installation.d.ts +3 -0
  216. package/dist/sessions/storage/json-installation.js +31 -0
  217. package/dist/sessions/storage/json-installation.js.map +1 -0
  218. package/dist/sessions/storage/json-journal.d.ts +113 -0
  219. package/dist/sessions/storage/json-journal.js +756 -0
  220. package/dist/sessions/storage/json-journal.js.map +1 -0
  221. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  222. package/dist/sessions/storage/json-recovery.js +195 -0
  223. package/dist/sessions/storage/json-recovery.js.map +1 -0
  224. package/dist/sessions/storage/json.d.ts +82 -0
  225. package/dist/sessions/storage/json.js +350 -0
  226. package/dist/sessions/storage/json.js.map +1 -0
  227. package/dist/sessions/storage/row.d.ts +6 -0
  228. package/dist/sessions/storage/row.js +49 -0
  229. package/dist/sessions/storage/row.js.map +1 -0
  230. package/dist/sessions/storage/search-text.d.ts +4 -0
  231. package/dist/sessions/storage/search-text.js +16 -0
  232. package/dist/sessions/storage/search-text.js.map +1 -0
  233. package/dist/sessions/store.d.ts +67 -73
  234. package/dist/sessions/store.js +341 -607
  235. package/dist/sessions/store.js.map +1 -1
  236. package/dist/sessions/streams.d.ts +12 -7
  237. package/dist/sessions/streams.js +58 -15
  238. package/dist/sessions/streams.js.map +1 -1
  239. package/dist/sessions/titles.js +1 -1
  240. package/dist/sessions/titles.js.map +1 -1
  241. package/dist/sessions/todo.d.ts +5 -0
  242. package/dist/sessions/todo.js +28 -21
  243. package/dist/sessions/todo.js.map +1 -1
  244. package/dist/sessions/tool-display.d.ts +8 -5
  245. package/dist/sessions/tool-display.js +28 -16
  246. package/dist/sessions/tool-display.js.map +1 -1
  247. package/dist/sessions/turn-context.d.ts +10 -1
  248. package/dist/sessions/turn-context.js +13 -6
  249. package/dist/sessions/turn-context.js.map +1 -1
  250. package/dist/sessions/turn.d.ts +110 -22
  251. package/dist/sessions/turn.js +918 -380
  252. package/dist/sessions/turn.js.map +1 -1
  253. package/dist/sessions/wakeup-pause.d.ts +8 -13
  254. package/dist/sessions/wakeup-pause.js +31 -49
  255. package/dist/sessions/wakeup-pause.js.map +1 -1
  256. package/dist/sessions/window.js +13 -12
  257. package/dist/sessions/window.js.map +1 -1
  258. package/dist/settings.d.ts +19 -7
  259. package/dist/settings.js +179 -183
  260. package/dist/settings.js.map +1 -1
  261. package/dist/sidecar/client.d.ts +41 -4
  262. package/dist/sidecar/client.js +192 -42
  263. package/dist/sidecar/client.js.map +1 -1
  264. package/dist/sidecar/discover.d.ts +2 -1
  265. package/dist/sidecar/discover.js +93 -10
  266. package/dist/sidecar/discover.js.map +1 -1
  267. package/dist/sidecar/env.d.ts +4 -0
  268. package/dist/sidecar/env.js +3 -0
  269. package/dist/sidecar/env.js.map +1 -0
  270. package/dist/sidecar/fake.d.ts +6 -2
  271. package/dist/sidecar/fake.js +34 -4
  272. package/dist/sidecar/fake.js.map +1 -1
  273. package/dist/spa.d.ts +9 -4
  274. package/dist/spa.js +36 -12
  275. package/dist/spa.js.map +1 -1
  276. package/dist/startup-timing.d.ts +66 -0
  277. package/dist/startup-timing.js +129 -0
  278. package/dist/startup-timing.js.map +1 -0
  279. package/dist/test/env-file.d.ts +5 -0
  280. package/dist/test/env-file.js +88 -0
  281. package/dist/test/env-file.js.map +1 -0
  282. package/dist/test/fs-handles.d.ts +11 -0
  283. package/dist/test/fs-handles.js +31 -0
  284. package/dist/test/fs-handles.js.map +1 -0
  285. package/dist/test/harness.d.ts +1 -0
  286. package/dist/test/harness.js +1 -1
  287. package/dist/test/harness.js.map +1 -1
  288. package/dist/test/passkeys.d.ts +22 -0
  289. package/dist/test/passkeys.js +62 -0
  290. package/dist/test/passkeys.js.map +1 -0
  291. package/dist/test/storage.d.ts +15 -0
  292. package/dist/test/storage.js +26 -0
  293. package/dist/test/storage.js.map +1 -0
  294. package/dist/tools/commands.d.ts +6 -1
  295. package/dist/tools/commands.js +8 -2
  296. package/dist/tools/commands.js.map +1 -1
  297. package/dist/tools/connectors.d.ts +33 -0
  298. package/dist/tools/connectors.js +121 -0
  299. package/dist/tools/connectors.js.map +1 -0
  300. package/dist/tools/crons.d.ts +46 -5
  301. package/dist/tools/crons.js +100 -26
  302. package/dist/tools/crons.js.map +1 -1
  303. package/dist/tools/dashboard-plugins.d.ts +34 -5
  304. package/dist/tools/dashboard-plugins.js +146 -14
  305. package/dist/tools/dashboard-plugins.js.map +1 -1
  306. package/dist/tools/extensions.d.ts +5 -4
  307. package/dist/tools/extensions.js +213 -207
  308. package/dist/tools/extensions.js.map +1 -1
  309. package/dist/tools/gateway-restart.d.ts +82 -0
  310. package/dist/tools/gateway-restart.js +222 -0
  311. package/dist/tools/gateway-restart.js.map +1 -0
  312. package/dist/tools/health.d.ts +28 -4
  313. package/dist/tools/health.js +171 -50
  314. package/dist/tools/health.js.map +1 -1
  315. package/dist/tools/hygiene.d.ts +4 -3
  316. package/dist/tools/hygiene.js +23 -9
  317. package/dist/tools/hygiene.js.map +1 -1
  318. package/dist/tools/insights.d.ts +1 -1
  319. package/dist/tools/insights.js +2 -2
  320. package/dist/tools/insights.js.map +1 -1
  321. package/dist/tools/kanban.d.ts +4 -2
  322. package/dist/tools/kanban.js +28 -19
  323. package/dist/tools/kanban.js.map +1 -1
  324. package/dist/tools/mcp-health.js +74 -24
  325. package/dist/tools/mcp-health.js.map +1 -1
  326. package/dist/tools/mcp.js +19 -1
  327. package/dist/tools/mcp.js.map +1 -1
  328. package/dist/tools/memory-masks.d.ts +5 -0
  329. package/dist/tools/memory-masks.js +256 -0
  330. package/dist/tools/memory-masks.js.map +1 -0
  331. package/dist/tools/memory.d.ts +29 -3
  332. package/dist/tools/memory.js +69 -27
  333. package/dist/tools/memory.js.map +1 -1
  334. package/dist/tools/plugin-oauth.d.ts +14 -0
  335. package/dist/tools/plugin-oauth.js +53 -0
  336. package/dist/tools/plugin-oauth.js.map +1 -0
  337. package/dist/tools/plugin-settings.d.ts +15 -0
  338. package/dist/tools/plugin-settings.js +90 -0
  339. package/dist/tools/plugin-settings.js.map +1 -0
  340. package/dist/tools/prune-advice.d.ts +50 -0
  341. package/dist/tools/prune-advice.js +114 -0
  342. package/dist/tools/prune-advice.js.map +1 -0
  343. package/dist/tools/skills.d.ts +13 -5
  344. package/dist/tools/skills.js +53 -19
  345. package/dist/tools/skills.js.map +1 -1
  346. package/dist/tools/update-notifications.d.ts +102 -17
  347. package/dist/tools/update-notifications.js +400 -82
  348. package/dist/tools/update-notifications.js.map +1 -1
  349. package/dist/tools/update-rollback.d.ts +11 -0
  350. package/dist/tools/update-rollback.js +28 -0
  351. package/dist/tools/update-rollback.js.map +1 -0
  352. package/dist/tools/updates.d.ts +100 -33
  353. package/dist/tools/updates.js +1085 -345
  354. package/dist/tools/updates.js.map +1 -1
  355. package/dist/util.d.ts +2 -0
  356. package/dist/util.js +5 -0
  357. package/dist/util.js.map +1 -1
  358. package/dist/workspace/git.d.ts +16 -1
  359. package/dist/workspace/git.js +291 -11
  360. package/dist/workspace/git.js.map +1 -1
  361. package/dist/workspace/media.d.ts +12 -9
  362. package/dist/workspace/media.js +98 -38
  363. package/dist/workspace/media.js.map +1 -1
  364. package/dist/workspace/preview.js +4 -14
  365. package/dist/workspace/preview.js.map +1 -1
  366. package/dist/workspace/rollback.d.ts +8 -4
  367. package/dist/workspace/rollback.js +22 -12
  368. package/dist/workspace/rollback.js.map +1 -1
  369. package/dist/workspace/workspaces.d.ts +15 -19
  370. package/dist/workspace/workspaces.js +55 -127
  371. package/dist/workspace/workspaces.js.map +1 -1
  372. package/dist/workspace/worktrees.js +170 -32
  373. package/dist/workspace/worktrees.js.map +1 -1
  374. package/package.json +6 -4
  375. package/sidecar/agent_dependency.json +4 -4
  376. package/sidecar/agent_support.json +29 -0
  377. package/sidecar/scripts/record-fixtures.py +12 -3
  378. package/sidecar/scripts/replay_sidecar.py +8 -0
  379. package/sidecar/scripts/suite_runner.py +458 -0
  380. package/sidecar/scripts/test.sh +11 -5
  381. package/sidecar/talaria_sidecar/__init__.py +6 -1
  382. package/sidecar/talaria_sidecar/__main__.py +13 -4
  383. package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
  384. package/sidecar/talaria_sidecar/agent_services.py +1116 -0
  385. package/sidecar/talaria_sidecar/context_detail.py +59 -0
  386. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  387. package/sidecar/talaria_sidecar/home.py +133 -14
  388. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  389. package/sidecar/talaria_sidecar/methods/aux.py +211 -41
  390. package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
  391. package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
  392. package/sidecar/talaria_sidecar/methods/commands.py +483 -13
  393. package/sidecar/talaria_sidecar/methods/config.py +195 -6
  394. package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
  395. package/sidecar/talaria_sidecar/methods/cron.py +374 -36
  396. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  397. package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
  398. package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
  399. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
  400. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  401. package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
  402. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  403. package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
  404. package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
  405. package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
  406. package/sidecar/talaria_sidecar/methods/process.py +229 -16
  407. package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
  408. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  409. package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
  410. package/sidecar/talaria_sidecar/methods/skills.py +83 -5
  411. package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
  412. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  413. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  414. package/sidecar/talaria_sidecar/methods/toolsets.py +51 -0
  415. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  416. package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
  417. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  418. package/sidecar/talaria_sidecar/rpc.py +70 -7
  419. package/sidecar/talaria_sidecar/runtime.py +122 -14
  420. package/sidecar/talaria_sidecar/speed.py +47 -0
  421. package/sidecar/talaria_sidecar/startup_timing.py +119 -0
  422. package/static/dist/FILES.txt +242 -215
  423. package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
  424. package/static/dist/assets/AppShell-3isgvoro.js +8 -0
  425. package/static/dist/assets/Button-f4n6dapf.js +1 -0
  426. package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
  427. package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
  428. package/static/dist/assets/Dialog-hta5utr3.js +1 -0
  429. package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
  430. package/static/dist/assets/Field-b6ez48oz.js +1 -0
  431. package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
  432. package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
  433. package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
  434. package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
  435. package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
  436. package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
  437. package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
  438. package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
  439. package/static/dist/assets/Select-kr07928w.js +1 -0
  440. package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
  441. package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
  442. package/static/dist/assets/States-icab19yg.js +1 -0
  443. package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
  444. package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
  445. package/static/dist/assets/_app-ga1moszl.js +1 -0
  446. package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
  447. package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
  448. package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
  449. package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
  450. package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
  451. package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
  452. package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
  453. package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
  454. package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
  455. package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
  456. package/static/dist/assets/_app.skills-n016rihd.js +1 -0
  457. package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
  458. package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
  459. package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
  460. package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
  461. package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
  462. package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
  463. package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
  464. package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
  465. package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
  466. package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
  467. package/static/dist/assets/archive-il7c6053.js +1 -0
  468. package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
  469. package/static/dist/assets/back-gu3nu279.js +1 -0
  470. package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
  471. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
  472. package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
  473. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
  474. package/static/dist/assets/cancel-bsnfo36u.js +1 -0
  475. package/static/dist/assets/channel-dsbtk01y.js +1 -0
  476. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
  477. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
  478. package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
  479. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
  480. package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
  481. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
  482. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
  483. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
  484. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
  485. package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
  486. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
  487. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
  488. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
  489. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
  490. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
  491. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
  492. package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
  493. package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
  494. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
  495. package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
  496. package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
  497. package/static/dist/assets/copied-bz3gkazh.js +1 -0
  498. package/static/dist/assets/copy-jo006coq.js +1 -0
  499. package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
  500. package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
  501. package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
  502. package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
  503. package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
  504. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
  505. package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
  506. package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
  507. package/static/dist/assets/dagre-o5jmjz46.js +1 -0
  508. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
  509. package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
  510. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
  511. package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
  512. package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
  513. package/static/dist/assets/dist-crc46di9.js +7 -0
  514. package/static/dist/assets/dist-etpoyscq.js +44 -0
  515. package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
  516. package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
  517. package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
  518. package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
  519. package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
  520. package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
  521. package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
  522. package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
  523. package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
  524. package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
  525. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
  526. package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
  527. package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
  528. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
  529. package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
  530. package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
  531. package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
  532. package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
  533. package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
  534. package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
  535. package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
  536. package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
  537. package/static/dist/assets/host-fr6lni1p.js +1 -0
  538. package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
  539. package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
  540. package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
  541. package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
  542. package/static/dist/assets/index-73649w7o.js +10 -0
  543. package/static/dist/assets/index-lhf1fsdf.css +2 -0
  544. package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
  545. package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
  546. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
  547. package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
  548. package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
  549. package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
  550. package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
  551. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
  552. package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
  553. package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
  554. package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
  555. package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
  556. package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
  557. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
  558. package/static/dist/assets/katex-cbdd1ohw.css +1 -0
  559. package/static/dist/assets/katex-pbij2oo6.js +257 -0
  560. package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
  561. package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
  562. package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
  563. package/static/dist/assets/linear-jlwrvvy6.js +1 -0
  564. package/static/dist/assets/link-iadw4ghw.js +2 -0
  565. package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
  566. package/static/dist/assets/locale-cs-n41hptse.js +5 -0
  567. package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
  568. package/static/dist/assets/locale-es-ih078lco.js +5 -0
  569. package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
  570. package/static/dist/assets/locale-it-kb96jufy.js +5 -0
  571. package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
  572. package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
  573. package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
  574. package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
  575. package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
  576. package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
  577. package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
  578. package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
  579. package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
  580. package/static/dist/assets/login-lkhtzfcl.js +1 -0
  581. package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
  582. package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
  583. package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
  584. package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
  585. package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
  586. package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
  587. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
  588. package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
  589. package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
  590. package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
  591. package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
  592. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  593. package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
  594. package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
  595. package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
  596. package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
  597. package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
  598. package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
  599. package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
  600. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
  601. package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
  602. package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
  603. package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
  604. package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
  605. package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
  606. package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
  607. package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
  608. package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
  609. package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
  610. package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
  611. package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
  612. package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
  613. package/static/dist/assets/registry-g7w3353u.js +1 -0
  614. package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
  615. package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
  616. package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
  617. package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
  618. package/static/dist/assets/runtime-nlxpnegr.js +1 -0
  619. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
  620. package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
  621. package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
  622. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
  623. package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
  624. package/static/dist/assets/share._token-o76ki0ly.js +2 -0
  625. package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
  626. package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
  627. package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
  628. package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
  629. package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
  630. package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
  631. package/static/dist/assets/src-h89s0hs4.js +1 -0
  632. package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
  633. package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
  634. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
  635. package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
  636. package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
  637. package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
  638. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
  639. package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
  640. package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
  641. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
  642. package/static/dist/assets/toast-liparz2j.js +56 -0
  643. package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
  644. package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
  645. package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
  646. package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
  647. package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
  648. package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
  649. package/static/dist/assets/untitled-mj86r4gz.js +1 -0
  650. package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
  651. package/static/dist/assets/useForm-o4uu74mx.js +1 -0
  652. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
  653. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
  654. package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
  655. package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
  656. package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
  657. package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
  658. package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
  659. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
  660. package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
  661. package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
  662. package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
  663. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
  664. package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
  665. package/static/dist/extension-sdk.js +1 -1
  666. package/static/dist/index.html +1 -1
  667. package/static/dist/sw.js +180 -136
  668. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  669. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  670. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  671. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  672. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  673. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  674. package/static/dist/assets/Markdown-lwviulmg.css +0 -1
  675. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  676. package/static/dist/assets/Select-kddq75qc.js +0 -1
  677. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  678. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  679. package/static/dist/assets/States-eq6fe29m.js +0 -1
  680. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  681. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  682. package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +0 -1
  683. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  684. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  685. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  686. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  687. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  688. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  689. package/static/dist/assets/_app.session._sessionId-fn1bhde6.js +0 -1
  690. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  691. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  692. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  693. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  694. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  695. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  696. package/static/dist/assets/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
  697. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  698. package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
  699. package/static/dist/assets/back-h7aun7ew.js +0 -1
  700. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  701. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  702. package/static/dist/assets/chunk-DU6HZSFF-ofajdixh.js +0 -128
  703. package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
  704. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  705. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  706. package/static/dist/assets/copied-hcplv46p.js +0 -1
  707. package/static/dist/assets/copy-magtofyp.js +0 -1
  708. package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
  709. package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
  710. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
  711. package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
  712. package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
  713. package/static/dist/assets/dagre-lkoknbjn.js +0 -1
  714. package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
  715. package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
  716. package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
  717. package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
  718. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  719. package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
  720. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  721. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  722. package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
  723. package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.js +0 -1
  724. package/static/dist/assets/host-i828xuo7.js +0 -1
  725. package/static/dist/assets/index-eh8zilnv.css +0 -2
  726. package/static/dist/assets/index-r648f4o1.js +0 -11
  727. package/static/dist/assets/info-A6RAGUB7-h11x0dxo.js +0 -1
  728. package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
  729. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  730. package/static/dist/assets/linear-becwlnhk.js +0 -1
  731. package/static/dist/assets/link-oot2yn73.js +0 -1
  732. package/static/dist/assets/login-gu28l9ou.js +0 -1
  733. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  734. package/static/dist/assets/mermaid-parser.core-ntne45i9.js +0 -7
  735. package/static/dist/assets/not-found-ccnnac39.js +0 -1
  736. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  737. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  738. package/static/dist/assets/packet-AYTQ26CC-l6jdka8p.js +0 -1
  739. package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
  740. package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
  741. package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
  742. package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
  743. package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.js +0 -1
  744. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  745. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  746. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  747. package/static/dist/assets/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
  748. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  749. package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
  750. package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
  751. package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
  752. package/static/dist/assets/root-mbklpuv4.js +0 -1
  753. package/static/dist/assets/saved-girnabkf.js +0 -1
  754. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  755. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  756. package/static/dist/assets/sizeCapture-INFHLROL-g612l48j.js +0 -1
  757. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  758. package/static/dist/assets/src-hjyib4mu.js +0 -1
  759. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  760. package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
  761. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  762. package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.js +0 -1
  763. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  764. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  765. package/static/dist/assets/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
  766. package/static/dist/assets/treemap-WGGIJYW6-nl85071a.js +0 -1
  767. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  768. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  769. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  770. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
  771. package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
  772. package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
  773. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
  774. /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
@@ -5,40 +5,45 @@
5
5
  * (Python `api/routes.py::_start_chat_stream_for_session`,
6
6
  * `api/streaming.py::_run_agent_streaming`, `cancel_stream`).
7
7
  */
8
+ import { StartupTiming, timingOutcome } from '../startup-timing.js';
8
9
  import { randomUUID } from 'node:crypto';
9
- import { closeSync, fstatSync, readFileSync, rmSync } from 'node:fs';
10
+ import { closeSync, fstatSync, readFileSync } from 'node:fs';
10
11
  import { openAnchoredFd } from '../workspace/fs.js';
11
12
  import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
12
- import { SidecarError } from '../sidecar/client.js';
13
+ import { runtimeFailureDetails, SidecarError } from '../sidecar/client.js';
13
14
  import { HttpFailure, markSessionTitleGenerated } from './service.js';
14
15
  import { StreamRegistry, SessionChannels } from './streams.js';
15
- import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
16
- import { PendingPrompts, clarifyReply } from './pending.js';
17
- import { RunJournal } from './journal.js';
18
- import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, titleFrom } from './session.js';
19
- import { anchorMessageKey, compressionReference, markerSummary, visibleMessagesForAnchor } from './compress.js';
20
- import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactSessionData, redactString, withToolId } from '../redact.js';
21
- import { dict } from '../config/agent-config.js';
16
+ import { TRANSCRIPT_WINDOW, ChatUsageSchema, ConnectorOperationSnapshotSchema } from '@maudecode/talaria-web-contracts';
17
+ import { toolProgress } from './tool-display.js';
18
+ import { catalogNames, connectorOperation } from '../tools/connectors.js';
19
+ import { PendingPrompts, clarifyReply, isVaultPrompt, vaultReply } from './pending.js';
20
+ import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, forgetContextFigures, titleFrom } from './session.js';
21
+ import { anchorMessageKey, markerSummary, visibleMessagesForAnchor } from './compress.js';
22
+ import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactString, withToolId } from '../redact.js';
23
+ import { dict, findPersonality } from '../config/agent-config.js';
22
24
  import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
23
- import { messageWindowForDisplay, messagesForLimitedPayload, toolCallsForMessageWindow } from './window.js';
24
- import { UNSETTLED_TODO_KEY, attachTodoState, parseTodoToolResult } from './todo.js';
25
- import { withSessionWireFlags } from './list.js';
26
- import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
25
+ import { UNSETTLED_TODO_KEY, parseTodoToolResult } from './todo.js';
26
+ import { SessionNotices } from './notices.js';
27
+ import { SessionGoals } from './goals.js';
28
+ import { turnTerminalState } from './anchor.js';
27
29
  import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
28
- import { maxIterationsFromConfig, maxTokensFromConfig, processWakeupMaxIterations, reasoningConfigFromConfig, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
29
- import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
30
- import { mayBecomeSilentReply, turnOrigin, withBackgroundUpdates } from './background-updates.js';
30
+ import { agentBuildParams, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
31
+ import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, withDataUseRequirements, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
32
+ import { mayBecomeSilentReply, turnOrigin, recordBackgroundReview, recordBackgroundUpdate } from './background-updates.js';
31
33
  import { countExchanges, fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
32
34
  import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
33
35
  import { CHECKPOINT_STORE_BUSY_MESSAGE } from '../workspace/rollback.js';
34
36
  import { str } from '../util.js';
35
37
  import { toolEditDiff } from './tool-display.js';
38
+ import { startsBackgroundWork } from './background-tasks.js';
36
39
  import { recordWakeupPause } from './wakeup-pause.js';
40
+ import { displayName } from '../providers/catalog.js';
37
41
  export const CHAT_LOCK_WAIT_SECONDS = 2;
38
42
  const IMAGE_MODE_TIMEOUT_MS = 15_000;
39
- const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
40
43
  /** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
41
44
  const METERING_INTERVAL_S = 1;
45
+ /** The Agent's content frames, which end a rate-limit wait (TAL-821) and a compression span (TAL-1006). A terminal frame ends the run, its wait, and its span. */
46
+ const CONTENT_EVENTS = new Set(['token', 'reasoning', 'interim_assistant', 'tool', 'tool_complete']);
42
47
  /** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
43
48
  const BACKGROUND_UNWIND_WAIT_MS = 30_000;
44
49
  const COMPRESSION_EXHAUSTED = { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
@@ -65,6 +70,9 @@ export function classifyProviderError(errStr, opts = {}) {
65
70
  const lower = errStr.toLowerCase();
66
71
  if (opts.compressionExhausted)
67
72
  return COMPRESSION_EXHAUSTED;
73
+ // TAL-798: the Agent's local llama.cpp provider has no key to add; its server is off or still starting.
74
+ if (opts.condition === 'local_runtime_unavailable')
75
+ return { label: 'Local model server unavailable', type: 'local_runtime_unavailable', hint: 'The local model server is off or still starting. Turn it on in Hermes Agent, or pick another model.' };
68
76
  if (opts.condition === 'credential_missing')
69
77
  return { label: 'Authentication failed', type: 'auth_mismatch', hint: 'The selected model may not be supported by your configured provider or your API key is invalid. Run `hermes model` in your terminal to update credentials, then restart the WebUI.' };
70
78
  const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
@@ -209,6 +217,10 @@ export class TurnRunner {
209
217
  liveUsage = new Map();
210
218
  /** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
211
219
  liveTodos = new Map();
220
+ /** The current presentation, scoped to its owning stream and released on every terminal path. */
221
+ runStatuses = new Map();
222
+ /** TAL-1006: each live stream's open compression span, by its start (epoch seconds). */
223
+ compressionSpans = new Map();
212
224
  /** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
213
225
  settledStreams = new Set();
214
226
  /** The last adaptive title refresh per session; a successor turn's refresh runs after it (TAL-589). */
@@ -216,20 +228,147 @@ export class TurnRunner {
216
228
  /**
217
229
  * TAL-364: what a Stop needs to write the turn's model context: its starting context, the prompt the Agent got, and
218
230
  * whether a settlement already used a canonical checkpoint (which marks the stop boundary; later work never replaces it).
231
+ * TAL-1162: `startReadPending` while the turn's starting state.db read is still in flight.
219
232
  */
220
233
  /** TAL-364: a Stop's in-flight `chat.interrupt` reply (null when it failed), so a worker that settles first can use its checkpoint. */
221
234
  interrupts = new Map();
222
235
  stopContexts = new Map();
223
236
  /** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
224
237
  workers = new Map();
238
+ /** TAL-899: sessions whose user pressed Stop; no automatic turn starts in them until the user's next turn is admitted. */
239
+ stopHeld = new Set();
240
+ notices = new SessionNotices(() => this.deps.now());
241
+ goals = new SessionGoals(() => this.deps.now());
225
242
  constructor(deps) {
226
243
  this.deps = deps;
227
244
  }
228
245
  get registry() { return this.deps.registry; }
229
246
  // ── admission ────────────────────────────────────────────────────────────
247
+ /** TAL-899: whether a Stop holds the session's automatic turns (session `automatic_turns_held`). */
248
+ async automaticTurnsHeld(sessionId) {
249
+ if (this.stopHeld.has(sessionId))
250
+ return true;
251
+ try {
252
+ return (await this.deps.store.get(sessionId, { metadataOnly: true })).automatic_turns_stop_held;
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
258
+ /** TAL-803: the Agent's active notices for the session (session `notices`). */
259
+ sessionNotices(sessionId) {
260
+ const redact = this.deps.redactEnabled();
261
+ return this.notices.list(sessionId).map((n) => ({ ...n, text: redactString(n.text, redact) }));
262
+ }
263
+ /** TAL-808: the session's standing goal (session `goal`). */
264
+ sessionGoal(sessionId) {
265
+ const goal = this.goals.view(sessionId);
266
+ if (!goal)
267
+ return null;
268
+ const redact = this.deps.redactEnabled();
269
+ return { ...goal, text: redactString(goal.text, redact), reason: goal.reason === null ? null : redactString(goal.reason, redact) };
270
+ }
271
+ /**
272
+ * TAL-808: reads the session's goal from the Agent and, when its view changed, sends `session_goal` on `put` (else the
273
+ * session's live stream) and `sessions_changed`, which reloads the session for a client between turns.
274
+ */
275
+ async refreshGoal(s, put = async (event, data) => { (await this.emitToSession(s.session_id, event, data)); }) {
276
+ if (!(await this.readGoal(s)))
277
+ return;
278
+ await put('session_goal', { session_id: s.session_id, goal: this.sessionGoal(s.session_id) });
279
+ this.deps.events.publish('goal', { profile: s.profile, sessionId: s.session_id });
280
+ }
281
+ /** TAL-808: a session's first detail after a restart reads its goal before answering, so it tells no one else. */
282
+ async loadGoal(sessionId) {
283
+ if (this.goals.has(sessionId))
284
+ return;
285
+ let s;
286
+ try {
287
+ s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
288
+ }
289
+ catch {
290
+ return;
291
+ }
292
+ await this.readGoal(s);
293
+ }
294
+ /** The Agent's `goals.get` into the cache, and a re-read when a timed park ends; whether the view changed. A failed read keeps the last one. */
295
+ async readGoal(s) {
296
+ const sidecar = this.deps.sidecar();
297
+ if (!sidecar)
298
+ return false;
299
+ let status;
300
+ try {
301
+ // Short: a session's first load waits on it.
302
+ status = await sidecar.call('goals.get', { session_id: s.session_id, profile_home: this.deps.profileHome(s.profile) }, { timeoutMs: 3_000 });
303
+ }
304
+ catch (error) {
305
+ this.deps.log(`[webui] WARNING: goal read failed for ${s.session_id}: ${error.message}`);
306
+ return false;
307
+ }
308
+ const changed = this.goals.set(s.session_id, status);
309
+ this.goals.rereadAfterPark(s.session_id, () => { void this.refreshGoal({ session_id: s.session_id, profile: s.profile }); });
310
+ return changed;
311
+ }
312
+ /** Hygiene sweep: a deleted session's hold, notices and goal go with it (TAL-899, TAL-803, TAL-808). */
313
+ async sweep() {
314
+ const exists = async (sid) => (await this.deps.store.exists(sid));
315
+ for (const sid of this.stopHeld)
316
+ if (!(await exists(sid)))
317
+ this.stopHeld.delete(sid);
318
+ await this.notices.sweep(exists);
319
+ await this.goals.sweep(exists);
320
+ }
321
+ /**
322
+ * Admit automatic turns through the same workspace recovery and model resolution as user turns. After the user's Stop
323
+ * none starts (TAL-899): the answer is a `held` 409, and the caller keeps its work for the user's next turn.
324
+ */
325
+ async startServerTurn(session, opts) {
326
+ if ((await this.automaticTurnsHeld(session.session_id)))
327
+ return { _status: 409, error: 'automatic turns are held after Stop', held: true };
328
+ try {
329
+ const service = this.deps.service();
330
+ const workspace = (await service.turnWorkspace(session));
331
+ const [model, modelProvider, normalizedModel] = service.turnModel(session);
332
+ return (await this.admit(session, { ...opts, workspace, model, modelProvider, normalizedModel }));
333
+ }
334
+ catch (error) {
335
+ if (error instanceof HttpFailure)
336
+ return { _status: error.status, error: error.message };
337
+ throw error;
338
+ }
339
+ }
340
+ /** A user's turn; admitting it releases the hold a Stop put on automatic turns (TAL-899). */
341
+ async start(session, opts) {
342
+ const response = (await this.admit(session, opts, true));
343
+ if (response._status === undefined)
344
+ this.stopHeld.delete(session.session_id);
345
+ return response;
346
+ }
230
347
  /** Python `_start_chat_stream_for_session`: persist pending state, register the channel, launch the worker. */
231
- start(session, opts) {
348
+ async admit(session, opts, releaseStopHold = false) {
349
+ const timing = opts.timing ?? new StartupTiming(this.deps.log);
350
+ timing.identify(session.session_id);
351
+ timing.begin('admission');
352
+ try {
353
+ const response = await this.admitTimed(session, { ...opts, timing }, releaseStopHold);
354
+ if (response._status !== undefined)
355
+ timing.finish('refused');
356
+ else if (!response.stream_id || !this.workers.has(response.stream_id))
357
+ timing.finish('noop');
358
+ return response;
359
+ }
360
+ catch (error) {
361
+ timing.finish(timingOutcome(error));
362
+ throw error;
363
+ }
364
+ }
365
+ async admitTimed(session, opts, releaseStopHold) {
366
+ const timing = opts.timing;
367
+ const closeChecks = timing.begin('admission_checks');
232
368
  const s = session;
369
+ const readOnly = (await this.deps.service().readOnlyReason(s));
370
+ if (readOnly)
371
+ return { error: readOnly, _status: 403 };
233
372
  if (this.deps.updateInProgress?.())
234
373
  return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
235
374
  if (this.deps.profileDeleting?.(s.profile ?? null))
@@ -242,7 +381,7 @@ export class TurnRunner {
242
381
  if (locked) {
243
382
  if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
244
383
  return { error: 'session already has an active stream', active_stream_id: locked, _status: 409 };
245
- this.deps.service().clearStaleStreamState(s);
384
+ await this.deps.service().clearStaleStreamState(s);
246
385
  if (s.active_stream_id)
247
386
  return { error: 'session already has an active stream', active_stream_id: s.active_stream_id, _status: 409 };
248
387
  }
@@ -251,11 +390,28 @@ export class TurnRunner {
251
390
  if (blocking)
252
391
  return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
253
392
  }
393
+ const accepted = isDict(s.extra.completion_acceptances) ? s.extra.completion_acceptances : {};
394
+ if (opts.completionKeys?.length && opts.completionKeys.every((key) => typeof accepted[key] === 'string' && accepted[key])) {
395
+ timing.identify(s.session_id, str(accepted[opts.completionKeys[0]]));
396
+ timing.finish('noop');
397
+ return { stream_id: str(accepted[opts.completionKeys[0]]), session_id: s.session_id };
398
+ }
399
+ closeChecks();
254
400
  const streamId = randomUUID().replace(/-/g, '');
401
+ timing.identify(s.session_id, streamId);
255
402
  const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
256
403
  const attachments = opts.attachments ?? [];
257
404
  // A background wakeup stays one in a forked session too (TAL-460).
258
405
  const source = opts.source !== 'process_wakeup' && str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
406
+ // TAL-1005: the context figures the last turn reported hold for a turn on the same model only, and are judged before
407
+ // admission clears the post-compression estimate. A `/btw` clone is thrown away after one answer, so it is never due.
408
+ const sameRuntime = str(s.model) === str(opts.model) && str(s.model_provider) === str(opts.modelProvider);
409
+ const compactionDue = !opts.ephemeral && sameRuntime && this.deps.service().compactionDue(s);
410
+ if (!sameRuntime)
411
+ forgetContextFigures(s);
412
+ const notificationOwner = this.deps.notificationOwner();
413
+ if (notificationOwner)
414
+ s.extra.notification_owner = notificationOwner;
259
415
  s.workspace = opts.workspace;
260
416
  s.model = opts.model;
261
417
  s.model_provider = opts.modelProvider;
@@ -265,6 +421,12 @@ export class TurnRunner {
265
421
  s.pending_attachments = attachments;
266
422
  s.pending_started_at = this.deps.now();
267
423
  s.pending_user_source = source;
424
+ // TAL-927: reasoning one provider issued can fail on another (hermes-agent#122831), so a provider switch moves the
425
+ // boundary its carriers replay from.
426
+ if (s.reasoning_replay_provider !== str(opts.modelProvider)) {
427
+ s.reasoning_replay_provider = str(opts.modelProvider);
428
+ s.reasoning_replay_from = s.pending_started_at;
429
+ }
268
430
  if (['', 'Untitled', 'New Chat'].includes(str(s.title).trim())) {
269
431
  const provisional = titleFrom([{ role: 'user', content: opts.msg }], s.title || 'Untitled');
270
432
  if (provisional && !['', 'Untitled', 'New Chat'].includes(provisional.trim()))
@@ -272,21 +434,47 @@ export class TurnRunner {
272
434
  }
273
435
  if (this.deps.saveMode() === 'eager')
274
436
  this.checkpointUserMessage(s, opts.msg, attachments, s.pending_started_at, source, streamId);
275
- this.deps.store.save(s);
437
+ const previousStopHold = s.automatic_turns_stop_held;
438
+ if (releaseStopHold)
439
+ s.automatic_turns_stop_held = false;
440
+ const previousAcceptances = s.extra.completion_acceptances;
441
+ if (opts.completionKeys?.length)
442
+ s.extra.completion_acceptances = { ...accepted, ...Object.fromEntries(opts.completionKeys.map((key) => [key, streamId])) };
443
+ const previousUpdates = s.extra.background_updates;
444
+ if (opts.backgroundUpdate)
445
+ recordBackgroundUpdate(s, streamId, opts.backgroundUpdate);
446
+ const admission = { stream_id: streamId, session_id: s.session_id, started_at: this.deps.now(), phase: 'starting', workspace: opts.workspace, model: opts.model, provider: opts.modelProvider, ephemeral: Boolean(opts.ephemeral), origin: turnOrigin(source), admitting: true };
447
+ this.registry.registerActiveRun(admission);
448
+ try {
449
+ await timing.measure('admission_persistence', () => this.deps.store.save(s));
450
+ }
451
+ catch (error) {
452
+ this.registry.activeRuns.delete(streamId);
453
+ s.extra.background_updates = previousUpdates;
454
+ s.extra.completion_acceptances = previousAcceptances;
455
+ s.automatic_turns_stop_held = previousStopHold;
456
+ throw error;
457
+ }
276
458
  this.registry.writebackOwners.set(s.session_id, streamId);
277
459
  // A new chat appears in the list; every turn tells open clients a run started (TAL-434).
278
460
  this.deps.events.publish(wasHiddenEmpty ? 'session_new' : 'turn_started', { profile: s.profile, sessionId: s.session_id });
279
461
  try {
280
- this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile);
462
+ this.deps.onTurnStart?.(s.session_id);
463
+ }
464
+ catch { /* best effort */ }
465
+ try {
466
+ await timing.measure('workspace_persistence', () => this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile));
281
467
  }
282
468
  catch { /* best effort */ }
283
469
  const channel = this.registry.create(streamId, s.session_id);
470
+ admission.admitting = false;
284
471
  if (opts.goalRelated)
285
472
  this.registry.goalRelated.add(streamId);
286
- this.registry.registerActiveRun({ stream_id: streamId, session_id: s.session_id, started_at: this.deps.now(), phase: 'starting', workspace: opts.workspace, model: opts.model, provider: opts.modelProvider, ephemeral: Boolean(opts.ephemeral), origin: turnOrigin(source) });
287
- const worker = this.run(s.session_id, streamId, channel, opts).catch((error) => {
473
+ timing.end('admission');
474
+ const worker = this.run(s.session_id, streamId, channel, opts, compactionDue).catch((error) => {
475
+ timing.finish(timingOutcome(error));
288
476
  this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
289
- }).finally(() => { this.workers.delete(streamId); });
477
+ }).finally(() => { timing.finish(this.registry.cancelled.has(streamId) ? 'cancelled' : undefined); this.workers.delete(streamId); });
290
478
  this.workers.set(streamId, worker);
291
479
  const response = { stream_id: streamId, session_id: s.session_id, pending_started_at: s.pending_started_at, turn_id: streamId, title: s.title };
292
480
  if (opts.normalizedModel && opts.model)
@@ -307,28 +495,78 @@ export class TurnRunner {
307
495
  s.messages.push(pendingUserRow(msg, attachments, startedAt, source, turnId));
308
496
  }
309
497
  // ── worker ───────────────────────────────────────────────────────────────
310
- async run(sessionId, streamId, channel, opts) {
498
+ async run(sessionId, streamId, channel, opts, compactionDue) {
311
499
  const { deps } = this;
500
+ const timing = opts.timing;
501
+ const closePreparation = timing?.begin('worker_preparation');
312
502
  let writer = null;
313
503
  try {
314
- writer = deps.journal.writer(sessionId, streamId);
504
+ writer = (await deps.journal.writer(sessionId, streamId));
315
505
  this.writers.set(streamId, writer);
316
506
  }
317
507
  catch (error) {
318
508
  deps.log(`[webui] WARNING: run journal degraded for stream ${streamId}: ${error.message}`);
319
509
  }
320
510
  const failure = { apperror: null, cancelled: false };
321
- const put = (event, data, meta = {}) => {
511
+ let waitDetail = null;
512
+ let providerPhase = null;
513
+ let statusEnded = false;
514
+ // TAL-1006: the open compression span closes on the Agent's end of it, else on the next content frame.
515
+ const closeCompression = async (createdAt) => {
516
+ if (!this.compressionSpans.has(streamId))
517
+ return;
518
+ await put('compressed', { session_id: sessionId, message: 'Compression finished' }, createdAt === undefined ? {} : { createdAt });
519
+ };
520
+ const put = async (event, data, meta = {}) => {
521
+ if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
522
+ return;
523
+ const terminal = ['done', 'cancel', 'apperror', 'error', 'stream_end'].includes(event);
524
+ if (terminal) {
525
+ statusEnded = true;
526
+ for (const call of this.registry.liveToolCalls.get(streamId) ?? [])
527
+ delete call.progress;
528
+ }
529
+ if (CONTENT_EVENTS.has(event) || terminal)
530
+ providerPhase = null;
531
+ if (event === 'rate_limit_wait')
532
+ waitDetail = data.wait;
533
+ // Both pre-turn compaction and Agent callbacks publish through this boundary.
534
+ if (event === 'compressing' && !statusEnded) {
535
+ const startedAt = this.compressionSpans.get(streamId) ?? meta.createdAt ?? deps.now();
536
+ this.compressionSpans.set(streamId, startedAt);
537
+ data = { ...data, started_at: startedAt };
538
+ }
539
+ else if (event === 'compressed')
540
+ this.compressionSpans.delete(streamId);
541
+ if (CONTENT_EVENTS.has(event)) {
542
+ if (waitDetail) {
543
+ await put('rate_limit_wait', { session_id: sessionId, wait: null }, meta.createdAt === undefined ? {} : { createdAt: meta.createdAt });
544
+ }
545
+ await closeCompression(meta.createdAt);
546
+ }
547
+ const status = event === 'run_status' ? data.status : statusEnded ? null : this.compressionSpans.has(streamId) ? { phase: 'compressing' }
548
+ : waitDetail ? { phase: 'rate_limit_wait', wait: waitDetail } : providerPhase ?? { phase: 'working' };
549
+ if (JSON.stringify(this.runStatuses.get(streamId) ?? null) !== JSON.stringify(status)) {
550
+ if (status)
551
+ this.runStatuses.set(streamId, status);
552
+ else
553
+ this.runStatuses.delete(streamId);
554
+ await put('run_status', { session_id: sessionId, status }, meta);
555
+ }
556
+ // The sidecar phase is private; only the selected status crosses the client boundary.
557
+ if (event === 'provider_phase')
558
+ return;
322
559
  if (event === 'apperror')
323
560
  failure.apperror = data;
324
561
  else if (event === 'cancel')
325
562
  failure.cancelled = true;
326
563
  for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
327
- put(steerEvent[0], steerEvent[1]);
328
- if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
564
+ (await put(steerEvent[0], steerEvent[1]));
565
+ if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
329
566
  return;
330
567
  // `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
331
568
  if (event === 'cancel' && !this.registry.streams.has(streamId)) {
569
+ timing?.frame(event, data);
332
570
  try {
333
571
  deps.onTerminal?.(streamId, 'cancelled');
334
572
  }
@@ -336,9 +574,9 @@ export class TurnRunner {
336
574
  return;
337
575
  }
338
576
  let eventId = null;
339
- if (writer) {
577
+ if (writer && event !== 'tool_progress') {
340
578
  try {
341
- eventId = writer.appendSseEvent(event, data, meta).event_id;
579
+ eventId = (await writer.appendSseEvent(event, data, meta)).event_id;
342
580
  }
343
581
  catch (error) {
344
582
  this.registry.degradedJournals.add(streamId);
@@ -346,6 +584,7 @@ export class TurnRunner {
346
584
  }
347
585
  }
348
586
  channel.put([event, data, eventId, meta.redacted]);
587
+ timing?.frame(event, data);
349
588
  if (event === 'done' || event === 'cancel' || event === 'apperror' || event === 'error') {
350
589
  // Relay's phase follows the turn outcome, so a background Live Activity ends as the app would end it.
351
590
  const state = str(data.terminal_state);
@@ -360,25 +599,46 @@ export class TurnRunner {
360
599
  const activeRun = this.registry.activeRuns.get(streamId);
361
600
  let s;
362
601
  try {
363
- s = deps.store.get(sessionId);
602
+ s = (await deps.store.get(sessionId));
364
603
  }
365
604
  catch {
366
- put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
367
- this.teardown(sessionId, streamId);
605
+ await put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
606
+ await this.teardown(sessionId, streamId);
368
607
  return;
369
608
  }
609
+ const notificationScope = { owner: str(s.extra.notification_owner) || 'local-owner', profile: s.profile ?? 'default' };
370
610
  const msgText = opts.msg;
371
- const previousMessages = structuredClone(s.messages);
372
- // TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
373
- const startRead = deps.service().stateDbRead(s);
374
- // TAL-578: an eager save already put this turn's prompt in the transcript; `user_message` carries it, so the history
375
- // leaves it out.
376
- const previousContext = structuredClone(deps.service().modelContext(s, startRead.rows)).filter((m) => m._turn_id !== streamId);
377
- // Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
378
- // the Agent gets back the exact bytes it sent (TAL-541).
379
- const apiHistory = sanitizeMessagesForApi(previousContext, { preserveApiContent: true });
611
+ const history = async () => {
612
+ // TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
613
+ const reading = deps.service().beginStateDbTurn(s);
614
+ // TAL-1162: the read runs on the state.db worker. Until it lands, a Stop settles from the local history with no
615
+ // starting id, as a turn whose starting read failed does.
616
+ const local = this.turnHistory(s, streamId, []);
617
+ this.stopContexts.set(streamId, { previousContext: local.previousContext, historyLength: local.apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: null, stateDbStartGeneration: null, startReadPending: true });
618
+ const startRead = await reading;
619
+ const previousMessages = structuredClone(s.messages);
620
+ const { previousContext, apiHistory } = this.turnHistory(s, streamId, startRead.rows);
621
+ // A Stop that landed during the read already took the local entry.
622
+ if (!this.registry.cancelled.has(streamId))
623
+ this.stopContexts.set(streamId, { previousContext, historyLength: apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null, stateDbStartGeneration: startRead.generation, startReadPending: false });
624
+ return { previousMessages, previousContext, apiHistory };
625
+ };
380
626
  // Before the first await: a Stop can land at any point after admission.
381
- this.stopContexts.set(streamId, { previousContext, historyLength: apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null });
627
+ const controller = new AbortController();
628
+ this.abortControllers.set(streamId, controller);
629
+ let { previousMessages, previousContext, apiHistory } = await history();
630
+ // TAL-1005: an over-threshold session compacts in its joinable compression job before the turn reads its history, so
631
+ // a Stop ends only this wait and a resend joins the same job. The turn then reads the compacted session afresh.
632
+ const compacted = await deps.service().ensureCompacted(sessionId, { turn: streamId, signal: controller.signal, due: compactionDue, onWait: async () => { (await put('compressing', { session_id: sessionId, message: 'Compressing context' })); } });
633
+ if (compacted) {
634
+ try {
635
+ s = (await deps.store.get(sessionId));
636
+ }
637
+ catch { /* the cancel or error path below reports a deleted session */ }
638
+ ;
639
+ ({ previousMessages, previousContext, apiHistory } = await history());
640
+ await put('compressed', this.compressedFrame(s));
641
+ }
382
642
  const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
383
643
  const sidecar = deps.sidecar();
384
644
  const partialText = this.registry.partialText.get(streamId) ?? [];
@@ -395,21 +655,19 @@ export class TurnRunner {
395
655
  // TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
396
656
  const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
397
657
  const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
398
- const putMetering = (force) => {
399
- const now = deps.now();
658
+ const putMetering = async (force, now = deps.now()) => {
400
659
  if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
401
660
  return;
402
661
  meter.sentAt = now;
403
662
  const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
404
- put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
663
+ await put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
405
664
  };
406
- const meterDelta = () => {
407
- const now = deps.now();
665
+ const meterDelta = async (now = deps.now()) => {
408
666
  if (!meter.deltas)
409
667
  meter.first = now;
410
668
  meter.last = now;
411
669
  meter.deltas += 1;
412
- putMetering(false);
670
+ await putMetering(false, now);
413
671
  };
414
672
  // TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
415
673
  // ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
@@ -418,13 +676,11 @@ export class TurnRunner {
418
676
  let heldText = '';
419
677
  const titles = new ReasoningTitleTracker();
420
678
  let capturedTerminalError = null;
421
- const controller = new AbortController();
422
- this.abortControllers.set(streamId, controller);
423
679
  const attachments = opts.attachments ?? [];
424
680
  // TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
425
681
  // without native images; only a turn with an image on either side asks for it.
426
682
  const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
427
- const userMessage = this.buildUserMessage(msgText, attachments, opts.workspace, sessionId, imageMode);
683
+ const userMessage = this.buildUserMessage(opts.prompt ?? msgText, attachments, opts.workspace, sessionId, imageMode);
428
684
  const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
429
685
  // The prompt the Agent actually gets, native image parts included.
430
686
  const stop = this.stopContexts.get(streamId);
@@ -434,6 +690,10 @@ export class TurnRunner {
434
690
  activeRun.phase = 'running';
435
691
  const settledAt = { value: false };
436
692
  let succeeded = false;
693
+ let ownershipRefused = false;
694
+ let agentDispatched = false;
695
+ let streamWrites = Promise.resolve();
696
+ let streamWriteError;
437
697
  // TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
438
698
  let followUp = [];
439
699
  try {
@@ -443,6 +703,11 @@ export class TurnRunner {
443
703
  // system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt with this
444
704
  // turn's workspace (TAL-710: never a tag on the user's message, which the Agent can persist from memory).
445
705
  const cfg = (await deps.profileConfig?.(s.profile ?? null)) ?? {};
706
+ const personality = str(s.personality);
707
+ // A selected personality never silently drops: an unknown catalog fails the turn (an absent name is the Agent's no-overlay).
708
+ const personalities = personality && deps.personalities
709
+ ? await deps.personalities(s.profile ?? null).catch((error) => { throw new SidecarError(`The session's personality "${personality}" could not be resolved: ${error instanceof Error ? error.message : String(error)}`, { condition: 'sidecar_error' }); })
710
+ : [];
446
711
  // The sidecar's per-session YOLO state does not survive its restarts: push the local flag (either way) before
447
712
  // the turn so a stale sidecar-side enable cannot auto-approve a session the UI reports as guarded.
448
713
  await this.withYoloLock(sessionId, async () => { try {
@@ -452,173 +717,288 @@ export class TurnRunner {
452
717
  // Last yield before `chat.start`: a cancel that landed during the attachment, config or YOLO awaits must not
453
718
  // start an Agent turn (the client also refuses an already-aborted signal outright).
454
719
  if (this.registry.cancelled.has(streamId)) {
455
- this.finalizeCancelled(s, streamId, opts.ephemeral);
456
- put('cancel', this.cancelFrame(sessionId));
720
+ await this.finalizeCancelled(s, streamId, opts.ephemeral);
721
+ await put('cancel', (await this.cancelFrame(sessionId)));
457
722
  return;
458
723
  }
459
724
  const turnContext = {
460
725
  system_message: workspaceSystemMessage(str(s.created_workspace) || str(s.workspace)),
461
- ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personality: str(s.personality) || null, sessionId, profile: s.profile ?? null, workspace: opts.workspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes', background: turnOrigin(opts.source) === 'background' }),
462
- max_iterations: processWakeupMaxIterations(maxIterationsFromConfig(cfg), opts.source ?? 'webui', deps.env ?? {}),
463
- max_tokens: maxTokensFromConfig(cfg),
464
- reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
726
+ ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personalityPrompt: findPersonality(personalities, personality)?.prompt ?? null, sessionId, profile: s.profile ?? null, workspace: opts.workspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes', background: turnOrigin(opts.source) === 'background' }),
727
+ ...agentBuildParams(cfg, opts.source ?? 'webui', opts.modelProvider ?? s.model_provider ?? null, deps.env ?? {}),
465
728
  };
466
729
  // Python `_public_prefill_context_status`: the session-recall prefill hook was dropped with TAL-245, so the
467
730
  // context frame always reports the not-configured shape.
468
- const persistentBefore = persistentStateSnapshot(deps.profileHome(s.profile));
469
- put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
731
+ const persistentHome = deps.profileHome(s.profile);
732
+ const persistentBefore = await persistentStateSnapshot(persistentHome);
733
+ await put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
734
+ // Setup awaits can outlive a gateway adoption, so the owner is rechecked right before dispatch.
735
+ await deps.service().assertWritable(s);
736
+ closePreparation?.();
737
+ timing?.emit('dispatch');
738
+ agentDispatched = true;
470
739
  const result = await sidecar.call('chat.start', {
471
- profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, workspace: opts.workspace, model: opts.model ?? '', model_provider: opts.modelProvider,
740
+ profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, ...this.agentWorkspace(s, opts.workspace), model: opts.model ?? '', model_provider: opts.modelProvider,
472
741
  user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
742
+ ...(opts.voiceTurn ? { voice_turn: true } : {}),
743
+ ...(opts.connectCard ? { connect_card: true } : {}),
744
+ ...(opts.skillSecret ? { skill_secret: true } : {}),
473
745
  }, {
746
+ timing,
474
747
  signal: controller.signal,
475
748
  timeoutMs: 0,
476
749
  onStream: (frame) => {
477
- const data = isDict(frame.data) ? frame.data : {};
478
- switch (frame.event) {
479
- case 'token': {
480
- tokenSent = true;
481
- firstTokenAt ??= deps.now();
482
- // Visible text closes the reasoning segment: a stable title snapshot (or a clear) goes out first.
483
- const stable = titles.stableSnapshot(reasoningText.join(''));
484
- if (stable !== null)
485
- put('reasoning', { text: '', titles: stable });
486
- partialText.push(str(data.text));
487
- if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
488
- heldText += str(data.text);
489
- meterDelta();
490
- return;
750
+ timing?.observe('first_sidecar_output');
751
+ const receivedAt = deps.now();
752
+ streamWrites = streamWrites.then(async () => {
753
+ const publish = put;
754
+ const emitFrame = (event, data, meta = {}) => publish(event, data, { ...meta, createdAt: receivedAt });
755
+ const data = isDict(frame.data) ? frame.data : {};
756
+ if ((frame.event === 'notice' || frame.event === 'notice_clear') && str(data.key).startsWith('startup-warning.')) {
757
+ try {
758
+ await deps.notifications.agentWarning(notificationScope, str(data.key), frame.event === 'notice' ? { text: str(data.text), level: data.level } : null);
491
759
  }
492
- put('token', { text: heldText + str(data.text) });
493
- heldText = '';
494
- meterDelta();
495
- return;
496
- }
497
- case 'reasoning': {
498
- if (partialText.length && !reasoningText.length)
499
- titles.resetSegment();
500
- reasoningText.push(str(data.text));
501
- // Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
502
- put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
503
- meterDelta();
504
- return;
505
- }
506
- case 'usage': {
507
- const usage = ChatUsageSchema.safeParse(data);
508
- if (usage.success) {
509
- applyAgentUsage(liveUsage, usage.data);
510
- this.liveUsage.set(streamId, liveUsage);
760
+ catch (error) {
761
+ // Notification storage must not turn a successful Agent result into a failed chat.
762
+ deps.log(`[webui] startup warning persistence failed for ${sessionId}: ${String(error)}`);
511
763
  }
512
- putMetering(true);
513
764
  return;
514
765
  }
515
- case 'steer_pending':
516
- // TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
517
- if (this.steerRewrites.has(streamId)) {
518
- this.deferredPendingSteer.set(streamId, str(data.text));
766
+ switch (frame.event) {
767
+ case 'token': {
768
+ tokenSent = true;
769
+ firstTokenAt ??= receivedAt;
770
+ // Visible text closes the reasoning segment: a stable title snapshot (or a clear) goes out first.
771
+ const stable = titles.stableSnapshot(reasoningText.join(''));
772
+ if (stable !== null)
773
+ (await emitFrame('reasoning', { text: '', titles: stable }));
774
+ partialText.push(str(data.text));
775
+ if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
776
+ heldText += str(data.text);
777
+ (await meterDelta(receivedAt));
778
+ return;
779
+ }
780
+ await emitFrame('token', { text: heldText + str(data.text) });
781
+ heldText = '';
782
+ await meterDelta(receivedAt);
519
783
  return;
520
784
  }
521
- this.settleConsumedSteers(sessionId, streamId, str(data.text));
522
- return;
523
- case 'tool': {
524
- const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
525
- liveToolCalls.push(call);
526
- toolIds.set(call, call.tid || mintToolId());
527
- toolStartedAt.set(call, deps.now());
528
- const redacted = deps.redactEnabled();
529
- put('tool', publicToolFrame(withToolId(data, toolIds.get(call)), redacted), { redacted });
530
- return;
531
- }
532
- case 'tool_complete': {
533
- // TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
534
- // are its display sections, which keep the stderr and exit code the flat preview drops.
535
- const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
536
- const outcome = toolOutcome(rawResult);
537
- complete.is_error = outcome.is_error;
538
- if (rawResult !== undefined)
539
- complete.result_view = outcome.result_view;
540
- // TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
541
- const editDiff = toolEditDiff(data.name, { diff: resultDiff });
542
- if (editDiff)
543
- complete.edit_diff = editDiff;
544
- const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
545
- if (tc) {
546
- const startedAt = toolStartedAt.get(tc);
547
- if (startedAt !== undefined)
548
- complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
549
- // TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
550
- Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
785
+ case 'reasoning': {
786
+ if (partialText.length && !reasoningText.length)
787
+ titles.resetSegment();
788
+ reasoningText.push(str(data.text));
789
+ // Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
790
+ await emitFrame('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
791
+ await meterDelta(receivedAt);
792
+ return;
551
793
  }
552
- const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
553
- this.lastCompletedTool.set(streamId, id);
554
- const redacted = deps.redactEnabled();
555
- put('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
556
- // TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
557
- const todos = data.name === 'todo' ? parseTodoToolResult(todoResult) : null;
558
- if (todos) {
559
- todos.ts = deps.now();
560
- this.liveTodos.set(streamId, todos);
561
- put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
794
+ case 'usage': {
795
+ const usage = ChatUsageSchema.safeParse(data);
796
+ if (usage.success) {
797
+ applyAgentUsage(liveUsage, usage.data);
798
+ this.liveUsage.set(streamId, liveUsage);
799
+ }
800
+ await putMetering(true, receivedAt);
801
+ return;
562
802
  }
563
- return;
564
- }
565
- // Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
566
- case 'approval': {
567
- deps.pending.submitApproval(sessionId, { ...data, session_id: sessionId });
568
- const { pending, pending_count } = deps.pending.approvalPending(sessionId);
569
- put('approval', { ...(pending ?? data), pending_count });
570
- return;
571
- }
572
- // TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
573
- case 'approval_resolved': {
574
- if (!str(data.approval_id))
803
+ case 'steer_pending': {
804
+ // TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
805
+ if (this.steerRewrites.has(streamId)) {
806
+ this.deferredPendingSteer.set(streamId, str(data.text));
807
+ return;
808
+ }
809
+ // Capture the causal position before the next frame; only persistence is asynchronous.
810
+ const saving = this.settleConsumedSteers(sessionId, streamId, str(data.text));
811
+ await saving;
575
812
  return;
576
- deps.pending.resolveApproval(sessionId, str(data.approval_id));
577
- put(...this.approvalHeadFrame(sessionId));
578
- return;
579
- }
580
- case 'clarify': {
581
- deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId, kind: 'clarify' });
582
- const head = deps.pending.clarifyHeadFrame(sessionId);
583
- if (head)
584
- put('clarify', head);
585
- return;
586
- }
587
- case 'clarify_resolved': {
588
- const { head } = deps.pending.resolveClarify(sessionId, str(data.clarify_id));
589
- if (head)
590
- put('clarify', head);
591
- return;
813
+ }
814
+ case 'tool': {
815
+ const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
816
+ liveToolCalls.push(call);
817
+ toolIds.set(call, call.tid || mintToolId());
818
+ toolStartedAt.set(call, receivedAt);
819
+ const redacted = deps.redactEnabled();
820
+ await emitFrame('tool', publicToolFrame(withToolId(data, toolIds.get(call)), redacted), { redacted });
821
+ return;
822
+ }
823
+ case 'tool_progress': {
824
+ if (statusEnded || this.registry.cancelled.has(streamId))
825
+ return;
826
+ const tc = liveToolCalls.findLast((call) => !call.done && call.tid === data.tool_call_id && Boolean(data.tool_call_id));
827
+ if (!tc)
828
+ return;
829
+ const redacted = deps.redactEnabled();
830
+ tc.progress = toolProgress({ ...data, message: typeof data.message === 'string' ? redactString(data.message, redacted) : null });
831
+ await emitFrame('tool_progress', { id: toolIds.get(tc), progress: tc.progress }, { redacted });
832
+ return;
833
+ }
834
+ case 'tool_complete': {
835
+ // TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
836
+ // are its display sections, which keep the stderr and exit code the flat preview drops.
837
+ const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, display_metadata: displayMetadata, ...complete } = data;
838
+ const outcome = toolOutcome(rawResult, data.name);
839
+ complete.progress = null;
840
+ complete.is_error = outcome.is_error;
841
+ if (rawResult !== undefined)
842
+ complete.result_view = outcome.result_view;
843
+ // TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame. TAL-909:
844
+ // else the diff in the metadata the Agent persisted on the result row, which a reload reads the same way.
845
+ const editDiff = toolEditDiff(data.name, { diff: resultDiff }, displayMetadata);
846
+ if (editDiff)
847
+ complete.edit_diff = editDiff;
848
+ const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
849
+ if (tc) {
850
+ const startedAt = toolStartedAt.get(tc);
851
+ if (startedAt !== undefined)
852
+ complete.duration = Math.round(Math.max(0, receivedAt - startedAt) * 1000) / 1000;
853
+ // TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
854
+ Object.assign(tc, { done: true, progress: null, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
855
+ }
856
+ if (startsBackgroundWork(data.name, rawResult))
857
+ deps.onBackgroundWork?.(sessionId, s.profile ?? null);
858
+ const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
859
+ this.lastCompletedTool.set(streamId, id);
860
+ const redacted = deps.redactEnabled();
861
+ await emitFrame('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
862
+ // TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
863
+ // TAL-889: the sidecar attaches `todo_result` only to the Agent's todo tool, so no name check here.
864
+ const todos = parseTodoToolResult(todoResult);
865
+ if (todos) {
866
+ todos.ts = receivedAt;
867
+ this.liveTodos.set(streamId, todos);
868
+ await emitFrame('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
869
+ }
870
+ return;
871
+ }
872
+ // Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
873
+ case 'approval': {
874
+ deps.pending.submitApproval(sessionId, { ...data, session_id: sessionId });
875
+ const { pending, pending_count } = deps.pending.approvalPending(sessionId);
876
+ await emitFrame('approval', { ...(pending ?? data), pending_count });
877
+ return;
878
+ }
879
+ // TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
880
+ case 'approval_resolved': {
881
+ if (!str(data.approval_id))
882
+ return;
883
+ deps.pending.resolveApproval(sessionId, str(data.approval_id));
884
+ await emitFrame(...this.approvalHeadFrame(sessionId));
885
+ return;
886
+ }
887
+ case 'clarify': {
888
+ deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId });
889
+ const head = deps.pending.clarifyHeadFrame(sessionId);
890
+ if (head)
891
+ (await emitFrame('clarify', head));
892
+ return;
893
+ }
894
+ case 'clarify_resolved': {
895
+ const { head } = deps.pending.resolveClarify(sessionId, str(data.clarify_id));
896
+ if (head)
897
+ (await emitFrame('clarify', head));
898
+ return;
899
+ }
900
+ case 'provider_phase': {
901
+ const phase = data.phase;
902
+ if (phase !== null && phase?.phase !== 'prompt_reading')
903
+ return;
904
+ const percent = phase?.progress_percent;
905
+ providerPhase = phase === null ? null : { phase: 'prompt_reading', progress_percent: typeof percent === 'number' && Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : null };
906
+ await put('provider_phase', {}, { createdAt: receivedAt });
907
+ return;
908
+ }
909
+ // TAL-821: the Agent sits out a provider's rate limit (`until`, epoch seconds: its reset, else the Agent's retry).
910
+ case 'rate_limit_wait': {
911
+ const until = typeof data.until === 'number' ? data.until : null;
912
+ if (until === null && !waitDetail)
913
+ return;
914
+ const provider = str(data.provider) || null;
915
+ await emitFrame('rate_limit_wait', { session_id: sessionId, wait: until === null ? null : { provider, provider_label: provider && (deps.providerLabel?.(s.profile ?? null, provider) ?? displayName(provider)), until, until_kind: data.until_kind === 'reset' ? 'reset' : 'retry' } });
916
+ return;
917
+ }
918
+ // TAL-818: a `manage_connections` card opened; clients poll and answer the operation by its `op_id`.
919
+ case 'connection': {
920
+ const snapshot = ConnectorOperationSnapshotSchema.safeParse(data);
921
+ if (snapshot.success)
922
+ (await emitFrame('connect_card', connectorOperation(snapshot.data, catalogNames(deps.profileHome(s.profile)))));
923
+ return;
924
+ }
925
+ case 'compressing':
926
+ await emitFrame('compressing', data);
927
+ return;
928
+ case 'compression_finished':
929
+ await closeCompression(receivedAt);
930
+ return;
931
+ case 'status':
932
+ if (data.kind === 'terminal_error' && !capturedTerminalError)
933
+ capturedTerminalError = str(data.message);
934
+ return;
935
+ // TAL-803: the session's notice list changed; every client replaces its copy.
936
+ case 'notice': {
937
+ const notice = this.notices.add(sessionId, data);
938
+ if (!notice)
939
+ return;
940
+ await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) });
941
+ // ponytail: the expiry reaches a client only while a turn streams; an idle tab drops it on its next session load.
942
+ if (notice.expires_at !== null) {
943
+ const expiresAt = notice.expires_at;
944
+ const emitAfterExpiry = async () => {
945
+ // Timers can run before the wall clock expires the notice. Let SessionNotices decide, then retry.
946
+ if (this.notices.list(sessionId).includes(notice))
947
+ setTimeout(() => { void emitAfterExpiry().catch((error) => { deps.log(`[webui] notice expiry failed: ${String(error)}`); }); }, Math.max(1, Math.ceil((expiresAt - deps.now()) * 1000))).unref();
948
+ else
949
+ (await this.emitToSession(sessionId, 'notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
950
+ };
951
+ await emitAfterExpiry();
952
+ }
953
+ return;
954
+ }
955
+ case 'notice_clear':
956
+ case 'notices_reset':
957
+ if (frame.event === 'notice_clear' ? this.notices.clear(sessionId, str(data.key)) : this.notices.reset(sessionId))
958
+ (await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
959
+ return;
960
+ default:
961
+ await emitFrame(frame.event, data);
592
962
  }
593
- case 'status':
594
- if (data.kind === 'terminal_error' && !capturedTerminalError)
595
- capturedTerminalError = str(data.message);
596
- return;
597
- default:
598
- put(frame.event, data);
599
- }
963
+ }).catch((error) => { streamWriteError = error instanceof Error ? error : new Error(String(error)); });
600
964
  },
601
965
  });
966
+ await streamWrites;
967
+ if (streamWriteError !== undefined)
968
+ throw streamWriteError;
602
969
  settledAt.value = true;
603
970
  // TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
604
971
  const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
605
972
  result.messages = withImages(result.messages);
606
973
  if (result.context_messages)
607
974
  result.context_messages = withImages(result.context_messages);
975
+ // Snapshot IO must finish before the cancellation/ownership gates and final transcript writeback.
976
+ let persistentChanges = null;
977
+ if (!opts.ephemeral && result.status === 'completed' && !this.registry.cancelled.has(streamId)) {
978
+ try {
979
+ persistentChanges = persistentStateChanges(persistentBefore, await persistentStateSnapshot(persistentHome));
980
+ }
981
+ catch (error) {
982
+ deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
983
+ }
984
+ }
985
+ // TAL-1162: the settlement's one state.db read runs on the worker before the gates below decide, so a Stop that
986
+ // lands during it takes the cancel path instead of racing this writeback.
987
+ const settlement = await deps.service().settlementRead(s);
608
988
  if (this.registry.cancelled.has(streamId) || result.status === 'cancelled') {
609
989
  // The Stop's pre-interrupt snapshot is the boundary, even when its reply lands after this result (the sidecar
610
990
  // answers each request on its own thread); without one, the Agent's interrupted result is its canonical transcript.
611
991
  const interrupted = await this.interrupts.get(streamId);
612
992
  applyAgentUsage(liveUsage, result.usage);
613
993
  this.liveUsage.set(streamId, liveUsage);
614
- this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
615
- put('cancel', this.cancelFrame(sessionId));
994
+ await this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
995
+ await put('cancel', (await this.cancelFrame(sessionId)));
616
996
  return;
617
997
  }
618
998
  // Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
619
999
  let current;
620
1000
  try {
621
- current = deps.store.get(sessionId);
1001
+ current = (await deps.store.get(sessionId));
622
1002
  }
623
1003
  catch {
624
1004
  current = s;
@@ -654,15 +1034,15 @@ export class TurnRunner {
654
1034
  await this.steerRewrites.get(streamId);
655
1035
  const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
656
1036
  followUp = leftovers;
657
- this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
1037
+ await this.persistError(s, settlement, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
658
1038
  // TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
659
1039
  if (!opts.ephemeral)
660
- payload.session = this.publicTerminalSession(s);
1040
+ payload.session = (await this.publicTerminalSession(s));
661
1041
  payload.session_id = s.session_id;
662
1042
  payload.old_session_id = sessionId;
663
1043
  for (const [event, data] of steerEvents)
664
- put(event, data);
665
- put('apperror', payload);
1044
+ (await put(event, data));
1045
+ await put('apperror', payload);
666
1046
  return;
667
1047
  }
668
1048
  // TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
@@ -676,11 +1056,11 @@ export class TurnRunner {
676
1056
  }
677
1057
  }
678
1058
  succeeded = true;
679
- opts.onDone?.(answer);
1059
+ await opts.onDone?.(answer);
680
1060
  // Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
681
- put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
1061
+ await put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
682
1062
  try {
683
- rmSync(deps.store.pathFor(sessionId), { force: true });
1063
+ await deps.store.delete(sessionId, { tombstone: false });
684
1064
  }
685
1065
  catch { /* ignore */ }
686
1066
  deps.store.sessions.delete(sessionId);
@@ -707,8 +1087,9 @@ export class TurnRunner {
707
1087
  const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
708
1088
  s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
709
1089
  s.context_messages = dedupeContext(context);
1090
+ // The settled turn's own figures: a compaction it waited on (TAL-1005) installed an estimate its prompt now supersedes.
1091
+ s.post_compression_context_tokens_estimate = result.compressed ? estimate : null;
710
1092
  if (result.compressed) {
711
- s.post_compression_context_tokens_estimate = estimate;
712
1093
  // TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
713
1094
  // holds no compaction marker, so the summary is the model context's newest one.
714
1095
  const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
@@ -733,20 +1114,11 @@ export class TurnRunner {
733
1114
  s.threshold_tokens = result.context.threshold_tokens;
734
1115
  if (typeof result.context.last_prompt_tokens === 'number')
735
1116
  s.last_prompt_tokens = result.context.last_prompt_tokens;
736
- if (result.compressed) {
737
- // TAL-540, Python `_live_usage_snapshot`: the counters and the ring's post-compression figures, so clients update before `done`.
738
- const usage = {
739
- ...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
740
- post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
741
- };
742
- put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
743
- }
1117
+ s.next_turn_threshold_tokens = result.context.next_turn_threshold_tokens ?? null;
1118
+ if (result.compressed)
1119
+ (await put('compressed', this.compressedFrame(s)));
744
1120
  s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
745
- s.active_stream_id = null;
746
- s.pending_user_message = null;
747
- s.pending_attachments = [];
748
- s.pending_started_at = null;
749
- s.pending_user_source = null;
1121
+ this.clearPendingTurn(s);
750
1122
  // TAL-576: a turn that ran means the provider has usable credentials again.
751
1123
  s.process_wakeup_pause = null;
752
1124
  const attachments = opts.attachments ?? [];
@@ -801,18 +1173,18 @@ export class TurnRunner {
801
1173
  this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
802
1174
  // TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
803
1175
  // (all of them, the dropped summary request too).
804
- deps.service().settleStateDb(s, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? previousContext, agentRows, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
805
- deps.store.save(s);
1176
+ this.settleStateDb(s, streamId, agentRows, { previousContext, read: settlement });
1177
+ recordBackgroundReview(s, streamId);
1178
+ await deps.store.save(s);
806
1179
  deps.pending.clearApprovals(sessionId);
807
1180
  deps.pending.clearClarifies(sessionId);
808
1181
  deps.events.publish('session_done', { profile: s.profile, sessionId: s.session_id });
809
1182
  // Python `_persistent_state_changes`: toast memory/skill files the agent wrote during the turn.
810
1183
  try {
811
- const changes = persistentStateChanges(persistentBefore, persistentStateSnapshot(deps.profileHome(s.profile)));
812
- if (changes.memory_saved)
813
- put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' });
814
- for (const change of changes.skills)
815
- put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name });
1184
+ if (persistentChanges?.memory_saved)
1185
+ (await put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' }));
1186
+ for (const change of persistentChanges?.skills ?? [])
1187
+ (await put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name }));
816
1188
  }
817
1189
  catch (error) {
818
1190
  deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
@@ -828,7 +1200,7 @@ export class TurnRunner {
828
1200
  cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
829
1201
  context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
830
1202
  };
831
- const doneSession = this.publicTerminalSession(s);
1203
+ const doneSession = (await this.publicTerminalSession(s));
832
1204
  // TAL-299: the ring's figures match the terminal session's (and a detail reload's).
833
1205
  for (const key of CONTEXT_USAGE_FIELDS)
834
1206
  doneUsage[key] = doneSession[key];
@@ -847,58 +1219,80 @@ export class TurnRunner {
847
1219
  if (result.tool_limit_reached)
848
1220
  donePayload.terminal_reason = 'max_iterations';
849
1221
  // TAL-397: the live meter ends on the persisted counters and turn rate.
850
- put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
851
- put('done', donePayload);
1222
+ await put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
1223
+ await put('done', donePayload);
852
1224
  for (const [event, payload] of steerEvents)
853
- put(event, payload);
1225
+ (await put(event, payload));
854
1226
  // The turn is over: release admission before the title work so a follow-up message is accepted while the
855
1227
  // (up to two) title prompts run; the channel and journal stay open for the `title` events (Python retired the
856
1228
  // worker before its daemon-thread title generation).
857
1229
  this.registry.activeRuns.delete(streamId);
858
1230
  this.settledStreams.add(streamId);
859
- this.startSteerFollowUp(sessionId, leftovers);
1231
+ // TAL-1005: a turn that settled at or above the next turn's threshold starts the compaction now, so the next message
1232
+ // usually finds the session compacted; a follow-up turn admitted meanwhile joins it.
1233
+ void deps.service().ensureCompacted(sessionId);
1234
+ await this.startSteerFollowUp(sessionId, leftovers);
860
1235
  // Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
861
1236
  await deps.syncUsage?.(s, usedModel || null);
862
1237
  // Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
863
1238
  const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
864
- put('stream_end', { session_id: sessionId });
1239
+ await put('stream_end', { session_id: sessionId });
865
1240
  for (const r of work)
866
1241
  if (r.status === 'rejected')
867
1242
  throw r.reason;
868
1243
  succeeded = true;
869
- opts.onDone?.(lastAnswer(s.messages));
1244
+ await opts.onDone?.(lastAnswer(s.messages));
870
1245
  }
871
1246
  catch (error) {
1247
+ timing?.fail(error);
1248
+ await streamWrites;
1249
+ const readOnly = !agentDispatched ? (await deps.service().readOnlyReason(s)) : null;
1250
+ if (readOnly) {
1251
+ timing?.fail({ status: 403 });
1252
+ ownershipRefused = true;
1253
+ if (this.registry.writebackOwners.get(sessionId) === streamId && s.active_stream_id === streamId) {
1254
+ // Cancellation persists the prompt and a Stop row; an unstarted, refused turn must add neither.
1255
+ s.messages = s.messages.filter((m) => !(m.role === 'user' && m._turn_id === streamId));
1256
+ this.clearPendingTurn(s);
1257
+ if (isDict(s.extra.completion_acceptances))
1258
+ s.extra.completion_acceptances = Object.fromEntries(Object.entries(s.extra.completion_acceptances).filter(([, value]) => value !== streamId));
1259
+ await deps.store.save(s, { touchUpdatedAt: false });
1260
+ }
1261
+ await put('apperror', { type: 'error', terminal_state: 'error', message: readOnly, session_id: sessionId, session: (await this.publicTerminalSession(s)) });
1262
+ return;
1263
+ }
872
1264
  if (settledAt.value && !(error instanceof SidecarError)) {
873
1265
  deps.log(`[webui] ERROR settling turn ${streamId}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
874
1266
  }
1267
+ // TAL-1162: the error's settlement read runs on the worker before the gates below decide.
1268
+ const settlement = await deps.service().settlementRead(s);
875
1269
  if (this.registry.cancelled.has(streamId)) {
876
- this.finalizeCancelled(s, streamId, opts.ephemeral);
877
- put('cancel', this.cancelFrame(sessionId));
1270
+ await this.finalizeCancelled(s, streamId, opts.ephemeral);
1271
+ await put('cancel', (await this.cancelFrame(sessionId)));
878
1272
  return;
879
1273
  }
880
1274
  const message = error instanceof Error ? error.message : String(error);
881
1275
  const condition = error instanceof SidecarError ? error.condition : undefined;
882
- const classification = classifyProviderError(message, { condition });
883
- const payload = providerErrorPayload(message, classification.type, classification.hint, deps.redactEnabled());
884
- if (condition === 'sidecar_unavailable' || condition === 'agent_runtime_stale') {
885
- payload.type = condition === 'agent_runtime_stale' ? 'agent_runtime_stale' : 'sidecar_unavailable';
886
- payload.condition = condition;
887
- }
1276
+ const runtime = error instanceof SidecarError && ['sidecar_unavailable', 'agent_runtime_stale', 'agent_incompatible'].includes(error.condition)
1277
+ ? runtimeFailureDetails(error.condition, error.data) : null;
1278
+ const classification = runtime ? { label: 'Error', type: runtime.type, hint: '' } : classifyProviderError(message, { condition });
1279
+ const payload = providerErrorPayload(runtime?.message ?? message, classification.type, classification.hint, deps.redactEnabled());
1280
+ if (runtime)
1281
+ Object.assign(payload, runtime);
888
1282
  try {
889
1283
  let current = s;
890
1284
  try {
891
- current = deps.store.get(sessionId);
1285
+ current = (await deps.store.get(sessionId));
892
1286
  }
893
1287
  catch {
894
1288
  current = s;
895
1289
  }
896
1290
  if (current.active_stream_id === streamId) {
897
1291
  for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
898
- put(event, data);
899
- this.persistError(current, streamId, classification.label, payload, activeTurnToken);
1292
+ (await put(event, data));
1293
+ await this.persistError(current, settlement, streamId, classification.label, payload, activeTurnToken);
900
1294
  if (!opts.ephemeral)
901
- payload.session = this.publicTerminalSession(current);
1295
+ payload.session = (await this.publicTerminalSession(current));
902
1296
  }
903
1297
  }
904
1298
  catch (persistError) {
@@ -906,15 +1300,20 @@ export class TurnRunner {
906
1300
  }
907
1301
  payload.session_id = sessionId;
908
1302
  payload.old_session_id = sessionId;
909
- put('apperror', payload);
1303
+ await put('apperror', payload);
910
1304
  }
911
1305
  finally {
912
- this.teardown(sessionId, streamId);
1306
+ await this.teardown(sessionId, streamId);
913
1307
  // After persistence and teardown: a background route's failure cleanup deletes the hidden session, which must
914
1308
  // not race the error writeback above (a re-saved session would resurface in the sidebar).
915
1309
  if (!succeeded)
916
- opts.onFailed?.(failure);
917
- this.startSteerFollowUp(sessionId, followUp);
1310
+ await opts.onFailed?.(failure);
1311
+ // TAL-1005: a Stop or an error after the turn ran on a compaction's result left that job's estimate under a context
1312
+ // its prompt and work may have grown; the idle trigger judges the context measured again.
1313
+ if (!succeeded && !ownershipRefused && !opts.ephemeral)
1314
+ void deps.service().remeasureContext(sessionId).then((measured) => { if (measured)
1315
+ void deps.service().ensureCompacted(sessionId); });
1316
+ await this.startSteerFollowUp(sessionId, followUp);
918
1317
  }
919
1318
  }
920
1319
  /**
@@ -995,33 +1394,30 @@ export class TurnRunner {
995
1394
  return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
996
1395
  }
997
1396
  /** The settled session as a terminal frame carries it: the display window, redacted. */
998
- publicTerminalSession(s) {
999
- return redactSessionData(this.terminalSessionPayload(s), this.deps.redactEnabled());
1397
+ async publicTerminalSession(s) {
1398
+ return this.deps.service().windowedDetail(s, TRANSCRIPT_WINDOW);
1000
1399
  }
1001
- /**
1002
- * Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
1003
- * msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
1004
- */
1005
- terminalSessionPayload(s) {
1006
- const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor, modelOptionFor: this.deps.service().deps.modelOptionFor }), this.registry.liveIds);
1007
- payload.assistant_name = this.deps.service().assistantName(s);
1008
- payload.workspace_name = this.deps.service().workspaceNames()(s);
1009
- const service = this.deps.service();
1010
- const scened = service.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(withAttachmentObjects(s.messages))), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true }));
1011
- const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
1012
- const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, service.mediaProjector(s)), s.active_stream_id));
1013
- payload.messages = limited;
1014
- payload.message_count = s.messages.length;
1015
- payload._messages_offset = offset;
1016
- payload._messages_truncated = offset > 0;
1017
- attachTodoState(payload, s.messages, s.extra[UNSETTLED_TODO_KEY]);
1018
- payload.tool_calls = toolCallsForMessageWindow(s.tool_calls, offset, limited.length);
1019
- payload.compression_reference = compressionReference(s, scened);
1020
- return payload;
1400
+ /** TAL-540, Python `_live_usage_snapshot`: a finished compression's frame, with the counters and the ring's post-compression figures, so clients update before `done`. */
1401
+ compressedFrame(s) {
1402
+ const usage = {
1403
+ ...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
1404
+ post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => this.deps.service().deps.contextLengthFor(s.model, s.model_provider)),
1405
+ };
1406
+ const sid = s.session_id;
1407
+ return { session_id: sid, old_session_id: sid, new_session_id: sid, continuation_session_id: sid, message: 'Compression finished', usage };
1408
+ }
1409
+ /** Release only the pending admission metadata; transcript settlement belongs to the caller. */
1410
+ clearPendingTurn(s) {
1411
+ s.active_stream_id = null;
1412
+ s.pending_user_message = null;
1413
+ s.pending_attachments = [];
1414
+ s.pending_started_at = null;
1415
+ s.pending_user_source = null;
1021
1416
  }
1022
1417
  /** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
1023
1418
  /** `runtimeProvider`: the provider the Agent resolved for the turn, when it reported one. */
1024
- persistError(s, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
1419
+ /** `read`: the settlement's one state.db read, taken before the caller's gates decided (TAL-1162). */
1420
+ async persistError(s, read, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
1025
1421
  const startedAt = s.pending_started_at;
1026
1422
  // TAL-576: an automatic wakeup with no usable credentials pauses the next ones instead of failing each.
1027
1423
  if (s.pending_user_source === 'process_wakeup' && payload.type === 'credential_pool_empty') {
@@ -1038,11 +1434,7 @@ export class TurnRunner {
1038
1434
  }
1039
1435
  this.materializePendingUserTurn(s, activeTurnToken, streamId);
1040
1436
  const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
1041
- s.active_stream_id = null;
1042
- s.pending_user_message = null;
1043
- s.pending_attachments = [];
1044
- s.pending_started_at = null;
1045
- s.pending_user_source = null;
1437
+ this.clearPendingTurn(s);
1046
1438
  this.appendPartialSnapshot(s, streamId);
1047
1439
  const hint = str(payload.hint);
1048
1440
  const errorMessage = { role: 'assistant', content: `**${label}:** ${str(payload.message) || label}${hint ? `\n\n*${hint}*` : ''}`, timestamp: Math.trunc(this.deps.now()), _error: true, _turn_id: streamId };
@@ -1068,14 +1460,12 @@ export class TurnRunner {
1068
1460
  this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
1069
1461
  // TAL-921: the next prompt remembers the turn's work; an exhausted compression's work would only overflow again. One
1070
1462
  // read builds the context and settles, so a row committed in between is never covered without reaching either.
1071
- const read = this.deps.service().stateDbRead(s);
1072
1463
  const context = payload.type === 'compression_exhausted' ? null : this.unsettledContext(streamId, agentRows, read);
1073
1464
  if (context)
1074
1465
  s.context_messages = context;
1075
- const stop = this.stopContexts.get(streamId);
1076
- this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, read });
1466
+ this.settleStateDb(s, streamId, agentRows, { read });
1077
1467
  try {
1078
- this.deps.store.save(s);
1468
+ (await this.deps.store.save(s));
1079
1469
  }
1080
1470
  catch (error) {
1081
1471
  this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
@@ -1086,6 +1476,20 @@ export class TurnRunner {
1086
1476
  this.deps.pending.clearClarifies(s.session_id);
1087
1477
  this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
1088
1478
  }
1479
+ /** The model history a turn starts from: the session's context merged with `stateRows`, without the turn's own rows. */
1480
+ turnHistory(s, streamId, stateRows) {
1481
+ // TAL-578: an eager save already put this turn's prompt in the transcript; `user_message` carries it, so the history
1482
+ // leaves it out.
1483
+ const previousContext = structuredClone(this.deps.service().modelContext(s, stateRows)).filter((m) => m._turn_id !== streamId);
1484
+ // Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
1485
+ // the Agent gets back the exact bytes it sent (TAL-541).
1486
+ return { previousContext, apiHistory: sanitizeMessagesForApi(previousContext, { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from }) };
1487
+ }
1488
+ /** Every terminal path carries the same starting read's id and generation into settlement. */
1489
+ settleStateDb(s, streamId, agentRows, opts) {
1490
+ const stop = this.stopContexts.get(streamId);
1491
+ this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? opts.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, startGeneration: stop?.stateDbStartGeneration ?? null, read: opts.read });
1492
+ }
1089
1493
  /**
1090
1494
  * TAL-921: the model context a turn that ends without its settled result leaves: the work its Agent reported, else the
1091
1495
  * rows it committed past its starting read in `read`, the read its settlement uses.
@@ -1094,7 +1498,7 @@ export class TurnRunner {
1094
1498
  const stop = this.stopContexts.get(streamId);
1095
1499
  if (!stop)
1096
1500
  return null;
1097
- const after = stop.stateDbStartId;
1501
+ const after = stop.stateDbStartGeneration !== null && read.generation !== null && stop.stateDbStartGeneration !== read.generation ? -1 : stop.stateDbStartId;
1098
1502
  const stateRows = after === null ? [] : read.rows.filter((m) => typeof m._state_db_row_id === 'number' && m._state_db_row_id > after);
1099
1503
  return this.deps.service().unsettledTurnContext({ ...stop, streamedText: (this.registry.partialText.get(streamId) ?? []).join(''), agentRows, stateRows });
1100
1504
  }
@@ -1143,11 +1547,11 @@ export class TurnRunner {
1143
1547
  * Python `_emit_cancel_event`: the terminal frame carries the settled session so tabs can render the partial without a
1144
1548
  * reload. TAL-364: no copy — clients show their one localized status for the `cancelled` outcome.
1145
1549
  */
1146
- cancelFrame(sessionId) {
1550
+ async cancelFrame(sessionId) {
1147
1551
  let snapshot = null;
1148
1552
  if (sessionId)
1149
1553
  try {
1150
- snapshot = this.publicTerminalSession(this.deps.store.get(sessionId));
1554
+ snapshot = (await this.publicTerminalSession((await this.deps.store.get(sessionId))));
1151
1555
  }
1152
1556
  catch {
1153
1557
  snapshot = null;
@@ -1159,21 +1563,33 @@ export class TurnRunner {
1159
1563
  * turn's model context keeps what the Agent had captured (`checkpoint`, its canonical transcript) and the prose that
1160
1564
  * streamed, so the next request continues from it; the terminal row carries the outcome and no copy.
1161
1565
  */
1162
- finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
1566
+ async finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
1163
1567
  if (this.registry.writebackOwners.get(s.session_id) !== streamId)
1164
1568
  return false;
1165
1569
  let current = s;
1166
1570
  try {
1167
- current = this.deps.store.get(s.session_id);
1571
+ current = (await this.deps.store.get(s.session_id));
1168
1572
  }
1169
1573
  catch {
1170
1574
  return false;
1171
1575
  }
1576
+ // TAL-921: one state.db read builds the context and settles the turn. TAL-1162: it runs on the state.db worker, before
1577
+ // any gate below decides.
1578
+ const read = await this.deps.service().settlementRead(current);
1579
+ if (this.registry.writebackOwners.get(s.session_id) !== streamId)
1580
+ return false;
1172
1581
  if (current.active_stream_id !== null && current.active_stream_id !== streamId)
1173
1582
  return false;
1583
+ const stop = this.stopContexts.get(streamId);
1584
+ if (stop?.startReadPending) {
1585
+ // TAL-1162: the Stop landed before the turn's starting read did, so the Agent never ran: every row this later read
1586
+ // holds predates the turn, and the context the turn would have started from keeps them. No row is its own work.
1587
+ const { previousContext, apiHistory } = this.turnHistory(current, streamId, read.rows);
1588
+ Object.assign(stop, { previousContext, historyLength: apiHistory.length, startReadPending: false });
1589
+ }
1174
1590
  if (ephemeral) {
1175
1591
  try {
1176
- rmSync(this.deps.store.pathFor(current.session_id), { force: true });
1592
+ await this.deps.store.delete(current.session_id, { tombstone: false });
1177
1593
  }
1178
1594
  catch { /* ignore */ }
1179
1595
  this.deps.store.sessions.delete(current.session_id);
@@ -1185,9 +1601,9 @@ export class TurnRunner {
1185
1601
  if (quiet && current.active_stream_id === null) {
1186
1602
  // TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
1187
1603
  if (checkpoint) {
1188
- this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? [], agentRows: checkpoint, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
1604
+ this.settleStateDb(current, streamId, checkpoint, { read });
1189
1605
  try {
1190
- this.deps.store.save(current);
1606
+ (await this.deps.store.save(current));
1191
1607
  }
1192
1608
  catch {
1193
1609
  return false;
@@ -1195,10 +1611,8 @@ export class TurnRunner {
1195
1611
  }
1196
1612
  return true;
1197
1613
  }
1198
- const stop = this.stopContexts.get(streamId);
1199
1614
  const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
1200
- // TAL-921: one state.db read builds the context and settles the turn.
1201
- const settle = (read) => {
1615
+ const settle = () => {
1202
1616
  if (!stop)
1203
1617
  return null;
1204
1618
  if (canonical)
@@ -1212,13 +1626,12 @@ export class TurnRunner {
1212
1626
  const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
1213
1627
  // TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
1214
1628
  if (ownStop && checkpoint) {
1215
- const read = this.deps.service().stateDbRead(current);
1216
- const late = canonical && !stop.checkpointed ? settle(read) : null;
1629
+ const late = canonical && !stop.checkpointed ? settle() : null;
1217
1630
  if (late)
1218
1631
  current.context_messages = dedupeContext(late);
1219
- this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null, read });
1632
+ this.settleStateDb(current, streamId, checkpoint, { read });
1220
1633
  try {
1221
- this.deps.store.save(current);
1634
+ (await this.deps.store.save(current));
1222
1635
  }
1223
1636
  catch {
1224
1637
  return false;
@@ -1228,24 +1641,19 @@ export class TurnRunner {
1228
1641
  }
1229
1642
  const startedAt = current.pending_started_at;
1230
1643
  this.materializePendingUserTurn(current, buildActiveTurnToken(streamId, current.pending_started_at), streamId);
1231
- const read = this.deps.service().stateDbRead(current);
1232
- const context = settle(read);
1644
+ const context = settle();
1233
1645
  if (context)
1234
1646
  current.context_messages = dedupeContext(context);
1235
- current.active_stream_id = null;
1236
- current.pending_user_message = null;
1237
- current.pending_attachments = [];
1238
- current.pending_started_at = null;
1239
- current.pending_user_source = null;
1647
+ this.clearPendingTurn(current);
1240
1648
  if (!quiet) {
1241
1649
  this.appendPartialSnapshot(current, streamId);
1242
1650
  current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
1243
1651
  }
1244
1652
  this.keepLiveState(current, streamId);
1245
1653
  this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
1246
- this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null, read });
1654
+ this.settleStateDb(current, streamId, checkpoint, { read });
1247
1655
  try {
1248
- this.deps.store.save(current);
1656
+ (await this.deps.store.save(current));
1249
1657
  }
1250
1658
  catch {
1251
1659
  return false;
@@ -1257,8 +1665,8 @@ export class TurnRunner {
1257
1665
  return true;
1258
1666
  }
1259
1667
  /** Push a frame onto the session's live chat stream from outside the turn (Python gateway notify callbacks). */
1260
- emitToSession(sessionId, event, data) {
1261
- this.sessionPuts.get(sessionId)?.(event, data);
1668
+ async emitToSession(sessionId, event, data) {
1669
+ await this.sessionPuts.get(sessionId)?.(event, data);
1262
1670
  }
1263
1671
  /** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
1264
1672
  keepLiveState(s, streamId) {
@@ -1269,13 +1677,15 @@ export class TurnRunner {
1269
1677
  if (todos)
1270
1678
  s.extra[UNSETTLED_TODO_KEY] = todos;
1271
1679
  }
1272
- teardown(sessionId, streamId) {
1680
+ async teardown(sessionId, streamId) {
1681
+ this.runStatuses.delete(streamId);
1273
1682
  this.liveUsage.delete(streamId);
1683
+ this.compressionSpans.delete(streamId);
1274
1684
  this.liveTodos.delete(streamId);
1275
1685
  const writer = this.writers.get(streamId);
1276
1686
  if (writer) {
1277
1687
  try {
1278
- writer.close();
1688
+ (await writer.close());
1279
1689
  }
1280
1690
  catch { /* ignore */ }
1281
1691
  this.writers.delete(streamId);
@@ -1304,21 +1714,28 @@ export class TurnRunner {
1304
1714
  catch { /* best effort */ }
1305
1715
  }
1306
1716
  /** Journal files with a live writer (retention must leave them alone). */
1307
- activeJournalPaths() {
1308
- return new Set([...this.writers.values()].map((w) => this.deps.journal.pathFor(w.sessionId, w.runId)));
1309
- }
1310
1717
  // ── title ────────────────────────────────────────────────────────────────
1311
1718
  /** Python `_generate_llm_session_title_via_aux`: aux-route statuses (`llm_aux`, `llm_aux_retry`, `llm_*_aux`). */
1312
1719
  async llmTitle(s, userText, assistantText) {
1313
1720
  const sidecar = this.deps.sidecar();
1314
1721
  if (!sidecar)
1315
1722
  return { title: '', status: 'llm_error_aux', rawPreview: '' };
1723
+ const service = this.deps.service();
1724
+ await service.deps.warmSessionModelRepair?.(s.profile);
1725
+ const [model, provider] = service.turnModel(s);
1316
1726
  const [qa, prompts] = titlePrompts(userText, assistantText);
1727
+ const source = [...s.messages, ...s.context_messages, ...await service.stateDbRows(s)];
1317
1728
  let status = 'llm_error_aux';
1318
1729
  for (const [idx, prompt] of prompts.entries()) {
1730
+ if ((await this.deps.service().isReadOnly(s)))
1731
+ return { title: '', status: 'read_only', rawPreview: '' };
1319
1732
  let raw = '';
1320
1733
  try {
1321
- const result = await sidecar.call('aux.complete', { profile_home: this.deps.profileHome(s.profile), task: 'title_generation', messages: [{ role: 'system', content: prompt }, { role: 'user', content: qa }], max_tokens: 64 }, { timeoutMs: 15_000 });
1734
+ const result = await sidecar.call('aux.complete', {
1735
+ profile_home: this.deps.profileHome(s.profile), task: 'title_generation',
1736
+ messages: withDataUseRequirements([{ role: 'system', content: prompt }, { role: 'user', content: qa }], source), max_tokens: 64,
1737
+ main_runtime: { model: model ?? '', provider }, main_fallback: true,
1738
+ });
1322
1739
  raw = str(result.text);
1323
1740
  }
1324
1741
  catch (error) {
@@ -1339,6 +1756,43 @@ export class TurnRunner {
1339
1756
  }
1340
1757
  return { title: '', status, rawPreview: '' };
1341
1758
  }
1759
+ /**
1760
+ * TAL-902: `/refine [focus]` starts the Agent's memory/skill review of the chat's model context on its agent
1761
+ * (`chat.refine`, the agent a turn would build); no turn starts. Without a chat, while a turn runs, or before anything
1762
+ * was said it answers with the Agent's wording instead.
1763
+ */
1764
+ async refine(s, focus) {
1765
+ if (!s)
1766
+ return 'Nothing to refine yet — send a message first.';
1767
+ const context = await this.deps.service().readModelContext(s);
1768
+ if (s.active_stream_id)
1769
+ return 'Agent is running — wait for the turn to finish, then /refine.';
1770
+ const messages = sanitizeMessagesForApi(context, { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from });
1771
+ if (!messages.length)
1772
+ return 'Nothing to refine yet — the conversation is empty.';
1773
+ // Like a turn: no agent is built in a profile whose home is going away.
1774
+ if (this.deps.profileDeleting?.(s.profile ?? null))
1775
+ throw new HttpFailure(409, `Profile '${str(s.profile)}' is being deleted.`);
1776
+ const sidecar = this.deps.sidecar();
1777
+ if (!sidecar)
1778
+ throw new SidecarError('The Agent sidecar is not running; /refine is unavailable until it starts.', { condition: 'sidecar_unavailable' });
1779
+ const [model, provider] = this.deps.service().turnModel(s);
1780
+ const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
1781
+ await this.deps.service().assertWritable(s);
1782
+ return (await sidecar.call('chat.refine', {
1783
+ profile_home: this.deps.profileHome(s.profile), session_id: s.session_id, messages, focus, model: model ?? '', model_provider: provider,
1784
+ enabled_toolsets: this.deps.toolsetsFor(s), ...agentBuildParams(cfg, 'webui', provider, this.deps.env ?? {}),
1785
+ ...this.agentWorkspace(s, (await this.deps.service().turnWorkspace(s))),
1786
+ }, { timeoutMs: 120_000 })).output;
1787
+ }
1788
+ /**
1789
+ * TAL-903: the workspace a session's agent is built at, and whether it is a host path (the profile's terminal runs
1790
+ * locally). Only then does the Agent anchor AGENTS.md discovery and its memory provider there; `chat.start` and
1791
+ * `chat.refine` send the same pair, so the agent `/refine` builds is the one the next turn reuses.
1792
+ */
1793
+ agentWorkspace(s, workspace) {
1794
+ return { workspace, workspace_local: this.deps.workspaces.profileSupportsLocalIo(s.profile) };
1795
+ }
1342
1796
  /**
1343
1797
  * Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
1344
1798
  * `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
@@ -1352,6 +1806,7 @@ export class TurnRunner {
1352
1806
  let rawPreview = '';
1353
1807
  if (assistantText)
1354
1808
  ({ title: next, status: llmStatus, rawPreview } = await this.llmTitle(s, userText, assistantText));
1809
+ await this.deps.service().assertWritable(s);
1355
1810
  if (next)
1356
1811
  return { title: next, status: llmStatus, rawPreview };
1357
1812
  const fallback = fallbackTitleFromExchange(userText, assistantText);
@@ -1366,7 +1821,7 @@ export class TurnRunner {
1366
1821
  async titleClaimed(s) {
1367
1822
  if (!looksLikeDefaultCliTitle({ title: s.title, source_tag: s.source_tag, raw_source: s.raw_source, session_source: s.session_source, source_label: s.source_label }))
1368
1823
  return;
1369
- await this.backgroundTitle(s, () => undefined).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
1824
+ await this.backgroundTitle(s, () => Promise.resolve()).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
1370
1825
  }
1371
1826
  /** Python `_run_background_title_update` (spawned only when `_background_title_generation_inputs` is eligible). */
1372
1827
  async backgroundTitle(s, put) {
@@ -1378,8 +1833,8 @@ export class TurnRunner {
1378
1833
  const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
1379
1834
  const [userText, assistantText] = firstExchangeSnippets(s.messages);
1380
1835
  // Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
1381
- const status = (status, reason = '', title = '', rawPreview = '') => {
1382
- put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
1836
+ const status = async (status, reason = '', title = '', rawPreview = '') => {
1837
+ await put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
1383
1838
  };
1384
1839
  // Python ran the adaptive refresh only when the first-title job was not eligible.
1385
1840
  if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText) {
@@ -1387,21 +1842,25 @@ export class TurnRunner {
1387
1842
  return;
1388
1843
  }
1389
1844
  if (s.manual_title) {
1390
- status('skipped', 'manual_title', placeholder);
1845
+ (await status('skipped', 'manual_title', placeholder));
1391
1846
  return;
1392
1847
  }
1393
1848
  // A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
1394
1849
  const cfg = await this.deps.profileConfig?.(s.profile ?? null);
1395
1850
  if (!cfg) {
1396
- status('skipped', 'config_unavailable', placeholder);
1851
+ (await status('skipped', 'config_unavailable', placeholder));
1397
1852
  return;
1398
1853
  }
1399
1854
  if (!titleGenerationEnabled(cfg)) {
1400
- status('skipped', 'title_generation_disabled', placeholder);
1855
+ (await status('skipped', 'title_generation_disabled', placeholder));
1401
1856
  return;
1402
1857
  }
1403
1858
  const generated = await this.llmTitle(s, userText, assistantText);
1404
1859
  const { status: llmStatus, rawPreview } = generated;
1860
+ if (llmStatus === 'read_only') {
1861
+ (await status('skipped', 'read_only', placeholder));
1862
+ return;
1863
+ }
1405
1864
  let next = generated.title;
1406
1865
  let source = llmStatus;
1407
1866
  if (!next) {
@@ -1414,26 +1873,30 @@ export class TurnRunner {
1414
1873
  const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
1415
1874
  let current = s;
1416
1875
  try {
1417
- current = this.deps.store.get(sessionId);
1876
+ current = (await this.deps.store.get(sessionId));
1418
1877
  }
1419
1878
  catch {
1420
1879
  // A session deleted while the title prompt ran stays deleted.
1421
- if (this.deps.store.wasDeleted(sessionId))
1880
+ if ((await this.deps.store.wasDeleted(sessionId)))
1422
1881
  return;
1423
1882
  current = s;
1424
1883
  }
1884
+ if ((await this.deps.service().isReadOnly(current))) {
1885
+ (await status('skipped', 'read_only', str(current.title).trim()));
1886
+ return;
1887
+ }
1425
1888
  let effective = str(current.title).trim();
1426
1889
  let wrote = false;
1427
1890
  if (next) {
1428
1891
  const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
1429
1892
  if (current.manual_title || !stillAuto) {
1430
- status('skipped', 'manual_title', effective);
1893
+ (await status('skipped', 'manual_title', effective));
1431
1894
  return;
1432
1895
  }
1433
1896
  if (next !== effective) {
1434
1897
  current.title = next;
1435
1898
  markSessionTitleGenerated(current);
1436
- this.deps.store.save(current, { touchUpdatedAt: false });
1899
+ await this.deps.store.save(current, { touchUpdatedAt: false });
1437
1900
  // Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
1438
1901
  await this.deps.syncTitle?.(current);
1439
1902
  this.deps.events.publish('title', { profile: current.profile, sessionId });
@@ -1442,11 +1905,11 @@ export class TurnRunner {
1442
1905
  }
1443
1906
  }
1444
1907
  if (wrote) {
1445
- status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
1446
- put('title', { session_id: sessionId, title: effective });
1908
+ await status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
1909
+ await put('title', { session_id: sessionId, title: effective });
1447
1910
  }
1448
1911
  else
1449
- status('skipped', source || 'unchanged', effective, rawPreview);
1912
+ (await status('skipped', source || 'unchanged', effective, rawPreview));
1450
1913
  }
1451
1914
  /**
1452
1915
  * Python `_maybe_schedule_title_refresh` + `_run_background_title_refresh`: every N exchanges a generated title is
@@ -1463,7 +1926,7 @@ export class TurnRunner {
1463
1926
  // Admission is released before title work, so an earlier turn's refresh may still be running: refreshes apply in
1464
1927
  // turn order and each reads the title its predecessor wrote.
1465
1928
  const sessionId = s.session_id;
1466
- const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(() => this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status));
1929
+ const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(async () => { await this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status); });
1467
1930
  this.titleRefreshes.set(sessionId, run);
1468
1931
  try {
1469
1932
  await run;
@@ -1474,44 +1937,48 @@ export class TurnRunner {
1474
1937
  }
1475
1938
  }
1476
1939
  async applyTitleRefresh(sessionId, s, userText, assistantText, put, status) {
1477
- const read = () => { try {
1478
- return this.deps.store.get(sessionId);
1940
+ const read = async () => { try {
1941
+ return (await this.deps.store.get(sessionId));
1479
1942
  }
1480
1943
  catch {
1481
1944
  return s;
1482
1945
  } };
1483
- const latest = read();
1946
+ const latest = (await read());
1484
1947
  const placeholder = str(latest.title).trim();
1485
1948
  if (['Untitled', 'New Chat', ''].includes(placeholder) || latest.manual_title || !latest.llm_title_generated)
1486
1949
  return;
1487
1950
  const cfg = await this.deps.profileConfig?.(latest.profile ?? null);
1488
1951
  if (!cfg || !titleGenerationEnabled(cfg)) {
1489
- status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder);
1952
+ (await status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder));
1490
1953
  return;
1491
1954
  }
1492
1955
  const { title: next, status: llmStatus, rawPreview } = await this.llmTitle(latest, userText, assistantText);
1493
1956
  if (!next) {
1494
- status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview);
1957
+ (await status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview));
1495
1958
  return;
1496
1959
  }
1497
1960
  const normalize = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
1498
1961
  if (normalize(next) === normalize(placeholder)) {
1499
- status('refresh_skipped', 'same_title', placeholder, rawPreview);
1962
+ (await status('refresh_skipped', 'same_title', placeholder, rawPreview));
1500
1963
  return;
1501
1964
  }
1502
- const current = read();
1965
+ const current = (await read());
1503
1966
  // The user may have renamed the session while the title prompt ran.
1504
1967
  if (current.manual_title || str(current.title).trim() !== placeholder) {
1505
- status('skipped', 'manual_title', str(current.title).trim());
1968
+ (await status('skipped', 'manual_title', str(current.title).trim()));
1969
+ return;
1970
+ }
1971
+ if ((await this.deps.service().isReadOnly(current))) {
1972
+ (await status('refresh_skipped', 'read_only', str(current.title).trim()));
1506
1973
  return;
1507
1974
  }
1508
1975
  current.title = next;
1509
1976
  markSessionTitleGenerated(current);
1510
- this.deps.store.save(current, { touchUpdatedAt: false });
1977
+ await this.deps.store.save(current, { touchUpdatedAt: false });
1511
1978
  await this.deps.syncTitle?.(current);
1512
1979
  this.deps.events.publish('title', { profile: current.profile, sessionId });
1513
- status('refreshed', llmStatus, next, rawPreview);
1514
- put('title', { session_id: sessionId, title: next });
1980
+ await status('refreshed', llmStatus, next, rawPreview);
1981
+ await put('title', { session_id: sessionId, title: next });
1515
1982
  }
1516
1983
  // ── cancel / steer ───────────────────────────────────────────────────────
1517
1984
  /**
@@ -1519,6 +1986,26 @@ export class TurnRunner {
1519
1986
  * `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
1520
1987
  */
1521
1988
  async cancel(streamId) {
1989
+ const ownerCancelled = await this.deps.service().owner?.cancel(streamId);
1990
+ if (ownerCancelled !== null && ownerCancelled !== undefined)
1991
+ return { cancelled: ownerCancelled, withdrawn: [] };
1992
+ // TAL-899: the user's Stop holds automatic turns, even one that lands after `done`, unless a newer turn already
1993
+ // took the session: a late or retried Stop for a turn the user followed holds nothing.
1994
+ const owner = this.registry.ownerSessionId(streamId) ?? this.registry.activeRuns.get(streamId)?.session_id;
1995
+ if (owner && this.registry.writebackOwners.get(owner) === streamId) {
1996
+ this.stopHeld.add(owner);
1997
+ try {
1998
+ const session = (await this.deps.store.get(owner));
1999
+ session.automatic_turns_stop_held = true;
2000
+ await this.deps.store.save(session, { touchUpdatedAt: false });
2001
+ }
2002
+ catch (error) {
2003
+ this.deps.log(`[webui] WARNING: failed to persist Stop hold for session ${owner}; interrupting with the in-memory hold: ${error.message}`);
2004
+ }
2005
+ }
2006
+ return (await this.stop(streamId));
2007
+ }
2008
+ async stop(streamId) {
1522
2009
  const channel = this.registry.peek(streamId);
1523
2010
  const run = this.registry.activeRuns.get(streamId);
1524
2011
  if (!channel && !run)
@@ -1553,24 +2040,24 @@ export class TurnRunner {
1553
2040
  if (sessionId) {
1554
2041
  let current = null;
1555
2042
  try {
1556
- current = this.deps.store.get(sessionId);
2043
+ current = (await this.deps.store.get(sessionId));
1557
2044
  }
1558
2045
  catch {
1559
2046
  current = null;
1560
2047
  }
1561
2048
  if (current?.active_stream_id === streamId) {
1562
- this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
2049
+ await this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
1563
2050
  }
1564
2051
  }
1565
2052
  // The worker may already have written its terminal row while the interrupt was in flight; the stream is then
1566
2053
  // gone from the registry and this path must not add a second one.
1567
2054
  if (channel && this.registry.streams.has(streamId)) {
1568
2055
  const writer = this.writers.get(streamId) ?? null;
1569
- const emit = (event, data) => {
2056
+ const emit = async (event, data) => {
1570
2057
  let eventId = null;
1571
2058
  if (writer) {
1572
2059
  try {
1573
- eventId = writer.appendSseEvent(event, data).event_id;
2060
+ eventId = (await writer.appendSseEvent(event, data)).event_id;
1574
2061
  }
1575
2062
  catch {
1576
2063
  eventId = null;
@@ -1579,8 +2066,10 @@ export class TurnRunner {
1579
2066
  channel.put([event, data, eventId]);
1580
2067
  };
1581
2068
  for (const [event, data] of steerEvents)
1582
- emit(event, data);
1583
- emit('cancel', this.cancelFrame(sessionId));
2069
+ (await emit(event, data));
2070
+ this.runStatuses.delete(streamId);
2071
+ await emit('run_status', { session_id: sessionId, status: null });
2072
+ await emit('cancel', (await this.cancelFrame(sessionId)));
1584
2073
  this.registry.streams.delete(streamId);
1585
2074
  this.registry.liveIds.delete(streamId);
1586
2075
  }
@@ -1596,7 +2085,7 @@ export class TurnRunner {
1596
2085
  const streamId = this.registry.activeRunStreamForSession(sessionId);
1597
2086
  if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
1598
2087
  return false;
1599
- await this.cancel(streamId);
2088
+ await this.stop(streamId);
1600
2089
  let timer;
1601
2090
  await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
1602
2091
  clearTimeout(timer);
@@ -1605,7 +2094,7 @@ export class TurnRunner {
1605
2094
  async steer(sessionId, text, displayText, steerId) {
1606
2095
  let s = null;
1607
2096
  try {
1608
- s = this.deps.store.get(sessionId, { metadataOnly: true });
2097
+ s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
1609
2098
  }
1610
2099
  catch {
1611
2100
  s = null;
@@ -1624,7 +2113,7 @@ export class TurnRunner {
1624
2113
  return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
1625
2114
  // Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
1626
2115
  // reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
1627
- return this.withSteerChain(activeStreamId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
2116
+ return this.withSteerChain(activeStreamId, sessionId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
1628
2117
  }
1629
2118
  async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
1630
2119
  const record = { steer_id: steerId, session_id: sessionId, stream_id: activeStreamId, text, display_text: displayText || text, created_at: this.deps.now(), state: 'pending', can_redirect: false };
@@ -1656,7 +2145,7 @@ export class TurnRunner {
1656
2145
  record.can_redirect = result.can_redirect === true;
1657
2146
  // TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
1658
2147
  if (this.steers.get(activeStreamId)?.includes(record))
1659
- this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
2148
+ (await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
1660
2149
  }
1661
2150
  return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
1662
2151
  }
@@ -1725,10 +2214,10 @@ export class TurnRunner {
1725
2214
  return boundary;
1726
2215
  }
1727
2216
  /** A steer is saved as it enters the stream, so a reload mid-turn already has it; the terminal save re-places it. */
1728
- saveConsumedSteers(sessionId, streamId) {
2217
+ async saveConsumedSteers(sessionId, streamId) {
1729
2218
  let s;
1730
2219
  try {
1731
- s = this.deps.store.get(sessionId);
2220
+ s = (await this.deps.store.get(sessionId));
1732
2221
  }
1733
2222
  catch {
1734
2223
  return;
@@ -1736,7 +2225,7 @@ export class TurnRunner {
1736
2225
  if (s.active_stream_id !== streamId || this.placeConsumedSteers(s, streamId, s.pending_started_at) === null)
1737
2226
  return;
1738
2227
  try {
1739
- this.deps.store.save(s, { touchUpdatedAt: false });
2228
+ (await this.deps.store.save(s, { touchUpdatedAt: false }));
1740
2229
  }
1741
2230
  catch (error) {
1742
2231
  this.deps.log(`[webui] WARNING: failed to save consumed steer for ${sessionId}: ${error.message}`);
@@ -1796,18 +2285,18 @@ export class TurnRunner {
1796
2285
  * TAL-424: the steers a finished turn never took go on as one follow-up turn, sent once by the server for every client.
1797
2286
  * Should another turn already hold the session, they steer into that one instead, so no text is lost.
1798
2287
  */
1799
- startSteerFollowUp(sessionId, texts) {
2288
+ async startSteerFollowUp(sessionId, texts) {
1800
2289
  if (!texts.length)
1801
2290
  return;
1802
2291
  let s;
1803
2292
  try {
1804
- s = this.deps.store.get(sessionId);
2293
+ s = (await this.deps.store.get(sessionId));
1805
2294
  }
1806
2295
  catch {
1807
2296
  return;
1808
2297
  }
1809
2298
  const msg = texts.join('\n');
1810
- const started = this.start(s, { msg, attachments: [], workspace: s.workspace, model: s.model, modelProvider: s.model_provider });
2299
+ const started = (await this.startServerTurn(s, { msg }));
1811
2300
  if (!started._status || started._status < 400)
1812
2301
  return;
1813
2302
  if (started._status === 409 && started.active_stream_id) {
@@ -1835,61 +2324,88 @@ export class TurnRunner {
1835
2324
  // Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
1836
2325
  const goalTurn = this.registry.goalRelated.has(streamId);
1837
2326
  if (goalTurn)
1838
- put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
2327
+ (await put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' }));
1839
2328
  let decision;
1840
2329
  try {
1841
- decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true });
2330
+ // The workspace the turn ran in, unrecovered: the Agent pauses the goal rather than run its gates anywhere else.
2331
+ await this.deps.service().assertWritable(s);
2332
+ decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true, workspace: str(s.workspace) || undefined });
1842
2333
  }
1843
2334
  catch (error) {
1844
2335
  this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
1845
2336
  if (goalTurn)
1846
- put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
2337
+ (await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' }));
1847
2338
  return;
1848
2339
  }
2340
+ // The judgement moved the goal on (turns, park, pause, done); an inactive one only matters if a goal still shows.
2341
+ if (decision.verdict !== 'inactive' || this.goals.view(sessionId))
2342
+ await this.refreshGoal(s, put);
1849
2343
  if (!goalTurn && decision.verdict === 'inactive')
1850
2344
  return;
1851
2345
  const prompt = str(decision.continuation_prompt).trim();
1852
2346
  const proceed = decision.should_continue && Boolean(prompt);
1853
2347
  const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
1854
- put('goal', { session_id: sessionId, state: proceed ? 'continuing' : 'idle', ...status });
1855
- if (!proceed)
2348
+ if (!proceed) {
2349
+ (await put('goal', { session_id: sessionId, state: 'idle', ...status }));
1856
2350
  return;
2351
+ }
1857
2352
  let current;
1858
2353
  try {
1859
- current = this.deps.store.get(sessionId);
2354
+ current = (await this.deps.store.get(sessionId));
1860
2355
  }
1861
2356
  catch {
1862
2357
  return;
1863
2358
  }
1864
- const started = this.start(current, { msg: prompt, attachments: [], workspace: current.workspace, model: current.model, modelProvider: current.model_provider, goalRelated: true });
2359
+ const started = (await this.startServerTurn(current, { msg: prompt, goalRelated: true }));
2360
+ // TAL-899: a Stop, even one after the answer, holds the continuation; the user's next turn is judged instead.
2361
+ if (started.held) {
2362
+ (await put('goal', { session_id: sessionId, state: 'idle', decision: 'held', message: '' }));
2363
+ return;
2364
+ }
2365
+ await put('goal', { session_id: sessionId, state: 'continuing', ...status });
1865
2366
  if (started._status === 409 && started.active_stream_id) {
1866
2367
  this.registry.goalRelated.add(started.active_stream_id);
1867
2368
  return;
1868
2369
  }
1869
2370
  if (started._status !== undefined && started._status >= 400) {
1870
2371
  this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
1871
- put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
2372
+ await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
1872
2373
  return;
1873
2374
  }
1874
- put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
2375
+ await put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
1875
2376
  }
1876
2377
  /** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
1877
- settleConsumedSteers(sessionId, streamId, pendingText) {
2378
+ async settleConsumedSteers(sessionId, streamId, pendingText) {
1878
2379
  for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
1879
- this.emitToSession(sessionId, 'steer_consumed', record);
1880
- this.saveConsumedSteers(sessionId, streamId);
2380
+ (await this.emitToSession(sessionId, 'steer_consumed', record));
2381
+ await this.saveConsumedSteers(sessionId, streamId);
1881
2382
  }
1882
2383
  /** What every client shows for a pending steer; the server decides which actions it offers. */
1883
2384
  pendingSteerView(r) {
1884
2385
  const open = r.state === 'pending';
1885
2386
  return { steer_id: r.steer_id, text: r.display_text, submitted_at: r.created_at, state: r.state, actions: { edit: open, cancel: open, send_now: open && r.can_redirect } };
1886
2387
  }
2388
+ liveToolProgress(streamId) {
2389
+ if (this.registry.cancelled.has(streamId))
2390
+ return [];
2391
+ return (this.registry.liveToolCalls.get(streamId) ?? []).filter((call) => !call.done && call.progress).map((call) => ({ ...call, id: str(call.tid) }));
2392
+ }
2393
+ runStatus(streamId) {
2394
+ return this.runStatuses.get(streamId) ?? null;
2395
+ }
2396
+ /** TAL-1006: the start (epoch seconds) of the stream's open compression span, else null. */
2397
+ compressionStartedAt(streamId) {
2398
+ return this.compressionSpans.get(streamId) ?? null;
2399
+ }
1887
2400
  /** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
1888
2401
  pendingSteers(streamId) {
1889
2402
  return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
1890
2403
  }
1891
- withSteerChain(streamId, fn) {
1892
- const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(fn);
2404
+ withSteerChain(streamId, sessionId, fn) {
2405
+ const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(async () => {
2406
+ await this.deps.service().getForMutation(sessionId);
2407
+ return fn();
2408
+ });
1893
2409
  const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
1894
2410
  this.steerChains.delete(streamId); });
1895
2411
  this.steerChains.set(streamId, settled);
@@ -1903,7 +2419,7 @@ export class TurnRunner {
1903
2419
  async rewriteSteer(sessionId, steerId, fallback, rewrite) {
1904
2420
  let s = null;
1905
2421
  try {
1906
- s = this.deps.store.get(sessionId, { metadataOnly: true });
2422
+ s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
1907
2423
  }
1908
2424
  catch {
1909
2425
  s = null;
@@ -1911,21 +2427,21 @@ export class TurnRunner {
1911
2427
  const streamId = s?.active_stream_id;
1912
2428
  const sidecar = this.deps.sidecar();
1913
2429
  if (!streamId || !sidecar)
1914
- return fallback;
1915
- return this.withSteerChain(streamId, async () => {
2430
+ return fallback('not_running');
2431
+ return this.withSteerChain(streamId, sessionId, async () => {
1916
2432
  const records = this.steers.get(streamId) ?? [];
1917
2433
  const index = records.findIndex((r) => r.steer_id === steerId);
1918
2434
  if (index < 0 || records[index].state !== 'pending')
1919
- return fallback;
2435
+ return fallback('taken');
1920
2436
  // In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
1921
2437
  let finished = () => undefined;
1922
2438
  this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
1923
- let outcome = { result: fallback, changed: false };
2439
+ let outcome = { result: fallback('failed'), changed: false };
1924
2440
  try {
1925
2441
  outcome = await rewrite(sidecar, streamId, records, index);
1926
2442
  }
1927
2443
  catch {
1928
- outcome = { result: fallback, changed: false };
2444
+ outcome = { result: fallback('failed'), changed: false };
1929
2445
  }
1930
2446
  finally {
1931
2447
  this.steerRewrites.delete(streamId);
@@ -1933,7 +2449,7 @@ export class TurnRunner {
1933
2449
  const deferred = this.deferredPendingSteer.get(streamId);
1934
2450
  this.deferredPendingSteer.delete(streamId);
1935
2451
  if (deferred !== undefined && !outcome.changed)
1936
- this.settleConsumedSteers(sessionId, streamId, deferred);
2452
+ (await this.settleConsumedSteers(sessionId, streamId, deferred));
1937
2453
  }
1938
2454
  return outcome.result;
1939
2455
  });
@@ -1948,54 +2464,61 @@ export class TurnRunner {
1948
2464
  * TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
1949
2465
  * emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
1950
2466
  */
1951
- withdrawSteer(sessionId, steerId, reason) {
1952
- return this.rewriteSteer(sessionId, steerId, { withdrawn: false }, async (sidecar, streamId, records, index) => {
2467
+ async withdrawSteer(sessionId, steerId, reason) {
2468
+ return (await this.rewriteSteer(sessionId, steerId, () => ({ withdrawn: false }), async (sidecar, streamId, records, index) => {
1953
2469
  const record = records[index];
1954
2470
  const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
1955
2471
  if (!reply.withdrawn)
1956
2472
  return { result: { withdrawn: false }, changed: false };
1957
2473
  this.dropSteer(streamId, record);
1958
- this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
2474
+ await this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
1959
2475
  return { result: { withdrawn: true, text: record.display_text }, changed: true };
1960
- });
2476
+ }));
1961
2477
  }
1962
2478
  /**
1963
2479
  * TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
1964
2480
  * once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
1965
2481
  * stays pending.
1966
2482
  */
1967
- sendSteerNow(sessionId, steerId) {
1968
- return this.rewriteSteer(sessionId, steerId, { redirected: false }, async (sidecar, streamId, records, index) => {
2483
+ async sendSteerNow(sessionId, steerId) {
2484
+ return (await this.rewriteSteer(sessionId, steerId, (outcome) => ({ redirected: false, outcome }), async (sidecar, streamId, records, index) => {
1969
2485
  const record = records[index];
1970
2486
  if (!record.can_redirect)
1971
- return { result: { redirected: false }, changed: false };
2487
+ return { result: { redirected: false, outcome: 'failed' }, changed: false };
1972
2488
  const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
1973
2489
  if (!reply.withdrawn)
1974
- return { result: { redirected: false }, changed: false };
2490
+ return { result: { redirected: false, outcome: reply.reason === 'not_running' ? 'not_running' : 'taken' }, changed: false };
1975
2491
  if (!reply.redirected) {
1976
2492
  // The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
1977
2493
  if (reply.requeued === 'last') {
1978
2494
  this.dropSteer(streamId, record);
1979
2495
  records.push(record);
1980
- this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
2496
+ (await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
1981
2497
  }
1982
- return { result: { redirected: false }, changed: true };
2498
+ return { result: { redirected: false, outcome: 'next_step' }, changed: true };
1983
2499
  }
1984
2500
  this.dropSteer(streamId, record);
1985
2501
  if (reply.delivery === 'steer') {
1986
2502
  record.state = 'sending_now';
1987
2503
  records.push(record);
1988
- this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
2504
+ await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
1989
2505
  }
1990
2506
  else {
1991
- this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
1992
- this.saveConsumedSteers(sessionId, streamId);
2507
+ await this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
2508
+ await this.saveConsumedSteers(sessionId, streamId);
1993
2509
  }
1994
- return { result: { redirected: true }, changed: true };
1995
- });
2510
+ return { result: { redirected: true, outcome: 'delivered' }, changed: true };
2511
+ }));
1996
2512
  }
1997
2513
  // ── approvals / clarify ──────────────────────────────────────────────────
1998
2514
  async respondApproval(sessionId, choice, approvalId, enableYolo) {
2515
+ if (await this.deps.service().owner?.isOwned(sessionId)) {
2516
+ if (enableYolo)
2517
+ throw new HttpFailure(403, 'Change approval policy through the owning Agent');
2518
+ await this.deps.service().owner.respond(sessionId, 'approval', approvalId, choice);
2519
+ return { ok: true, choice };
2520
+ }
2521
+ sessionId = (await this.deps.service().idForMutation(sessionId));
1999
2522
  // Python `_enable_session_yolo_and_release_pending`: a stale card id while another approval is parked
2000
2523
  // must not commit YOLO for the whole session — fail closed with the relay envelope instead.
2001
2524
  if (enableYolo && approvalId && !this.deps.pending.hasApprovalId(sessionId, approvalId) && this.deps.pending.hasPendingApproval(sessionId)) {
@@ -2008,7 +2531,7 @@ export class TurnRunner {
2008
2531
  const relayFailure = (message) => ({ ok: false, choice, error: `The Agent sidecar did not accept the approval (${message}); retry in a moment.`, _status: 503 });
2009
2532
  let s = null;
2010
2533
  try {
2011
- s = this.deps.store.get(sessionId, { metadataOnly: true });
2534
+ s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
2012
2535
  }
2013
2536
  catch {
2014
2537
  s = null;
@@ -2050,7 +2573,7 @@ export class TurnRunner {
2050
2573
  return relayFailure(str(error.message));
2051
2574
  }
2052
2575
  }
2053
- this.deps.service().setYolo(sessionId, true);
2576
+ await this.deps.service().setYolo(sessionId, true);
2054
2577
  return null;
2055
2578
  });
2056
2579
  if (failure)
@@ -2084,17 +2607,21 @@ export class TurnRunner {
2084
2607
  return;
2085
2608
  }
2086
2609
  if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
2087
- this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
2610
+ (await this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId)));
2088
2611
  }
2089
2612
  withYoloLock(sessionId, fn) {
2090
- const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(fn);
2613
+ const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(async () => {
2614
+ sessionId = (await this.deps.service().idForMutation(sessionId));
2615
+ return fn();
2616
+ });
2091
2617
  const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
2092
2618
  this.yoloChains.delete(sessionId); });
2093
2619
  this.yoloChains.set(sessionId, settled);
2094
2620
  return run;
2095
2621
  }
2096
2622
  /** Python `set_session_yolo_enabled(False)`: the sidecar must drop its per-session YOLO state before the local flag clears. */
2097
- disableYolo(sessionId) {
2623
+ async disableYolo(sessionId) {
2624
+ sessionId = (await this.deps.service().idForMutation(sessionId));
2098
2625
  return this.withYoloLock(sessionId, async () => {
2099
2626
  const sidecar = this.deps.sidecar();
2100
2627
  if (sidecar) {
@@ -2105,7 +2632,7 @@ export class TurnRunner {
2105
2632
  return { ok: false, error: `The Agent sidecar did not disable YOLO (${str(error.message)}); retry in a moment.`, yolo_enabled: this.deps.service().yolo(sessionId).yolo_enabled === true, _status: 503 };
2106
2633
  }
2107
2634
  }
2108
- return this.deps.service().setYolo(sessionId, false);
2635
+ return (await this.deps.service().setYolo(sessionId, false));
2109
2636
  });
2110
2637
  }
2111
2638
  /**
@@ -2114,16 +2641,25 @@ export class TurnRunner {
2114
2641
  * did not acknowledge (prompt retained).
2115
2642
  */
2116
2643
  async respondClarify(sessionId, clarifyId, reply) {
2644
+ if (await this.deps.service().owner?.isOwned(sessionId)) {
2645
+ await this.deps.service().owner.respond(sessionId, 'clarify', clarifyId, reply);
2646
+ return { ok: true };
2647
+ }
2648
+ sessionId = (await this.deps.service().idForMutation(sessionId));
2117
2649
  const entry = this.deps.pending.peekClarify(sessionId, clarifyId);
2118
2650
  if (!entry)
2119
2651
  return { ok: false, stale: true };
2120
- const response = typeof reply === 'string' ? reply : clarifyReply(entry, reply);
2652
+ // TAL-887: a password-manager answer goes to the Agent and nowhere else; `null` (a decline) is its empty reply.
2653
+ const vault = isVaultPrompt(entry);
2654
+ if (vault && typeof reply === 'string')
2655
+ return { ok: false, invalid: true };
2656
+ const response = vault ? vaultReply(entry, reply) : reply === null ? null : clarifyReply(entry, reply);
2121
2657
  if (response === null)
2122
2658
  return { ok: false, invalid: true };
2123
2659
  // A test-hook prompt has no parked Agent waiter: it is answered here.
2124
2660
  if (entry._injected === true) {
2125
2661
  this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
2126
- return { ok: true, response };
2662
+ return vault ? { ok: true } : { ok: true, response };
2127
2663
  }
2128
2664
  const sidecar = this.deps.sidecar();
2129
2665
  if (!sidecar)
@@ -2133,14 +2669,16 @@ export class TurnRunner {
2133
2669
  ok = (await sidecar.call('clarify.respond', { session_id: sessionId, clarify_id: str(entry.clarify_id), response })).ok;
2134
2670
  }
2135
2671
  catch (error) {
2672
+ if (vault)
2673
+ return { ok: false, error: 'The Agent sidecar did not accept the secure answer; retry in a moment.' };
2136
2674
  return { ok: false, error: `The Agent sidecar did not accept the answer (${str(error.message)}); retry in a moment.` };
2137
2675
  }
2138
2676
  if (!ok)
2139
2677
  return { ok: false, stale: true };
2140
2678
  const { head } = this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
2141
2679
  if (head)
2142
- this.emitToSession(sessionId, 'clarify', head);
2143
- return { ok: true, response };
2680
+ (await this.emitToSession(sessionId, 'clarify', head));
2681
+ return vault ? { ok: true } : { ok: true, response };
2144
2682
  }
2145
2683
  }
2146
2684
  /** The turn's answer: the last non-error assistant reply with content. */