@maudecode/talaria-web 1.17.2 → 1.19.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 (623) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.d.ts +1 -1
  3. package/dist/api/auth-raw.js +20 -20
  4. package/dist/api/auth-raw.js.map +1 -1
  5. package/dist/api/automation-raw.d.ts +7 -1
  6. package/dist/api/automation-raw.js +74 -27
  7. package/dist/api/automation-raw.js.map +1 -1
  8. package/dist/api/automation-router.d.ts +128 -23
  9. package/dist/api/automation-router.js +29 -13
  10. package/dist/api/automation-router.js.map +1 -1
  11. package/dist/api/chat-router.d.ts +1845 -40
  12. package/dist/api/chat-router.js +196 -114
  13. package/dist/api/chat-router.js.map +1 -1
  14. package/dist/api/git-router.d.ts +37 -3
  15. package/dist/api/git-router.js +47 -48
  16. package/dist/api/git-router.js.map +1 -1
  17. package/dist/api/raw-routes.d.ts +9 -1
  18. package/dist/api/raw-routes.js +286 -73
  19. package/dist/api/raw-routes.js.map +1 -1
  20. package/dist/api/router.d.ts +16 -6
  21. package/dist/api/router.js +28 -26
  22. package/dist/api/router.js.map +1 -1
  23. package/dist/api/session-visibility.d.ts +4 -4
  24. package/dist/api/session-visibility.js +26 -16
  25. package/dist/api/session-visibility.js.map +1 -1
  26. package/dist/api/sessions-router.d.ts +3312 -131
  27. package/dist/api/sessions-router.js +287 -157
  28. package/dist/api/sessions-router.js.map +1 -1
  29. package/dist/api/settings-router.d.ts +457 -8
  30. package/dist/api/settings-router.js +54 -33
  31. package/dist/api/settings-router.js.map +1 -1
  32. package/dist/api/sse-routes.d.ts +7 -2
  33. package/dist/api/sse-routes.js +109 -62
  34. package/dist/api/sse-routes.js.map +1 -1
  35. package/dist/api/tools-raw.d.ts +5 -0
  36. package/dist/api/tools-raw.js +63 -55
  37. package/dist/api/tools-raw.js.map +1 -1
  38. package/dist/api/tools-router.d.ts +998 -85
  39. package/dist/api/tools-router.js +447 -61
  40. package/dist/api/tools-router.js.map +1 -1
  41. package/dist/api/transcribe-stream.d.ts +16 -0
  42. package/dist/api/transcribe-stream.js +172 -0
  43. package/dist/api/transcribe-stream.js.map +1 -0
  44. package/dist/app.d.ts +4 -1
  45. package/dist/app.js +98 -59
  46. package/dist/app.js.map +1 -1
  47. package/dist/auth/gate.js +15 -11
  48. package/dist/auth/gate.js.map +1 -1
  49. package/dist/auth/oidc.d.ts +5 -2
  50. package/dist/auth/oidc.js +8 -7
  51. package/dist/auth/oidc.js.map +1 -1
  52. package/dist/auth/passkeys.d.ts +15 -14
  53. package/dist/auth/passkeys.js +89 -91
  54. package/dist/auth/passkeys.js.map +1 -1
  55. package/dist/auth/store.d.ts +17 -7
  56. package/dist/auth/store.js +93 -52
  57. package/dist/auth/store.js.map +1 -1
  58. package/dist/bin/talaria-web.js +12 -81
  59. package/dist/bin/talaria-web.js.map +1 -1
  60. package/dist/cli/ctl.js +2 -1
  61. package/dist/cli/ctl.js.map +1 -1
  62. package/dist/cli/dotenv.d.ts +5 -0
  63. package/dist/cli/dotenv.js +15 -2
  64. package/dist/cli/dotenv.js.map +1 -1
  65. package/dist/cli/launcher.js +5 -5
  66. package/dist/cli/launcher.js.map +1 -1
  67. package/dist/cli/main.d.ts +1 -0
  68. package/dist/cli/main.js +85 -0
  69. package/dist/cli/main.js.map +1 -0
  70. package/dist/cli/supervise.d.ts +2 -0
  71. package/dist/cli/supervise.js +67 -4
  72. package/dist/cli/supervise.js.map +1 -1
  73. package/dist/config/agent-config.d.ts +34 -11
  74. package/dist/config/agent-config.js +116 -60
  75. package/dist/config/agent-config.js.map +1 -1
  76. package/dist/config.d.ts +4 -0
  77. package/dist/config.js +19 -6
  78. package/dist/config.js.map +1 -1
  79. package/dist/fs/atomic.d.ts +1 -0
  80. package/dist/fs/atomic.js +4 -1
  81. package/dist/fs/atomic.js.map +1 -1
  82. package/dist/fs/stat.d.ts +1 -0
  83. package/dist/fs/stat.js +11 -0
  84. package/dist/fs/stat.js.map +1 -0
  85. package/dist/http/context.d.ts +31 -12
  86. package/dist/http/context.js +60 -24
  87. package/dist/http/context.js.map +1 -1
  88. package/dist/http/csp.d.ts +2 -1
  89. package/dist/http/csp.js +3 -2
  90. package/dist/http/csp.js.map +1 -1
  91. package/dist/onboarding.d.ts +2 -2
  92. package/dist/onboarding.js +10 -8
  93. package/dist/onboarding.js.map +1 -1
  94. package/dist/profiles/profiles.d.ts +20 -4
  95. package/dist/profiles/profiles.js +115 -30
  96. package/dist/profiles/profiles.js.map +1 -1
  97. package/dist/projects.d.ts +15 -6
  98. package/dist/projects.js +55 -47
  99. package/dist/projects.js.map +1 -1
  100. package/dist/providers/catalog.d.ts +84 -14
  101. package/dist/providers/catalog.js +474 -151
  102. package/dist/providers/catalog.js.map +1 -1
  103. package/dist/providers/env-file.d.ts +10 -3
  104. package/dist/providers/env-file.js +24 -75
  105. package/dist/providers/env-file.js.map +1 -1
  106. package/dist/providers/runtime-env.d.ts +4 -2
  107. package/dist/providers/runtime-env.js +9 -6
  108. package/dist/providers/runtime-env.js.map +1 -1
  109. package/dist/providers/tables.d.ts +3 -1
  110. package/dist/providers/tables.js +4 -2
  111. package/dist/providers/tables.js.map +1 -1
  112. package/dist/raster-image.d.ts +7 -0
  113. package/dist/raster-image.js +267 -0
  114. package/dist/raster-image.js.map +1 -0
  115. package/dist/redact.js +342 -46
  116. package/dist/redact.js.map +1 -1
  117. package/dist/release.d.ts +7 -0
  118. package/dist/release.js +14 -0
  119. package/dist/release.js.map +1 -1
  120. package/dist/runtime.d.ts +5 -1
  121. package/dist/runtime.js +305 -96
  122. package/dist/runtime.js.map +1 -1
  123. package/dist/server.js +15 -0
  124. package/dist/server.js.map +1 -1
  125. package/dist/sessions/anchor.d.ts +9 -3
  126. package/dist/sessions/anchor.js +28 -36
  127. package/dist/sessions/anchor.js.map +1 -1
  128. package/dist/sessions/background-events.d.ts +2 -0
  129. package/dist/sessions/background-events.js +10 -0
  130. package/dist/sessions/background-events.js.map +1 -0
  131. package/dist/sessions/background-tasks.d.ts +26 -27
  132. package/dist/sessions/background-tasks.js +89 -97
  133. package/dist/sessions/background-tasks.js.map +1 -1
  134. package/dist/sessions/background-updates.d.ts +15 -8
  135. package/dist/sessions/background-updates.js +101 -25
  136. package/dist/sessions/background-updates.js.map +1 -1
  137. package/dist/sessions/cli-sessions.d.ts +27 -6
  138. package/dist/sessions/cli-sessions.js +152 -42
  139. package/dist/sessions/cli-sessions.js.map +1 -1
  140. package/dist/sessions/completions.d.ts +77 -9
  141. package/dist/sessions/completions.js +482 -45
  142. package/dist/sessions/completions.js.map +1 -1
  143. package/dist/sessions/compress.d.ts +8 -0
  144. package/dist/sessions/compress.js +2 -2
  145. package/dist/sessions/compress.js.map +1 -1
  146. package/dist/sessions/drafts.d.ts +14 -10
  147. package/dist/sessions/drafts.js +38 -43
  148. package/dist/sessions/drafts.js.map +1 -1
  149. package/dist/sessions/export.d.ts +5 -0
  150. package/dist/sessions/export.js +30 -0
  151. package/dist/sessions/export.js.map +1 -1
  152. package/dist/sessions/gateway-sessions.d.ts +46 -0
  153. package/dist/sessions/gateway-sessions.js +378 -0
  154. package/dist/sessions/gateway-sessions.js.map +1 -0
  155. package/dist/sessions/gateway-watcher.d.ts +7 -3
  156. package/dist/sessions/gateway-watcher.js +31 -19
  157. package/dist/sessions/gateway-watcher.js.map +1 -1
  158. package/dist/sessions/goals.d.ts +31 -0
  159. package/dist/sessions/goals.js +75 -0
  160. package/dist/sessions/goals.js.map +1 -0
  161. package/dist/sessions/handoff.d.ts +32 -0
  162. package/dist/sessions/handoff.js +114 -0
  163. package/dist/sessions/handoff.js.map +1 -0
  164. package/dist/sessions/journal.d.ts +5 -79
  165. package/dist/sessions/journal.js +15 -606
  166. package/dist/sessions/journal.js.map +1 -1
  167. package/dist/sessions/list.d.ts +31 -16
  168. package/dist/sessions/list.js +139 -129
  169. package/dist/sessions/list.js.map +1 -1
  170. package/dist/sessions/merge.d.ts +69 -13
  171. package/dist/sessions/merge.js +265 -64
  172. package/dist/sessions/merge.js.map +1 -1
  173. package/dist/sessions/notices.d.ts +22 -0
  174. package/dist/sessions/notices.js +72 -0
  175. package/dist/sessions/notices.js.map +1 -0
  176. package/dist/sessions/pending.d.ts +10 -4
  177. package/dist/sessions/pending.js +83 -7
  178. package/dist/sessions/pending.js.map +1 -1
  179. package/dist/sessions/prefix-index.d.ts +82 -0
  180. package/dist/sessions/prefix-index.js +232 -0
  181. package/dist/sessions/prefix-index.js.map +1 -0
  182. package/dist/sessions/recovery.d.ts +19 -0
  183. package/dist/sessions/recovery.js +322 -0
  184. package/dist/sessions/recovery.js.map +1 -0
  185. package/dist/sessions/relay.d.ts +38 -19
  186. package/dist/sessions/relay.js +302 -79
  187. package/dist/sessions/relay.js.map +1 -1
  188. package/dist/sessions/service.d.ts +260 -55
  189. package/dist/sessions/service.js +1448 -462
  190. package/dist/sessions/service.js.map +1 -1
  191. package/dist/sessions/session.d.ts +24 -2
  192. package/dist/sessions/session.js +69 -7
  193. package/dist/sessions/session.js.map +1 -1
  194. package/dist/sessions/shares.d.ts +7 -6
  195. package/dist/sessions/shares.js +22 -45
  196. package/dist/sessions/shares.js.map +1 -1
  197. package/dist/sessions/source-kind.js +1 -0
  198. package/dist/sessions/source-kind.js.map +1 -1
  199. package/dist/sessions/state-db-identity.d.ts +18 -0
  200. package/dist/sessions/state-db-identity.js +67 -0
  201. package/dist/sessions/state-db-identity.js.map +1 -0
  202. package/dist/sessions/state-db-worker.d.ts +1 -0
  203. package/dist/sessions/state-db-worker.js +35 -0
  204. package/dist/sessions/state-db-worker.js.map +1 -0
  205. package/dist/sessions/state-db.d.ts +113 -7
  206. package/dist/sessions/state-db.js +861 -44
  207. package/dist/sessions/state-db.js.map +1 -1
  208. package/dist/sessions/storage/backend.d.ts +162 -0
  209. package/dist/sessions/storage/backend.js +19 -0
  210. package/dist/sessions/storage/backend.js.map +1 -0
  211. package/dist/sessions/storage/conformance.d.ts +20 -0
  212. package/dist/sessions/storage/conformance.js +237 -0
  213. package/dist/sessions/storage/conformance.js.map +1 -0
  214. package/dist/sessions/storage/document.d.ts +16 -0
  215. package/dist/sessions/storage/document.js +44 -0
  216. package/dist/sessions/storage/document.js.map +1 -0
  217. package/dist/sessions/storage/json-documents.d.ts +23 -0
  218. package/dist/sessions/storage/json-documents.js +233 -0
  219. package/dist/sessions/storage/json-documents.js.map +1 -0
  220. package/dist/sessions/storage/json-files.d.ts +12 -0
  221. package/dist/sessions/storage/json-files.js +124 -0
  222. package/dist/sessions/storage/json-files.js.map +1 -0
  223. package/dist/sessions/storage/json-installation.d.ts +3 -0
  224. package/dist/sessions/storage/json-installation.js +31 -0
  225. package/dist/sessions/storage/json-installation.js.map +1 -0
  226. package/dist/sessions/storage/json-journal.d.ts +114 -0
  227. package/dist/sessions/storage/json-journal.js +686 -0
  228. package/dist/sessions/storage/json-journal.js.map +1 -0
  229. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  230. package/dist/sessions/storage/json-recovery.js +195 -0
  231. package/dist/sessions/storage/json-recovery.js.map +1 -0
  232. package/dist/sessions/storage/json.d.ts +68 -0
  233. package/dist/sessions/storage/json.js +258 -0
  234. package/dist/sessions/storage/json.js.map +1 -0
  235. package/dist/sessions/storage/row.d.ts +6 -0
  236. package/dist/sessions/storage/row.js +49 -0
  237. package/dist/sessions/storage/row.js.map +1 -0
  238. package/dist/sessions/storage/search-text.d.ts +4 -0
  239. package/dist/sessions/storage/search-text.js +16 -0
  240. package/dist/sessions/storage/search-text.js.map +1 -0
  241. package/dist/sessions/store.d.ts +65 -68
  242. package/dist/sessions/store.js +305 -602
  243. package/dist/sessions/store.js.map +1 -1
  244. package/dist/sessions/streams.d.ts +4 -0
  245. package/dist/sessions/streams.js +9 -1
  246. package/dist/sessions/streams.js.map +1 -1
  247. package/dist/sessions/titles.d.ts +2 -0
  248. package/dist/sessions/titles.js +5 -1
  249. package/dist/sessions/titles.js.map +1 -1
  250. package/dist/sessions/todo.d.ts +5 -0
  251. package/dist/sessions/todo.js +28 -21
  252. package/dist/sessions/todo.js.map +1 -1
  253. package/dist/sessions/tool-display.d.ts +5 -4
  254. package/dist/sessions/tool-display.js +30 -50
  255. package/dist/sessions/tool-display.js.map +1 -1
  256. package/dist/sessions/turn-context.d.ts +11 -2
  257. package/dist/sessions/turn-context.js +18 -11
  258. package/dist/sessions/turn-context.js.map +1 -1
  259. package/dist/sessions/turn.d.ts +122 -17
  260. package/dist/sessions/turn.js +939 -358
  261. package/dist/sessions/turn.js.map +1 -1
  262. package/dist/sessions/wakeup-pause.d.ts +54 -0
  263. package/dist/sessions/wakeup-pause.js +163 -0
  264. package/dist/sessions/wakeup-pause.js.map +1 -0
  265. package/dist/sessions/window.js +13 -12
  266. package/dist/sessions/window.js.map +1 -1
  267. package/dist/settings.d.ts +19 -7
  268. package/dist/settings.js +178 -184
  269. package/dist/settings.js.map +1 -1
  270. package/dist/sidecar/client.d.ts +17 -3
  271. package/dist/sidecar/client.js +78 -8
  272. package/dist/sidecar/client.js.map +1 -1
  273. package/dist/sidecar/discover.d.ts +2 -1
  274. package/dist/sidecar/discover.js +93 -10
  275. package/dist/sidecar/discover.js.map +1 -1
  276. package/dist/sidecar/env.d.ts +4 -0
  277. package/dist/sidecar/env.js +3 -0
  278. package/dist/sidecar/env.js.map +1 -0
  279. package/dist/sidecar/fake.d.ts +2 -2
  280. package/dist/sidecar/fake.js +19 -2
  281. package/dist/sidecar/fake.js.map +1 -1
  282. package/dist/startup.d.ts +13 -5
  283. package/dist/startup.js +48 -0
  284. package/dist/startup.js.map +1 -1
  285. package/dist/test/env-file.d.ts +5 -0
  286. package/dist/test/env-file.js +88 -0
  287. package/dist/test/env-file.js.map +1 -0
  288. package/dist/test/harness.d.ts +1 -0
  289. package/dist/test/harness.js +1 -1
  290. package/dist/test/harness.js.map +1 -1
  291. package/dist/test/passkeys.d.ts +22 -0
  292. package/dist/test/passkeys.js +62 -0
  293. package/dist/test/passkeys.js.map +1 -0
  294. package/dist/test/storage.d.ts +15 -0
  295. package/dist/test/storage.js +26 -0
  296. package/dist/test/storage.js.map +1 -0
  297. package/dist/test/ts-worker-hooks.d.ts +1 -0
  298. package/dist/test/ts-worker-hooks.js +24 -0
  299. package/dist/test/ts-worker-hooks.js.map +1 -0
  300. package/dist/text/diff.d.ts +9 -14
  301. package/dist/text/diff.js +74 -108
  302. package/dist/text/diff.js.map +1 -1
  303. package/dist/tools/commands.d.ts +6 -1
  304. package/dist/tools/commands.js +8 -2
  305. package/dist/tools/commands.js.map +1 -1
  306. package/dist/tools/connectors.d.ts +33 -0
  307. package/dist/tools/connectors.js +121 -0
  308. package/dist/tools/connectors.js.map +1 -0
  309. package/dist/tools/crons.d.ts +38 -1
  310. package/dist/tools/crons.js +122 -18
  311. package/dist/tools/crons.js.map +1 -1
  312. package/dist/tools/dashboard-plugins.d.ts +53 -0
  313. package/dist/tools/dashboard-plugins.js +188 -0
  314. package/dist/tools/dashboard-plugins.js.map +1 -0
  315. package/dist/tools/extensions.d.ts +18 -5
  316. package/dist/tools/extensions.js +344 -266
  317. package/dist/tools/extensions.js.map +1 -1
  318. package/dist/tools/gateway-restart.d.ts +62 -0
  319. package/dist/tools/gateway-restart.js +142 -0
  320. package/dist/tools/gateway-restart.js.map +1 -0
  321. package/dist/tools/health.d.ts +44 -0
  322. package/dist/tools/health.js +201 -46
  323. package/dist/tools/health.js.map +1 -1
  324. package/dist/tools/hygiene.d.ts +4 -3
  325. package/dist/tools/hygiene.js +23 -9
  326. package/dist/tools/hygiene.js.map +1 -1
  327. package/dist/tools/kanban.d.ts +4 -2
  328. package/dist/tools/kanban.js +28 -19
  329. package/dist/tools/kanban.js.map +1 -1
  330. package/dist/tools/mcp.d.ts +13 -0
  331. package/dist/tools/mcp.js +88 -0
  332. package/dist/tools/mcp.js.map +1 -1
  333. package/dist/tools/memory-masks.d.ts +5 -0
  334. package/dist/tools/memory-masks.js +256 -0
  335. package/dist/tools/memory-masks.js.map +1 -0
  336. package/dist/tools/memory.d.ts +29 -3
  337. package/dist/tools/memory.js +69 -27
  338. package/dist/tools/memory.js.map +1 -1
  339. package/dist/tools/plugin-settings.d.ts +15 -0
  340. package/dist/tools/plugin-settings.js +90 -0
  341. package/dist/tools/plugin-settings.js.map +1 -0
  342. package/dist/tools/project-os.d.ts +11 -0
  343. package/dist/tools/project-os.js +267 -0
  344. package/dist/tools/project-os.js.map +1 -0
  345. package/dist/tools/skills.d.ts +14 -5
  346. package/dist/tools/skills.js +49 -16
  347. package/dist/tools/skills.js.map +1 -1
  348. package/dist/tools/update-notifications.d.ts +34 -15
  349. package/dist/tools/update-notifications.js +141 -57
  350. package/dist/tools/update-notifications.js.map +1 -1
  351. package/dist/tools/update-rollback.d.ts +11 -0
  352. package/dist/tools/update-rollback.js +28 -0
  353. package/dist/tools/update-rollback.js.map +1 -0
  354. package/dist/tools/updates.d.ts +110 -32
  355. package/dist/tools/updates.js +1112 -294
  356. package/dist/tools/updates.js.map +1 -1
  357. package/dist/tools/wiki.d.ts +68 -0
  358. package/dist/tools/wiki.js +413 -0
  359. package/dist/tools/wiki.js.map +1 -0
  360. package/dist/util.d.ts +2 -0
  361. package/dist/util.js +5 -0
  362. package/dist/util.js.map +1 -1
  363. package/dist/workspace/escape.d.ts +61 -0
  364. package/dist/workspace/escape.js +149 -0
  365. package/dist/workspace/escape.js.map +1 -0
  366. package/dist/workspace/extract.d.ts +22 -0
  367. package/dist/workspace/extract.js +308 -0
  368. package/dist/workspace/extract.js.map +1 -0
  369. package/dist/workspace/fs.d.ts +12 -0
  370. package/dist/workspace/fs.js +92 -23
  371. package/dist/workspace/fs.js.map +1 -1
  372. package/dist/workspace/git.d.ts +34 -1
  373. package/dist/workspace/git.js +328 -35
  374. package/dist/workspace/git.js.map +1 -1
  375. package/dist/workspace/media.d.ts +16 -13
  376. package/dist/workspace/media.js +43 -61
  377. package/dist/workspace/media.js.map +1 -1
  378. package/dist/workspace/rollback.d.ts +36 -22
  379. package/dist/workspace/rollback.js +178 -272
  380. package/dist/workspace/rollback.js.map +1 -1
  381. package/dist/workspace/unzip.js +4 -1
  382. package/dist/workspace/unzip.js.map +1 -1
  383. package/dist/workspace/upload.d.ts +7 -4
  384. package/dist/workspace/upload.js +5 -2
  385. package/dist/workspace/upload.js.map +1 -1
  386. package/dist/workspace/workspaces.d.ts +15 -19
  387. package/dist/workspace/workspaces.js +55 -127
  388. package/dist/workspace/workspaces.js.map +1 -1
  389. package/dist/workspace/worktrees.d.ts +2 -1
  390. package/dist/workspace/worktrees.js +179 -42
  391. package/dist/workspace/worktrees.js.map +1 -1
  392. package/package.json +7 -3
  393. package/sidecar/agent_dependency.json +4 -4
  394. package/sidecar/agent_support.json +26 -0
  395. package/sidecar/scripts/record-fixtures.py +12 -3
  396. package/sidecar/scripts/replay_sidecar.py +32 -0
  397. package/sidecar/scripts/suite_runner.py +302 -0
  398. package/sidecar/scripts/test.sh +12 -4
  399. package/sidecar/talaria_sidecar/__init__.py +6 -1
  400. package/sidecar/talaria_sidecar/__main__.py +4 -2
  401. package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
  402. package/sidecar/talaria_sidecar/agent_services.py +1053 -0
  403. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  404. package/sidecar/talaria_sidecar/home.py +56 -3
  405. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  406. package/sidecar/talaria_sidecar/methods/aux.py +166 -24
  407. package/sidecar/talaria_sidecar/methods/chat.py +852 -135
  408. package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
  409. package/sidecar/talaria_sidecar/methods/commands.py +294 -4
  410. package/sidecar/talaria_sidecar/methods/config.py +123 -4
  411. package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
  412. package/sidecar/talaria_sidecar/methods/cron.py +395 -36
  413. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  414. package/sidecar/talaria_sidecar/methods/gateway.py +212 -55
  415. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
  416. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  417. package/sidecar/talaria_sidecar/methods/kanban.py +108 -34
  418. package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
  419. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  420. package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
  421. package/sidecar/talaria_sidecar/methods/process.py +218 -15
  422. package/sidecar/talaria_sidecar/methods/profiles.py +121 -13
  423. package/sidecar/talaria_sidecar/methods/providers.py +217 -28
  424. package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
  425. package/sidecar/talaria_sidecar/methods/skills.py +67 -4
  426. package/sidecar/talaria_sidecar/methods/state_db.py +316 -75
  427. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  428. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  429. package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
  430. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  431. package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
  432. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  433. package/sidecar/talaria_sidecar/rpc.py +45 -4
  434. package/sidecar/talaria_sidecar/runtime.py +98 -13
  435. package/sidecar/talaria_sidecar/speed.py +35 -0
  436. package/static/dist/FILES.txt +126 -126
  437. package/static/dist/assets/AppShell-cey33v9s.js +8 -0
  438. package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
  439. package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
  440. package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
  441. package/static/dist/assets/Field-j8uqoigz.js +9 -0
  442. package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
  443. package/static/dist/assets/Markdown-lahuichm.js +598 -0
  444. package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
  445. package/static/dist/assets/Select-gqatm2e0.js +1 -0
  446. package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
  447. package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
  448. package/static/dist/assets/States-e7dkgtxf.js +1 -0
  449. package/static/dist/assets/Toaster-bfqqy786.js +1 -0
  450. package/static/dist/assets/_app-n1n6pfx1.js +1 -0
  451. package/static/dist/assets/{_app.ext._extensionId-opyh9y0v.js → _app.ext._extensionId-buxjxav3.js} +1 -1
  452. package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
  453. package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
  454. package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
  455. package/static/dist/assets/_app.logs-dm873z90.js +1 -0
  456. package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
  457. package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
  458. package/static/dist/assets/{_app.session._sessionId-bfd4h3id.js → _app.session._sessionId-g2vzghvc.js} +1 -1
  459. package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
  460. package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
  461. package/static/dist/assets/_app.skills-cii852yq.js +1 -0
  462. package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
  463. package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
  464. package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
  465. package/static/dist/assets/{abnfDiagram-VCTEODGH-b2x1lqot.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
  466. package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
  467. package/static/dist/assets/{architectureDiagram-5GKGNRK7-h38o5l7b.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
  468. package/static/dist/assets/archive-b1j004vn.js +1 -0
  469. package/static/dist/assets/back-g74gkozu.js +1 -0
  470. package/static/dist/assets/{blockDiagram-I7D4REHJ-bqquxv2u.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
  471. package/static/dist/assets/{c4Diagram-7LVT6UL2-f3fb8w6z.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
  472. package/static/dist/assets/channel-es95o4y8.js +1 -0
  473. package/static/dist/assets/{chunk-4HAMMTFA-bzfyyzp3.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
  474. package/static/dist/assets/{chunk-75Z2AOVW-ly99rfx5.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
  475. package/static/dist/assets/{chunk-DU6HZSFF-bg6ecfqf.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
  476. package/static/dist/assets/{chunk-F27PBJKO-e8k7mmqw.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
  477. package/static/dist/assets/{chunk-GMAD6QVW-cmalnoaj.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
  478. package/static/dist/assets/{chunk-GVQU2GXP-gmif5o01.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
  479. package/static/dist/assets/{chunk-IMKFNOWR-f1nwwh6u.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
  480. package/static/dist/assets/{chunk-L3NEJ4N5-oktfh5v7.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
  481. package/static/dist/assets/{chunk-OSK3NFVY-b1sdx96z.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
  482. package/static/dist/assets/{chunk-P2QGCYS3-fhu4nzxt.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
  483. package/static/dist/assets/{chunk-POPQ4Y6H-em31rnef.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
  484. package/static/dist/assets/{chunk-PWAF6VOD-bxhhyftw.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
  485. package/static/dist/assets/{chunk-SHT3W25Y-fmrvwcav.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
  486. package/static/dist/assets/{chunk-SVP7TREG-lymukl2j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
  487. package/static/dist/assets/{chunk-TICWLB2K-lmza6k23.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
  488. package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
  489. package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
  490. package/static/dist/assets/copied-bli9tbcx.js +1 -0
  491. package/static/dist/assets/{copy-fw5b1pc6.js → copy-jamtd1pf.js} +1 -1
  492. package/static/dist/assets/{cynefin-OW5HDTMX-czlhbyhs.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
  493. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-gq7mvswb.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
  494. package/static/dist/assets/{dagre-GXQ25YYZ-cqdbf7dr.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
  495. package/static/dist/assets/{diagram-S7CK7UJ4-o6arxa58.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
  496. package/static/dist/assets/{diagram-UQ7AKVKN-nhnd68tz.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
  497. package/static/dist/assets/{diagram-VSXAHHWV-dk2sclzn.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
  498. package/static/dist/assets/{diagram-VX7I27RA-n3k5nyx3.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
  499. package/static/dist/assets/{diagram-Z3DM3KII-m6q9cwdh.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
  500. package/static/dist/assets/{ebnfDiagram-PWID7BFC-lsa5641p.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
  501. package/static/dist/assets/{erDiagram-RLTQ6QDP-fo2ckbe6.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
  502. package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
  503. package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
  504. package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
  505. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-nzqz6x9g.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
  506. package/static/dist/assets/{gitGraph-4MIJSDKK-gukvcxe6.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
  507. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-lqhdp8f0.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
  508. package/static/dist/assets/{highlighted-body-KPVGNVTW-igw6cp39.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
  509. package/static/dist/assets/host-hzm8n85n.js +1 -0
  510. package/static/dist/assets/index-drdcaqeq.css +2 -0
  511. package/static/dist/assets/index-jzm844he.js +11 -0
  512. package/static/dist/assets/{info-A6RAGUB7-gvu7a93l.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
  513. package/static/dist/assets/{infoDiagram-27XIBGKW-m8t8g6w1.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
  514. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-l07vkbgj.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
  515. package/static/dist/assets/{journeyDiagram-3NMN7TZE-hybkgj8l.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
  516. package/static/dist/assets/{kanban-definition-UXKFOSKX-by13uvtn.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
  517. package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
  518. package/static/dist/assets/{line-osxq6fq7.js → line-h0ojzsvs.js} +1 -1
  519. package/static/dist/assets/link-k01ryaf9.js +1 -0
  520. package/static/dist/assets/login-bsae49ew.js +1 -0
  521. package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
  522. package/static/dist/assets/{mermaid-parser.core-i91oxmou.js → mermaid-parser.core-bnre27u8.js} +3 -3
  523. package/static/dist/assets/{mindmap-definition-YA3MSWOX-m82mx8ww.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
  524. package/static/dist/assets/onboarding-c65tysya.js +1 -0
  525. package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
  526. package/static/dist/assets/{packet-AYTQ26CC-nbtgge2o.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
  527. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  528. package/static/dist/assets/{pegDiagram-XKGWAZYB-f1zxzga1.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
  529. package/static/dist/assets/{pie-WAS4IAKB-of8vuhxs.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
  530. package/static/dist/assets/{pieDiagram-E7YTZNPT-o7y8ftio.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
  531. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-iqiwia1t.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
  532. package/static/dist/assets/{radar-RG4KPBEZ-delnsy2v.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
  533. package/static/dist/assets/{railroad-74A4TZTK-nspnyq9u.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
  534. package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
  535. package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
  536. package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
  537. package/static/dist/assets/{railroadDiagram-O6MQD6OU-z41tqx5z.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
  538. package/static/dist/assets/{refresh-cw-nt1dtsx4.js → refresh-cw-mqbkv2a3.js} +1 -1
  539. package/static/dist/assets/refresh-l29ljp76.js +1 -0
  540. package/static/dist/assets/{requirementDiagram-BXWQKSXE-bdvif194.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
  541. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-h73e0rol.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
  542. package/static/dist/assets/saved-ndd4t2xb.js +1 -0
  543. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-iv95mb1z.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
  544. package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
  545. package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
  546. package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
  547. package/static/dist/assets/{stateDiagram-D77RDMKH-k417ixmf.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
  548. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
  549. package/static/dist/assets/{swimlanes-42K2YHIH-esola70w.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
  550. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
  551. package/static/dist/assets/{timeline-definition-24CTP7MA-getrz2y8.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
  552. package/static/dist/assets/toast-i3h34v6o.js +64 -0
  553. package/static/dist/assets/{treeView-Q6P3EWNA-hc7ap1q7.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
  554. package/static/dist/assets/{treemap-WGGIJYW6-b7k1syy5.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
  555. package/static/dist/assets/untitled-oaoya4qj.js +1 -0
  556. package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
  557. package/static/dist/assets/{useParams-lhg566ew.js → useParams-gjc70qru.js} +1 -1
  558. package/static/dist/assets/{useSearch-lbrh6ejn.js → useSearch-e6ae539u.js} +1 -1
  559. package/static/dist/assets/{vennDiagram-4TSXK5OY-n6pcy5qb.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
  560. package/static/dist/assets/{wardley-WFR3VGLG-lpa18vz1.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
  561. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-kpclwwg7.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
  562. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-paa2n8y0.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
  563. package/static/dist/index.html +1 -1
  564. package/static/dist/sw.js +1 -1
  565. package/static/dist/assets/AppShell-fb2td9zi.js +0 -1
  566. package/static/dist/assets/ChatPage-cj96kiy9.js +0 -25
  567. package/static/dist/assets/Dialog-kxcg7ard.js +0 -1
  568. package/static/dist/assets/ExtensionRoute-jf7h0754.js +0 -1
  569. package/static/dist/assets/HubRoute-gmanpd5y.js +0 -2
  570. package/static/dist/assets/Markdown-mdwlvj3w.js +0 -598
  571. package/static/dist/assets/RightPanel-ftvrjlcp.js +0 -1
  572. package/static/dist/assets/Select-bilaypej.js +0 -1
  573. package/static/dist/assets/SessionListPanel-og8n36qs.js +0 -1
  574. package/static/dist/assets/SettingsLayout-jonjefmj.js +0 -1
  575. package/static/dist/assets/States-nmejrwrm.js +0 -1
  576. package/static/dist/assets/Toaster-nnl8zkt9.js +0 -1
  577. package/static/dist/assets/_app-gdidzd94.js +0 -1
  578. package/static/dist/assets/_app.index-hbarzbjx.js +0 -1
  579. package/static/dist/assets/_app.insights-luywlarz.js +0 -1
  580. package/static/dist/assets/_app.kanban-kincnbr8.js +0 -1
  581. package/static/dist/assets/_app.logs-oi2gr3gs.js +0 -1
  582. package/static/dist/assets/_app.memory-b6jfivt6.js +0 -1
  583. package/static/dist/assets/_app.profiles-lpvixmqp.js +0 -1
  584. package/static/dist/assets/_app.settings-vowx3iq0.js +0 -1
  585. package/static/dist/assets/_app.settings._section-cnntg1tu.js +0 -3
  586. package/static/dist/assets/_app.skills-fb57lpau.js +0 -1
  587. package/static/dist/assets/_app.tasks-eszpx1q7.js +0 -2
  588. package/static/dist/assets/_app.todos-nr05ac6w.js +0 -1
  589. package/static/dist/assets/_app.workspaces-d2luomu6.js +0 -1
  590. package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +0 -1
  591. package/static/dist/assets/back-bemaer09.js +0 -1
  592. package/static/dist/assets/cancel-drd0g6d9.js +0 -1
  593. package/static/dist/assets/channel-cpdgf867.js +0 -1
  594. package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +0 -1
  595. package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +0 -1
  596. package/static/dist/assets/copied-ima3pysb.js +0 -1
  597. package/static/dist/assets/endpoints-d33siokx.js +0 -64
  598. package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +0 -1
  599. package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +0 -1
  600. package/static/dist/assets/index-cxq58xb1.js +0 -11
  601. package/static/dist/assets/index-hgtx08n5.css +0 -2
  602. package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +0 -1
  603. package/static/dist/assets/link-jyxicdhg.js +0 -1
  604. package/static/dist/assets/login-ec21t39x.js +0 -1
  605. package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +0 -1
  606. package/static/dist/assets/onboarding-btnltvyz.js +0 -1
  607. package/static/dist/assets/onboarding_model_label-dp7xffoh.js +0 -1
  608. package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +0 -1
  609. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +0 -1
  610. package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +0 -1
  611. package/static/dist/assets/refresh-b4uams1i.js +0 -1
  612. package/static/dist/assets/saved-kg52csy7.js +0 -1
  613. package/static/dist/assets/settings_section_extensions_title-ej0iapxe.js +0 -1
  614. package/static/dist/assets/share._token-f97e94r0.js +0 -2
  615. package/static/dist/assets/sliders-horizontal-fj18it47.js +0 -1
  616. package/static/dist/assets/sse-fwpqtzbg.js +0 -1
  617. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +0 -1
  618. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +0 -8
  619. package/static/dist/assets/todoStore-eginf9h4.js +0 -1
  620. package/static/dist/assets/tts-lxdxnmjd.js +0 -1
  621. package/static/dist/assets/untitled-ovpfthg4.js +0 -1
  622. package/static/dist/assets/useForm-bcblv1rx.js +0 -1
  623. package/static/dist/assets/useLocale-fi5jb8ma.js +0 -1
@@ -7,12 +7,19 @@
7
7
  */
8
8
  import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
9
9
  import { DatabaseSync } from 'node:sqlite';
10
+ import { createHash } from 'node:crypto';
11
+ import { Worker } from 'node:worker_threads';
10
12
  import { str } from '../util.js';
13
+ import { stateDbMessageFingerprint, stateDbMessageKey } from './state-db-identity.js';
11
14
  import { MESSAGING_SOURCES } from './source-kind.js';
12
15
  import { looksLikeDefaultCliTitle, normalizeSourceName } from './titles.js';
13
16
  export const CLI_MIN_UNTITLED_USER_MESSAGE_COUNT = 2;
14
- const STATE_DB_BUSY_TIMEOUT_MS = 500;
17
+ /** The Agent's `_READ_BUSY_TIMEOUT_S` (TAL-890): a DELETE-journal commit by another process blocks readers across its fsyncs. */
18
+ const STATE_DB_BUSY_TIMEOUT_MS = 5000;
19
+ /** The gateway watcher's change check stays cheap; a lock there fails over to a full projection. */
20
+ const STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS = 500;
15
21
  const SOURCE_LABELS = {
22
+ bluebubbles: 'BlueBubbles', dingtalk: 'DingTalk', feishu: 'Feishu', qqbot: 'QQBot', yuanbao: 'Yuanbao', whatsapp_cloud: 'WhatsApp Cloud',
16
23
  acp: 'ACP', api_server: 'API', cli: 'CLI', cron: 'Cron', discord: 'Discord', email: 'Email', kanban: 'Kanban', wecom: 'WeCom', wecom_callback: 'WeCom Callback',
17
24
  slack: 'Slack', telegram: 'Telegram', tool: 'Tool', tui: 'TUI', webhook: 'Webhook', webui: 'WebUI', weixin: 'Weixin', matrix: 'Matrix', signal: 'Signal',
18
25
  };
@@ -89,6 +96,8 @@ export function isCliSessionRow(row) {
89
96
  export function isCliSessionRowVisible(row) {
90
97
  if (!isCliSessionRow(row))
91
98
  return true;
99
+ if (row._lineage_root_id && row._lineage_tip_id)
100
+ return true;
92
101
  const actual = positiveInt(row.actual_message_count);
93
102
  const messageCount = actual || positiveInt(row.message_count);
94
103
  if (messageCount <= 0)
@@ -107,9 +116,13 @@ export function isCliSessionRowVisible(row) {
107
116
  return countUserTurns(row) >= CLI_MIN_UNTITLED_USER_MESSAGE_COUNT;
108
117
  }
109
118
  // ── compression-chain projection ─────────────────────────────────────────
119
+ /** The Agent's `_CHAIN_CAP`: the most hops a walk along a compression lineage takes. Its visited check stops a cycle sooner. */
120
+ const COMPRESSION_CHAIN_CAP = 1000;
110
121
  function isContinuationSession(parent, child) {
111
122
  if (!parent || !child)
112
123
  return false;
124
+ if (lower(child.source) === 'tool' || explicitForkMarkers(child, true).length > 0)
125
+ return false;
113
126
  if (lower(child.session_source) === 'fork')
114
127
  return false;
115
128
  const parentSource = lower(parent.source);
@@ -202,6 +215,10 @@ export function projectAgentSessionRows(rows) {
202
215
  };
203
216
  const projected = [];
204
217
  for (const row of rows) {
218
+ if (row._lineage_root_id && row._lineage_tip_id) {
219
+ projected.push({ ...row });
220
+ continue;
221
+ }
205
222
  if (continuationChildIds.has(str(row.id)))
206
223
  continue;
207
224
  let segments = 1;
@@ -246,15 +263,175 @@ export function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOU
246
263
  const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
247
264
  return db;
248
265
  }
266
+ /** The table's columns; a failed read throws, so an unreadable database is never mistaken for an older schema (TAL-785). */
249
267
  function tableColumns(db, table) {
268
+ return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
269
+ }
270
+ const LOCAL_POLICY_PREFIX = 'gateway.local_policy.v1:';
271
+ /** Read-only equivalent of Agent validate_local_lineage. A receipt, never a reset edge alone, proves ownership. */
272
+ function localLineages(db, sid) {
273
+ const result = new Map();
274
+ if (!tableColumns(db, 'state_meta').has('value'))
275
+ return result;
276
+ const cols = tableColumns(db, 'sessions');
277
+ if (!['chat_id', 'user_id', 'session_key', 'model_config', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
278
+ return result;
279
+ const meta = db.prepare('SELECT value FROM state_meta WHERE key = ?');
280
+ const fetch = db.prepare('SELECT * FROM sessions WHERE id IN (SELECT value FROM json_each(?))');
281
+ const messageCols = tableColumns(db, 'messages');
282
+ const lastMessage = ['session_id', 'timestamp'].every((c) => messageCols.has(c)) ? '(SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id)' : 'NULL';
283
+ const activity = `MAX(COALESCE(${cols.has('last_activity_at') ? 's.last_activity_at' : 'NULL'}, 0), COALESCE(${lastMessage}, 0), COALESCE(s.started_at, 0))`;
284
+ // Agent _CHAIN_STEP_SQL prefers a compression child, then a live child, then recency and id.
285
+ const children = db.prepare(`SELECT s.* FROM sessions s WHERE s.parent_session_id = ? ORDER BY CASE WHEN s.end_reason = 'compression' THEN 0 WHEN s.ended_at IS NULL THEN 1 ELSE 2 END, ${activity} DESC, s.started_at DESC, s.id DESC`);
286
+ const explicitChild = (child, includeReset) => {
287
+ return lower(child.source) === 'tool' || lower(child.session_source) === 'fork'
288
+ || explicitForkMarkers(child, includeReset).length > 0;
289
+ };
290
+ const compressionChildren = (parent) => str(parent.end_reason) === 'compression'
291
+ ? children.all(str(parent.id)).filter((child) => !explicitChild(child, true)) : [];
292
+ const owner = sid ? db.prepare('SELECT chat_id FROM sessions WHERE id = ?').get(sid)?.chat_id || sid : null;
293
+ const receipts = owner
294
+ ? db.prepare('SELECT key, value FROM state_meta WHERE key = ?').all(`${LOCAL_POLICY_PREFIX}${str(owner)}`)
295
+ : db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`);
296
+ for (const saved of receipts) {
297
+ try {
298
+ if (saved.value.length > 1_000_000)
299
+ continue;
300
+ const receipt = JSON.parse(saved.value);
301
+ const owner = saved.key.slice(LOCAL_POLICY_PREFIX.length);
302
+ const ids = receipt.lineage;
303
+ const entry = receipt.entry;
304
+ if (receipt.session_id !== owner || !Array.isArray(ids) || ids.length < 2 || ids.length > COMPRESSION_CHAIN_CAP
305
+ || !ids.every((id) => typeof id === 'string' && Boolean(id)) || new Set(ids).size !== ids.length
306
+ || ids[0] !== owner || ids.at(-1) !== entry?.session_id)
307
+ continue;
308
+ if (!['profile_id', 'principal_id', 'request_id', 'route'].every((key) => typeof receipt[key] === 'string' && receipt[key]))
309
+ continue;
310
+ if ('legacy_session_id' in receipt) {
311
+ const legacy = meta.get(`gateway.local_legacy.v1:${owner}`);
312
+ if (receipt.legacy_session_id !== owner || !legacy)
313
+ continue;
314
+ const binding = JSON.parse(legacy.value);
315
+ if (!['session_id', 'profile_id', 'principal_id', 'legacy_session_id'].every((key) => binding[key] === receipt[key]))
316
+ continue;
317
+ }
318
+ else {
319
+ // Python json.dumps uses ensure_ascii=True, including UTF-16 surrogate pairs.
320
+ const identity = JSON.stringify([receipt.profile_id, receipt.principal_id, receipt.request_id]).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
321
+ if (owner !== `local-${createHash('sha256').update(identity).digest('hex')}`)
322
+ continue;
323
+ }
324
+ const byId = new Map(fetch.all(JSON.stringify(ids)).map((row) => [str(row.id), row]));
325
+ const rows = ids.map((id) => byId.get(id));
326
+ if (!rows.every((row) => Boolean(row) && row.chat_id === owner && row.user_id === receipt.principal_id && row.session_key === receipt.route))
327
+ continue;
328
+ let contextStart = 0;
329
+ let valid = true;
330
+ for (let i = 1; i < rows.length; i += 1) {
331
+ const parent = rows[i - 1], child = rows[i];
332
+ if (child.parent_session_id !== parent.id || explicitChild(child, false)) {
333
+ valid = false;
334
+ break;
335
+ }
336
+ if (parent.end_reason === 'session_reset') {
337
+ const config = jsonLoadsIfString(child.model_config);
338
+ if (!config || config._reset_from !== parent.id) {
339
+ valid = false;
340
+ break;
341
+ }
342
+ contextStart = i;
343
+ }
344
+ else {
345
+ const next = compressionChildren(parent);
346
+ if (next[0]?.id !== child.id) {
347
+ valid = false;
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ if (!valid || compressionChildren(rows.at(-1)).length)
353
+ continue;
354
+ const lineage = { owner, ids, context: ids.slice(contextStart), rows, title: [...rows].reverse().find((row) => row.title)?.title ?? null };
355
+ for (const id of ids)
356
+ result.set(id, lineage);
357
+ }
358
+ catch { /* Malformed receipts cannot attach history to another conversation. */ }
359
+ }
360
+ return result;
361
+ }
362
+ /** The validated receipt's current physical target, or the original id on old Agents / unproven metadata. */
363
+ export function stateDbLocalTarget(dbPath, sid, opts = {}) {
364
+ if (!existsSync(dbPath)) {
365
+ opts.onRead?.(sid);
366
+ return sid;
367
+ }
368
+ let db;
250
369
  try {
251
- return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
370
+ db = openStateDbReadonly(dbPath);
371
+ }
372
+ catch {
373
+ return sid;
374
+ }
375
+ try {
376
+ db.exec('BEGIN');
377
+ const target = localLineages(db, sid).get(sid)?.ids.at(-1) ?? sid;
378
+ opts.onRead?.(target);
379
+ return target;
252
380
  }
253
381
  catch {
254
- return new Set();
382
+ return sid;
383
+ }
384
+ finally {
385
+ db.close();
255
386
  }
256
387
  }
388
+ export const AGENT_STORAGE_OK = Object.freeze({ state: 'ok', reason: null });
389
+ /**
390
+ * The corrupt-storage reason when `error` is SQLite's `SQLITE_CORRUPT` or `SQLITE_NOTADB` (the Agent's
391
+ * `hermes_state_health`), else null. The message is matched too, since a listing worker failure crosses the thread
392
+ * boundary as its message alone.
393
+ */
394
+ export function stateDbCorruptReason(error) {
395
+ const code = error?.errcode;
396
+ const message = error instanceof Error ? error.message : String(error);
397
+ const corrupt = (typeof code === 'number' && [11, 26].includes(code & 0xff)) || /malformed|not a database/i.test(message);
398
+ return corrupt ? `Agent session storage (state.db) is damaged: ${message}. Agent sessions are unavailable until it is repaired or restored from a backup.` : null;
399
+ }
400
+ /** The storage state a failed `state.db` read reports: `corrupt` when SQLite says the file is damaged, else `unavailable`. */
401
+ export function stateDbReadFailure(error) {
402
+ const corrupt = stateDbCorruptReason(error);
403
+ if (corrupt)
404
+ return { state: 'corrupt', reason: corrupt };
405
+ return { state: 'unavailable', reason: `Agent session storage (state.db) could not be read: ${error instanceof Error ? error.message : String(error)}.` };
406
+ }
407
+ /**
408
+ * TAL-785: whether `state.db` can serve the session list: `corrupt` when SQLite reports the file damaged, `unavailable`
409
+ * when it cannot be read for another reason (permissions, a lock, an I/O error), `outdated` when a readable database
410
+ * has no `sessions.source` column. A missing file is `ok`. Full-text index damage is never probed, so it stays `ok`, as
411
+ * the Agent treats it.
412
+ */
413
+ export function stateDbStorage(dbPath) {
414
+ if (!existsSync(dbPath))
415
+ return AGENT_STORAGE_OK;
416
+ try {
417
+ const db = openStateDbReadonly(dbPath);
418
+ try {
419
+ if (!tableColumns(db, 'sessions').has('source'))
420
+ return { state: 'outdated', reason: `state.db has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this.` };
421
+ }
422
+ finally {
423
+ db.close();
424
+ }
425
+ }
426
+ catch (error) {
427
+ return stateDbReadFailure(error);
428
+ }
429
+ return AGENT_STORAGE_OK;
430
+ }
257
431
  const warnedNoSourceColumn = new Set();
432
+ let readDelayMs = 0;
433
+ /** Test hook: each listing read (and each listing worker request) blocks its thread this long first, standing in for a slow `state.db` (TAL-920). */
434
+ export function setStateDbReadDelayForTests(ms) { readDelayMs = ms; }
258
435
  /**
259
436
  * Python `read_importable_agent_session_rows`. The recency slice is bounded to
260
437
  * `limit * 8` candidates before the messages aggregate runs; subagent parents
@@ -269,8 +446,12 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
269
446
  const limit = opts.limit === undefined ? 200 : opts.limit;
270
447
  const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
271
448
  const include = opts.includeSources ?? null;
449
+ if (readDelayMs > 0)
450
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, readDelayMs);
272
451
  const db = openStateDbReadonly(dbPath);
273
452
  try {
453
+ db.exec('BEGIN'); // Receipt and physical rows must belong to the same committed generation.
454
+ const local = localLineages(db, opts.sessionIds?.length === 1 ? opts.sessionIds[0] : undefined);
274
455
  const sessionCols = tableColumns(db, 'sessions');
275
456
  const messageCols = tableColumns(db, 'messages');
276
457
  if (!sessionCols.has('source')) {
@@ -331,14 +512,26 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
331
512
  let candidateOrder = 'ORDER BY s.started_at DESC';
332
513
  const where = ['s.source IS NOT NULL'];
333
514
  const params = [];
515
+ if (opts.sessionIds !== undefined) {
516
+ where.push('s.id IN (SELECT value FROM json_each(?))');
517
+ params.push(JSON.stringify(opts.sessionIds.map((id) => local.get(id)?.ids.at(-1) ?? id)));
518
+ }
519
+ const superseded = [...local].filter(([id, lineage]) => id !== lineage.ids.at(-1)).map(([id]) => id);
520
+ if (superseded.length) {
521
+ where.push('s.id NOT IN (SELECT value FROM json_each(?))');
522
+ params.push(JSON.stringify(superseded));
523
+ }
524
+ const hasPolicies = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'state_meta'").get());
525
+ const policySource = (id) => `(SELECT CASE WHEN json_valid(value) THEN json_extract(value, '$.policy.source') END FROM state_meta WHERE key = '${LOCAL_POLICY_PREFIX}' || ${id})`;
526
+ const sourceSql = hasPolicies ? `COALESCE(${policySource('s.id')}, ${sessionCols.has('chat_id') ? policySource('s.chat_id') : 'NULL'}, s.source)` : 's.source';
334
527
  const included = (include ?? []).map(String).filter(Boolean);
335
528
  if (included.length) {
336
- where.push(`s.source IN (${included.map(() => '?').join(', ')})`);
529
+ where.push(`${sourceSql} IN (${included.map(() => '?').join(', ')})`);
337
530
  params.push(...included);
338
531
  }
339
532
  const excluded = (exclude ?? []).map(String).filter(Boolean);
340
533
  if (excluded.length) {
341
- where.push(`s.source NOT IN (${excluded.map(() => '?').join(', ')})`);
534
+ where.push(`${sourceSql} NOT IN (${excluded.map(() => '?').join(', ')})`);
342
535
  params.push(...excluded);
343
536
  }
344
537
  const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
@@ -351,7 +544,7 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
351
544
  orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
352
545
  candidateOrder = 'ORDER BY COALESCE((SELECT MAX(mx.timestamp) FROM messages mx WHERE mx.session_id = s.id), s.started_at) DESC, s.started_at DESC';
353
546
  }
354
- const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['session_source', 'user_id', 'chat_id', 'chat_type', 'thread_id', 'session_key', 'origin_chat_id', 'origin_user_id', 'platform', 'parent_session_id', 'ended_at', 'end_reason'].map(col).join(', ')}, ${actualCountExpr} AS actual_message_count, ${userCountExpr} AS actual_user_message_count, ${lastActivityExpr} AS last_activity`;
547
+ const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['model_config', 'session_source', 'user_id', 'chat_id', 'chat_type', 'thread_id', 'session_key', 'origin_chat_id', 'origin_user_id', 'platform', 'parent_session_id', 'ended_at', 'end_reason'].map(col).join(', ')}, ${actualCountExpr} AS actual_message_count, ${userCountExpr} AS actual_user_message_count, ${lastActivityExpr} AS last_activity`;
355
548
  let raw;
356
549
  if (limit !== null) {
357
550
  const resultLimit = Math.max(0, Math.trunc(limit));
@@ -366,8 +559,21 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
366
559
  else {
367
560
  raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
368
561
  }
369
- let projected = projectAgentSessionRows(raw.map((r) => ({ ...r })));
370
- projected = projected.map((r) => ({ ...r, ...normalizeAgentSessionSource(r.source) }));
562
+ const owners = sessionSources(db, sessionCols, raw.map((r) => str(r.id)));
563
+ raw = raw.map((r) => { const owner = owners.get(str(r.id)); return { ...r, source: typeof owner === 'object' ? owner.source : owner || r.source }; });
564
+ let projected = projectAgentSessionRows(raw.map((r) => {
565
+ const lineage = local.get(str(r.id));
566
+ if (!lineage)
567
+ return { ...r };
568
+ const root = lineage.rows[0];
569
+ const title = lineage.title || r.title;
570
+ return { ...r, title, started_at: root.started_at, last_activity: r.last_activity ?? r.started_at, parent_session_id: root.parent_session_id,
571
+ _lineage_root_id: lineage.owner, _lineage_tip_id: r.id, _lineage_ids: [...lineage.ids], _compression_segment_count: lineage.ids.length };
572
+ }));
573
+ projected = projected.map((r) => {
574
+ const parent = local.get(str(r.parent_session_id));
575
+ return { ...r, ...normalizeAgentSessionSource(r.source), ...(parent ? { _parent_lineage_root_id: parent.owner, parent_title: parent.title, parent_source: parent.rows.at(-1)?.source ?? null } : {}) };
576
+ });
371
577
  projected = projected.filter(isCliSessionRowVisible);
372
578
  if (limit === null)
373
579
  return projected;
@@ -469,15 +675,138 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
469
675
  db.close();
470
676
  }
471
677
  }
472
- /**
473
- * TAL-358: the `sessions.source` owner of each present id, in one chunked read. A missing state.db (or one without a
474
- * `source` column) owns nothing; an unreadable one answers null, so callers fail closed on an unknown owner.
475
- */
476
- export function stateDbSessionSources(dbPath, sessionIds) {
678
+ /** A source label remains display metadata; canonical receipts and ledgers independently retain write ownership. */
679
+ export const CANONICAL_READ_ONLY_REASON = 'This session is owned by the Agent gateway and is display-only in Talaria.';
680
+ export const OWNERSHIP_UNAVAILABLE_REASON = 'Agent session ownership could not be verified. The session is read-only until storage is readable.';
681
+ export const RETIRED_READ_ONLY_REASON = 'The Agent has retired this session identity. Open or import a new conversation to continue.';
682
+ /** Ownership fences outlive the fully validated localLineages history: even a pruned or broken receipt cannot release a writer. */
683
+ function sessionSources(db, cols, ids) {
477
684
  const sources = new Map();
685
+ const canReadRows = cols.has('id') && cols.has('source');
686
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
687
+ const walk = canReadRows ? resumeLineageWalker(db, cols) : (sid) => [sid];
688
+ const lineages = new Map(ids.map((id) => [id, walk(id)]));
689
+ const segments = [...new Set([...lineages.values()].flat())];
690
+ const rows = new Map();
691
+ for (let i = 0; canReadRows && i < segments.length; i += 400) {
692
+ const chunk = segments.slice(i, i + 400);
693
+ for (const row of db.prepare(`SELECT id, source, ${cols.has('chat_id') ? 'chat_id' : 'NULL AS chat_id'} FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
694
+ rows.set(str(row.id), row);
695
+ }
696
+ const candidates = [...new Set([...segments, ...[...rows.values()].map((r) => str(r.chat_id)).filter(Boolean)])];
697
+ const candidateSet = new Set(candidates);
698
+ const policies = new Map();
699
+ const receiptOwners = new Map();
700
+ const retired = new Set();
701
+ const bindings = new Set();
702
+ const wanted = new Set(segments);
703
+ const policyValue = (value) => {
704
+ try {
705
+ const parsed = JSON.parse(str(value));
706
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
707
+ }
708
+ catch {
709
+ return null;
710
+ }
711
+ };
712
+ const rememberPolicy = (owner, policy) => {
713
+ const entry = policy?.entry;
714
+ const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
715
+ const lineage = Array.isArray(policy?.lineage) ? policy.lineage : [];
716
+ const aliases = [owner, policy?.session_id, policy?.legacy_session_id, target, ...lineage];
717
+ if (!candidateSet.has(owner) && !aliases.some((id) => typeof id === 'string' && wanted.has(id)))
718
+ return;
719
+ policies.set(owner, policy);
720
+ for (const id of aliases)
721
+ if (typeof id === 'string' && wanted.has(id) && !receiptOwners.has(id))
722
+ receiptOwners.set(id, owner);
723
+ };
724
+ if (tables.has('state_meta')) {
725
+ const read = db.prepare('SELECT value FROM state_meta WHERE key = ?');
726
+ for (const owner of candidates) {
727
+ const raw = read.get(`${LOCAL_POLICY_PREFIX}${owner}`);
728
+ if (raw)
729
+ rememberPolicy(owner, policyValue(raw.value));
730
+ if (read.get(`gateway.retired_session.v1.${owner}`))
731
+ retired.add(owner);
732
+ if (read.get(`gateway.api.binding.v1.${owner}`))
733
+ bindings.add(owner);
734
+ }
735
+ }
736
+ const ledgerIds = new Set();
737
+ for (const [table, column] of [['session_admissions', 'target_session_id'], ['worker_executions', 'session_id']]) {
738
+ if (!tables.has(table))
739
+ continue;
740
+ const hasLineage = tableColumns(db, table).has('lineage_json');
741
+ for (let i = 0; i < candidates.length; i += 400) {
742
+ const chunk = candidates.slice(i, i + 400);
743
+ const marks = chunk.map(() => '?').join(',');
744
+ const linked = hasLineage ? ` OR EXISTS (SELECT 1 FROM json_each(lineage_json) WHERE value IN (${marks}))` : '';
745
+ const found = db.prepare(`SELECT ${column} AS id, ${hasLineage ? 'lineage_json' : 'NULL AS lineage_json'} FROM ${table} WHERE ${column} IN (${marks})${linked}`).all(...chunk, ...(hasLineage ? chunk : []));
746
+ for (const row of found) {
747
+ ledgerIds.add(row.id);
748
+ if (row.lineage_json !== null) {
749
+ const lineage = JSON.parse(row.lineage_json);
750
+ if (!Array.isArray(lineage) || !lineage.every((v) => typeof v === 'string'))
751
+ throw new Error('Unreadable Agent admission lineage');
752
+ for (const sid of lineage)
753
+ ledgerIds.add(sid);
754
+ }
755
+ }
756
+ }
757
+ }
758
+ // Pruned/reset segments can survive only in a receipt, without a row or chat_id to follow.
759
+ if (tables.has('state_meta') && segments.some((id) => !receiptOwners.has(id) && !ledgerIds.has(id) && !retired.has(id) && !bindings.has(id))) {
760
+ for (const raw of db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`)) {
761
+ const policy = policyValue(raw.value);
762
+ const entry = policy?.entry;
763
+ const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
764
+ if (!policy || typeof policy.session_id !== 'string' || typeof target !== 'string' || (policy.lineage !== undefined && (!Array.isArray(policy.lineage) || !policy.lineage.length || !policy.lineage.every((id) => typeof id === 'string'))))
765
+ throw new Error('Unreadable Agent ownership receipt');
766
+ const owner = raw.key.slice(LOCAL_POLICY_PREFIX.length);
767
+ if (!owner)
768
+ throw new Error('Unreadable Agent ownership receipt');
769
+ rememberPolicy(owner, policy);
770
+ }
771
+ }
772
+ const kanban = new Set(kanbanOwnedIds(db, cols, ids.filter((id) => rows.has(id))));
773
+ for (const id of ids) {
774
+ const row = rows.get(id);
775
+ let source = kanban.has(id) ? 'kanban' : str(row?.source);
776
+ let canonical = false;
777
+ let retiredId = false;
778
+ for (const segment of lineages.get(id) ?? [id]) {
779
+ const chatId = str(rows.get(segment)?.chat_id);
780
+ for (const owner of new Set([segment, chatId, receiptOwners.get(segment)].filter((v) => Boolean(v)))) {
781
+ if (retired.has(owner))
782
+ retiredId = true;
783
+ if (ledgerIds.has(owner) || bindings.has(owner))
784
+ canonical = true;
785
+ if (!policies.has(owner))
786
+ continue;
787
+ const policy = policies.get(owner);
788
+ // A broken receipt never releases its owner or a physical row naming that owner.
789
+ canonical = true;
790
+ const origin = policy?.policy;
791
+ if (origin && typeof origin === 'object' && !Array.isArray(origin) && str(origin.source))
792
+ source = str(origin.source);
793
+ }
794
+ }
795
+ const authority = canonical;
796
+ // LOCAL is the canonical transport. Missing receipts cannot prove it safe to claim.
797
+ if (lower(source) === 'local')
798
+ canonical = true;
799
+ if (!row && !canonical && !retiredId)
800
+ continue;
801
+ sources.set(id, retiredId && !authority ? { source, canonical: true, reason: RETIRED_READ_ONLY_REASON } : canonical ? { source, canonical: true } : source);
802
+ }
803
+ return sources;
804
+ }
805
+ /** Batched source/ownership probe. An unreadable authority answers null so callers fail closed. */
806
+ export function stateDbSessionSources(dbPath, sessionIds) {
478
807
  const ids = [...new Set([...sessionIds].map((s) => s.trim()).filter(Boolean))];
479
808
  if (!ids.length || !existsSync(dbPath))
480
- return sources;
809
+ return new Map();
481
810
  let db;
482
811
  try {
483
812
  db = openStateDbReadonly(dbPath);
@@ -486,15 +815,8 @@ export function stateDbSessionSources(dbPath, sessionIds) {
486
815
  return null;
487
816
  }
488
817
  try {
489
- const cols = tableColumns(db, 'sessions');
490
- if (!cols.has('id') || !cols.has('source'))
491
- return sources;
492
- for (let i = 0; i < ids.length; i += 500) {
493
- const chunk = ids.slice(i, i + 500);
494
- for (const row of db.prepare(`SELECT id, source FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
495
- sources.set(row.id, str(row.source));
496
- }
497
- return sources;
818
+ db.exec('BEGIN');
819
+ return sessionSources(db, tableColumns(db, 'sessions'), ids);
498
820
  }
499
821
  catch {
500
822
  return null;
@@ -503,6 +825,76 @@ export function stateDbSessionSources(dbPath, sessionIds) {
503
825
  db.close();
504
826
  }
505
827
  }
828
+ /** The same marker parser serves conservative history boundaries and exact-parent ownership walks. */
829
+ function explicitForkMarkers(row, includeReset) {
830
+ let cfg = row.model_config;
831
+ if (typeof cfg === 'string') {
832
+ try {
833
+ cfg = JSON.parse(cfg);
834
+ }
835
+ catch {
836
+ return [];
837
+ }
838
+ }
839
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
840
+ return [];
841
+ const c = cfg;
842
+ return [c._branched_from, c._delegate_from, ...(includeReset ? [c._reset_from] : [])].filter((m) => m !== undefined && m !== null);
843
+ }
844
+ /** Python `_is_explicit_fork_child_row`: a tool child, or a branch/delegate (`includeReset`: also reset) fork of its parent. */
845
+ function isExplicitForkChild(row, includeReset) {
846
+ if (str(row.source) === 'tool')
847
+ return true;
848
+ const markers = explicitForkMarkers(row, includeReset);
849
+ const parentId = row.parent_session_id;
850
+ return parentId ? markers.includes(parentId) : markers.length > 0;
851
+ }
852
+ /**
853
+ * Python `_resume_lineage_ids`: `sid` and the compression ancestors a resume loads, root first. Each hop's parent ended
854
+ * `compression` and the child is no branch, reset, delegate or tool fork. The walk stops at a cycle or the Agent's
855
+ * 1000-hop `_CHAIN_CAP`; without the lineage columns it is `[sid]`.
856
+ */
857
+ function resumeLineageWalker(db, cols) {
858
+ if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
859
+ return (sid) => [sid];
860
+ const select = db.prepare(`SELECT id, source, parent_session_id, end_reason, ${cols.has('model_config') ? 'model_config' : 'NULL AS model_config'} FROM sessions WHERE id = ?`);
861
+ return (sid) => {
862
+ const chain = [sid];
863
+ let current = select.get(sid);
864
+ if (!current || isExplicitForkChild(current, false))
865
+ return chain;
866
+ while (chain.length < 1000) {
867
+ const parentId = str(current.parent_session_id);
868
+ if (!parentId || chain.includes(parentId) || isExplicitForkChild(current, true))
869
+ break;
870
+ const parent = select.get(parentId);
871
+ if (str(parent?.end_reason) !== 'compression')
872
+ break;
873
+ chain.push(parentId);
874
+ current = parent;
875
+ }
876
+ return chain.reverse();
877
+ };
878
+ }
879
+ /**
880
+ * TAL-793 (Agent `is_kanban_owned_session`): the `ids` whose compression lineage has a `kanban` `source` or
881
+ * `created_source` (schema v31; `source` alone decides before it), a Kanban worker's transcript only its dispatcher continues.
882
+ */
883
+ function kanbanOwnedIds(db, cols, ids) {
884
+ if (!ids.length)
885
+ return [];
886
+ const walk = resumeLineageWalker(db, cols);
887
+ const lineages = ids.map(walk);
888
+ const segments = [...new Set(lineages.flat())];
889
+ const match = cols.has('created_source') ? "(source = 'kanban' OR created_source = 'kanban')" : "source = 'kanban'";
890
+ const kanban = new Set();
891
+ for (let i = 0; i < segments.length; i += 500) {
892
+ const chunk = segments.slice(i, i + 500);
893
+ for (const row of db.prepare(`SELECT id FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')}) AND ${match}`).all(...chunk))
894
+ kanban.add(row.id);
895
+ }
896
+ return ids.filter((_, i) => lineages[i].some((sid) => kanban.has(sid)));
897
+ }
506
898
  /** Python `state_db_has_session`: true only when `sid` is a row of the sessions table; a missing or unreadable db is false. */
507
899
  export function stateDbHasSession(dbPath, sid) {
508
900
  const id = sid.trim();
@@ -535,7 +927,7 @@ export function stateDbHasSession(dbPath, sid) {
535
927
  export function cheapChangeFingerprint(dbPath, onError) {
536
928
  try {
537
929
  const parts = [];
538
- const db = openStateDbReadonly(dbPath);
930
+ const db = openStateDbReadonly(dbPath, STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS);
539
931
  try {
540
932
  for (const table of ['sessions', 'messages']) {
541
933
  try {
@@ -579,19 +971,143 @@ export function cheapChangeFingerprint(dbPath, onError) {
579
971
  return null;
580
972
  }
581
973
  }
974
+ let listingWorker = null;
975
+ let nextCallId = 0;
976
+ /** Set while `stopStateDbWorker` waits for the worker to exit; calls then reject instead of starting another worker. */
977
+ let stoppingWorker = false;
978
+ /** Reject every call still waiting on `current` and forget it, so the next call starts a fresh worker. */
979
+ function retireListingWorker(current, error) {
980
+ if (listingWorker === current)
981
+ listingWorker = null;
982
+ for (const call of current.pending.values())
983
+ call.reject(error);
984
+ current.pending.clear();
985
+ }
986
+ /** The lazily started, long-lived listing worker; requests run one at a time in arrival order. */
987
+ function startedListingWorker() {
988
+ if (listingWorker)
989
+ return listingWorker;
990
+ // Source runs (vitest) load the TypeScript worker; the build runs the compiled one beside this module.
991
+ const ext = import.meta.url.endsWith('.ts') ? 'ts' : 'js';
992
+ const current = { worker: new Worker(new URL(`./state-db-worker.${ext}`, import.meta.url)), pending: new Map() };
993
+ current.worker.unref();
994
+ current.worker.on('message', (reply) => {
995
+ const call = current.pending.get(reply.id);
996
+ if (!call)
997
+ return;
998
+ current.pending.delete(reply.id);
999
+ if (!current.pending.size)
1000
+ current.worker.unref();
1001
+ call.resolve(reply);
1002
+ });
1003
+ current.worker.on('error', (error) => { retireListingWorker(current, error); });
1004
+ current.worker.on('exit', (code) => { retireListingWorker(current, new Error(`state.db worker exited with code ${String(code)}`)); });
1005
+ listingWorker = current;
1006
+ return current;
1007
+ }
1008
+ function callListingWorker(call) {
1009
+ // A multi-read caller the stop interrupted goes on to its next read; that read must not restart the worker.
1010
+ if (stoppingWorker)
1011
+ return Promise.reject(new Error('state.db worker stopped'));
1012
+ const current = startedListingWorker();
1013
+ const id = ++nextCallId;
1014
+ return new Promise((resolve, reject) => {
1015
+ current.pending.set(id, { resolve, reject });
1016
+ // A call in flight keeps the process alive until it settles; an idle worker never does.
1017
+ current.worker.ref();
1018
+ current.worker.postMessage({ ...call, id, delayMs: readDelayMs });
1019
+ });
1020
+ }
1021
+ /** `readImportableAgentSessionRows` on the listing worker, so a slow `state.db` never blocks the event loop. Rejects when the worker fails. */
1022
+ export async function readImportableAgentSessionRowsAsync(dbPath, opts = {}) {
1023
+ const { log, ...query } = opts;
1024
+ const reply = await callListingWorker({ op: 'rows', dbPath, opts: query });
1025
+ for (const line of reply.logs)
1026
+ log?.(line);
1027
+ if (reply.error !== undefined)
1028
+ throw new Error(reply.error);
1029
+ return reply.value;
1030
+ }
1031
+ /** `cheapChangeFingerprint` on the listing worker; a worker failure reports as a failed change check. */
1032
+ export async function cheapChangeFingerprintAsync(dbPath, onError) {
1033
+ let reply;
1034
+ try {
1035
+ reply = await callListingWorker({ op: 'fingerprint', dbPath });
1036
+ }
1037
+ catch (error) {
1038
+ onError?.('change check failed', error);
1039
+ return null;
1040
+ }
1041
+ for (const [reason, message] of reply.failures)
1042
+ onError?.(reason, new Error(message));
1043
+ return reply.value;
1044
+ }
1045
+ /** `stateDbStorage` on the listing worker; a worker failure is `unavailable`. */
1046
+ export async function stateDbStorageAsync(dbPath) {
1047
+ try {
1048
+ return (await callListingWorker({ op: 'storage', dbPath })).value;
1049
+ }
1050
+ catch (error) {
1051
+ return stateDbReadFailure(error);
1052
+ }
1053
+ }
1054
+ /** `stateDbSessionSources` on the listing worker; a worker failure is an unreadable state.db (null), so owners stay locked. */
1055
+ export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
1056
+ try {
1057
+ return (await callListingWorker({ op: 'sources', dbPath, ids: [...sessionIds] })).value;
1058
+ }
1059
+ catch {
1060
+ return null;
1061
+ }
1062
+ }
1063
+ /** `agentSessionRowsExisting` on the listing worker; a worker failure assumes every row present, so nothing is pruned. */
1064
+ export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
1065
+ const ids = [...sessionIds];
1066
+ try {
1067
+ return (await callListingWorker({ op: 'existing', dbPath, ids })).value;
1068
+ }
1069
+ catch {
1070
+ return new Set(ids.map((s) => s.trim()).filter(Boolean));
1071
+ }
1072
+ }
1073
+ /** Test hook: the listing worker exits abruptly while this call is in flight. */
1074
+ export function crashStateDbWorkerForTests() {
1075
+ return callListingWorker({ op: 'crash' });
1076
+ }
1077
+ /** Stop the listing worker (server shutdown). Calls in flight reject; a later call starts a new worker. */
1078
+ export async function stopStateDbWorker() {
1079
+ const current = listingWorker;
1080
+ if (!current)
1081
+ return;
1082
+ stoppingWorker = true;
1083
+ try {
1084
+ retireListingWorker(current, new Error('state.db worker stopped'));
1085
+ await current.worker.terminate();
1086
+ }
1087
+ finally {
1088
+ stoppingWorker = false;
1089
+ }
1090
+ }
582
1091
  // ── message reader ────────────────────────────────────────────────────────
583
1092
  const STATE_DB_CONTENT_JSON_PREFIX = '\0json:';
584
1093
  // `display_kind` / `display_metadata`: the Agent's own tag for rows it writes itself (steers, delivered notifications; TAL-371).
585
- const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata'];
586
- const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata']);
1094
+ const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata', 'absorbed_message_uids'];
1095
+ const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata', 'absorbed_message_uids']);
587
1096
  /** Python `_decode_state_db_content`: the Agent's sentinel-prefixed structured content, left untouched otherwise. */
588
1097
  function decodeStateDbContent(value) {
589
1098
  const text = Buffer.isBuffer(value) ? value.toString('utf8') : value;
590
1099
  if (typeof text !== 'string' || !text.startsWith(STATE_DB_CONTENT_JSON_PREFIX))
591
1100
  return text;
592
1101
  try {
593
- const decoded = JSON.parse(text.slice(STATE_DB_CONTENT_JSON_PREFIX.length));
594
- return Array.isArray(decoded) ? decoded : text;
1102
+ const payload = text.slice(STATE_DB_CONTENT_JSON_PREFIX.length);
1103
+ let unsafeInteger = false;
1104
+ const decoded = JSON.parse(payload, (_key, value, context) => {
1105
+ if (typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context?.source ?? ''))
1106
+ unsafeInteger = true;
1107
+ return value;
1108
+ });
1109
+ // Python keeps arbitrary-precision integers. Retain the JSON text rather than silently rounding any of them.
1110
+ return unsafeInteger ? payload : decoded;
595
1111
  }
596
1112
  catch {
597
1113
  return text;
@@ -616,11 +1132,13 @@ function projectStateDbMessage(row, hasId) {
616
1132
  const value = row[col];
617
1133
  if (value === null || value === undefined || value === '')
618
1134
  continue;
619
- msg[col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
1135
+ msg[col === 'absorbed_message_uids' ? '_absorbed_message_uids' : col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
620
1136
  }
621
1137
  // TAL-493: the merge tells rows committed after the session's last read by this id.
622
1138
  if (hasId && row.id !== null && row.id !== undefined)
623
1139
  msg._state_db_row_id = row.id;
1140
+ if (typeof row.message_uid === 'string' && row.message_uid.length)
1141
+ msg._state_db_message_uid = row.message_uid;
624
1142
  if (msg.role === 'tool' && msg.tool_name && !msg.name)
625
1143
  msg.name = msg.tool_name;
626
1144
  return msg;
@@ -639,6 +1157,74 @@ function stateDbGeneration(dbPath) {
639
1157
  return null;
640
1158
  }
641
1159
  }
1160
+ // Agent v0.21.6's exact in-flight request wrapper. Unknown rewrites are not copy evidence.
1161
+ const INFLIGHT_REPLAY_HEADER = '[STILL IN PROGRESS — this is the active request, restated after the compaction boundary because it was not finished yet. Continue it; do not start over.]';
1162
+ /** The payload comparison ignores only the Agent's documented replay wrapper and dropped api_content. */
1163
+ function replayPayload(row) {
1164
+ const message = projectStateDbMessage(row, false);
1165
+ delete message.timestamp;
1166
+ const copyKey = JSON.stringify(message);
1167
+ let content = message.content;
1168
+ let replay = false;
1169
+ if (row.role === 'user') {
1170
+ if (typeof content === 'string') {
1171
+ replay = content.startsWith(`${INFLIGHT_REPLAY_HEADER}\n`);
1172
+ content = (replay ? content.slice(INFLIGHT_REPLAY_HEADER.length + 1) : content).trim();
1173
+ }
1174
+ else if (Array.isArray(content)) {
1175
+ // Multimodal replays prepend a text block and retain all original parts, including media.
1176
+ const parts = content;
1177
+ let start = 0;
1178
+ while (start < parts.length) {
1179
+ const first = parts[start];
1180
+ if (!first || typeof first !== 'object' || Array.isArray(first))
1181
+ break;
1182
+ const part = first;
1183
+ if (part.type !== 'text' || part.text !== `${INFLIGHT_REPLAY_HEADER}\n` || Object.keys(part).some((k) => k !== 'type' && k !== 'text'))
1184
+ break;
1185
+ start += 1;
1186
+ }
1187
+ replay = start > 0;
1188
+ content = parts.slice(start);
1189
+ }
1190
+ }
1191
+ message.content = content;
1192
+ delete message.api_content; // _reappend_inflight_user_task drops this stale model-facing sidecar.
1193
+ return { copyKey, replayKey: JSON.stringify(message), replay };
1194
+ }
1195
+ /**
1196
+ * Compare adjacent generations, never all rows of one UID with its oldest archive. The archive retains
1197
+ * authored payload changes even after active/compacted flags change. Only unchanged timestamp copies or
1198
+ * exact request restatements inherit identity; edits (including A -> B -> A) start at their own row ID.
1199
+ */
1200
+ function replayIdentities(db, chain, rows, available) {
1201
+ const identities = new Map();
1202
+ // Tool-result text can be pruned without changing its tool-call identity; retain the existing rule.
1203
+ const uids = [...new Set(rows.filter((row) => row.role !== 'tool').map((row) => row.message_uid).filter((uid) => typeof uid === 'string' && uid.length > 0))];
1204
+ if (!uids.length)
1205
+ return identities;
1206
+ const liveIds = new Set(rows.map((row) => row.id));
1207
+ const previous = new Map();
1208
+ const columns = ['id', 'message_uid', 'active', 'compacted', 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
1209
+ // One ordered scan of the selected sessions, filtering against a materialized UID set. Stream the rows;
1210
+ // memory holds only the preceding payload per live UID, not the accumulated archive. NULL/nonstandard
1211
+ // active flags take exactly the same path and do not depend on the Agent's partial active=1 UID index.
1212
+ const history = db.prepare(`SELECT ${columns.join(', ')} FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND message_uid IN (SELECT value FROM json_each(?)) ORDER BY id`);
1213
+ for (const row of history.iterate(...chain, JSON.stringify(uids))) {
1214
+ const uid = row.message_uid;
1215
+ const payload = replayPayload(row);
1216
+ const prior = previous.get(uid);
1217
+ const copy = payload.copyKey === prior?.copyKey && row.timestamp === prior.timestamp;
1218
+ const restatement = prior && payload.replay && payload.replayKey === prior.replayKey && (prior.active !== 0 || prior.compacted === 1);
1219
+ const identity = prior && (copy || restatement) ? prior.identity : Number(row.id);
1220
+ const changed = prior ? (copy || restatement ? prior.changed : true) : false;
1221
+ const sourceKey = prior && (copy || restatement) ? prior.sourceKey : stateDbMessageFingerprint(projectStateDbMessage(row, false));
1222
+ if (prior && row.role !== 'tool' && liveIds.has(row.id))
1223
+ identities.set(Number(row.id), { id: identity, changed, sourceKey });
1224
+ previous.set(uid, { ...payload, identity, changed, sourceKey, timestamp: row.timestamp, active: row.active, compacted: row.compacted });
1225
+ }
1226
+ return identities;
1227
+ }
642
1228
  export function stateDbSessionMessages(dbPath, sid, opts = {}) {
643
1229
  return stateDbSessionRead(dbPath, sid, opts).rows;
644
1230
  }
@@ -647,7 +1233,14 @@ export function stateDbSessionMessages(dbPath, sid, opts = {}) {
647
1233
  * (TAL-493: an empty id-capable read is a baseline of 0; a missing database or a failed read says nothing).
648
1234
  */
649
1235
  export function stateDbSessionRead(dbPath, sid, opts = {}) {
650
- const none = { rows: [], idCapable: false, ok: false };
1236
+ return readStateDbSession(dbPath, sid, opts);
1237
+ }
1238
+ /** The same snapshot and provenance projection, restricted to physical rows past the seen marker. */
1239
+ export function stateDbSessionTailRead(dbPath, sid, afterId, opts = {}) {
1240
+ return readStateDbSession(dbPath, sid, { ...opts, stitch: false, afterId });
1241
+ }
1242
+ function readStateDbSession(dbPath, sid, opts) {
1243
+ const none = { rows: [], idCapable: false, ok: false, reason: null, generation: null };
651
1244
  const id = sid.trim();
652
1245
  if (!id || !existsSync(dbPath))
653
1246
  return none;
@@ -659,20 +1252,23 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
659
1252
  return none;
660
1253
  }
661
1254
  try {
1255
+ db.exec('BEGIN'); // The live rows and their archived predecessors must describe one snapshot.
662
1256
  const available = tableColumns(db, 'messages');
663
1257
  if (!['role', 'content', 'timestamp'].every((c) => available.has(c)))
664
1258
  return { ...none, ok: available.size > 0 };
665
1259
  const hasId = available.has('id');
666
- const selected = [...(hasId ? ['id'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
1260
+ const selected = [...(hasId ? ['id'] : []), ...(available.has('message_uid') ? ['message_uid'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
667
1261
  // TAL-529: an explicit lineage (the session and its compression continuations) replaces the parent walk.
668
- const chain = opts.lineage?.length ? [...opts.lineage] : [id];
669
- if (!opts.lineage?.length && (opts.stitch ?? true)) {
1262
+ const local = localLineages(db, id).get(id);
1263
+ const chain = local ? (opts.stitch === false ? [local.ids.at(-1) ?? id] : [...local.context]) : opts.lineage?.length ? [...opts.lineage] : [id];
1264
+ if (!local && !opts.lineage?.length && (opts.stitch ?? true)) {
670
1265
  const sessionCols = tableColumns(db, 'sessions');
671
1266
  if (['parent_session_id', 'end_reason', 'started_at', 'source'].every((c) => sessionCols.has(c))) {
672
- const select = db.prepare('SELECT id, source, started_at, parent_session_id, ended_at, end_reason FROM sessions WHERE id = ?');
1267
+ const extra = ['model_config', 'session_source'].map((c) => sessionCols.has(c) ? c : `NULL AS ${c}`).join(', ');
1268
+ const select = db.prepare(`SELECT id, source, started_at, parent_session_id, ended_at, end_reason, ${extra} FROM sessions WHERE id = ?`);
673
1269
  let current = select.get(id);
674
1270
  const seen = new Set([id]);
675
- for (let hop = 0; hop < 20 && current; hop += 1) {
1271
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
676
1272
  const parentId = str(current.parent_session_id);
677
1273
  if (!parentId || seen.has(parentId))
678
1274
  break;
@@ -686,19 +1282,74 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
686
1282
  }
687
1283
  }
688
1284
  const activeClause = available.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
1285
+ if (opts.afterId !== undefined && !hasId)
1286
+ return { ...none, ok: true };
1287
+ const range = opts.afterId === undefined ? '' : ' AND id > ?';
1288
+ const rangeArgs = opts.afterId === undefined ? [] : [opts.afterId];
689
1289
  const order = hasId ? 'id' : 'timestamp';
690
- const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause} ORDER BY ${order} ASC`).all(...chain);
1290
+ const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause}${range} ORDER BY ${order} ASC`).all(...chain, ...rangeArgs);
691
1291
  const projected = rows.map((row) => projectStateDbMessage(row, hasId));
1292
+ // TAL-709: an in-place compaction archives the live rows and re-inserts each as a copy that keeps its role and timestamp
1293
+ // (content may be pruned). A copy takes the id of the oldest archived row it copies, so the merge sees a row it has
1294
+ // already read instead of a new one. Only a compaction archive (`compacted = 1`) vouches for a changed copy: a rewound
1295
+ // row (`compacted = 0`, also the compaction's carried tail) counts only for the same tool call or the same content, so
1296
+ // an edit that kept its timestamp stays new.
1297
+ if (hasId && available.has('active')) {
1298
+ // One indexed lookup per live row ((session_id, active, timestamp)): the archive is never loaded, however many
1299
+ // compactions it holds.
1300
+ const sameCall = available.has('tool_call_id') ? " AND COALESCE(a.tool_call_id, '') = COALESCE(m.tool_call_id, '')" : '';
1301
+ const vouched = [available.has('compacted') ? 'a.compacted = 1' : '', available.has('tool_call_id') ? 'm.tool_call_id IS NOT NULL' : '', 'a.content = m.content'].filter(Boolean).join(' OR ');
1302
+ const twins = db.prepare(`SELECT m.id AS id, MIN(a.id) AS twin FROM messages m JOIN messages a ON a.session_id = m.session_id AND a.active = 0 AND a.role = m.role AND a.timestamp = m.timestamp AND a.id < m.id${sameCall} WHERE m.session_id IN (${chain.map(() => '?').join(', ')}) AND (m.active IS NULL OR m.active != 0) AND (${vouched})${opts.afterId === undefined ? '' : ' AND m.id > ?'} GROUP BY m.id`).all(...chain, ...rangeArgs);
1303
+ const twinOf = new Map(twins.map((row) => [row.id, row.twin]));
1304
+ for (const [i, row] of rows.entries()) {
1305
+ const twin = twinOf.get(row.id);
1306
+ if (typeof twin === 'number')
1307
+ projected[i]._state_db_row_id = twin;
1308
+ }
1309
+ }
1310
+ if (hasId && ['message_uid', 'active', 'compacted'].every((c) => available.has(c))) {
1311
+ const identities = replayIdentities(db, local?.context ?? chain, rows, available);
1312
+ for (const [i, row] of rows.entries()) {
1313
+ const identity = identities.get(Number(row.id));
1314
+ // A known changed generation overrides even a coincidental legacy timestamp twin.
1315
+ if (identity !== undefined) {
1316
+ projected[i]._state_db_row_id = identity.id;
1317
+ if (identity.id !== row.id)
1318
+ projected[i]._state_db_source_key = identity.sourceKey;
1319
+ if (identity.changed)
1320
+ projected[i]._state_db_authored_change = true;
1321
+ }
1322
+ }
1323
+ }
692
1324
  // TAL-493: the database file's identity, so a marker taken in a replaced state.db is never applied to its successor.
1325
+ const generation = stateDbGeneration(dbPath);
693
1326
  if (hasId) {
694
- const generation = stateDbGeneration(dbPath);
695
1327
  for (const m of projected)
696
1328
  m._state_db_generation = generation;
697
1329
  }
698
- return { rows: projected, idCapable: hasId, ok: true };
1330
+ let anchor;
1331
+ if (opts.afterId !== undefined) {
1332
+ anchor = projected.find((m) => m._state_db_row_id === opts.afterId);
1333
+ if (!anchor) {
1334
+ const row = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id = ?${activeClause} LIMIT 1`).get(...chain, opts.afterId);
1335
+ if (row)
1336
+ anchor = { ...projectStateDbMessage(row, true), _state_db_generation: generation };
1337
+ }
1338
+ }
1339
+ let requiresFullRead = false;
1340
+ if (opts.afterId !== undefined && opts.unidentified) {
1341
+ const matches = projected.filter((m) => opts.unidentified.some((keys) => keys.has(stateDbMessageKey(m))));
1342
+ if (matches.length) {
1343
+ // Projection can normalize content/timestamps. A bounded role-key superset is conservative copy detection.
1344
+ const roles = [...new Set(matches.map((m) => m.role))];
1345
+ const covered = db.prepare(`SELECT 1 FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id <= ? AND role IN (SELECT value FROM json_each(?))${activeClause} LIMIT 1`).get(...chain, opts.afterId, JSON.stringify(roles));
1346
+ requiresFullRead = Boolean(covered) || available.has('message_uid');
1347
+ }
1348
+ }
1349
+ return { rows: projected, idCapable: hasId, ok: true, reason: null, generation, requiresFullRead, ...(anchor ? { anchor } : {}) };
699
1350
  }
700
- catch {
701
- return none;
1351
+ catch (error) {
1352
+ return { ...none, reason: stateDbReadFailure(error).reason };
702
1353
  }
703
1354
  finally {
704
1355
  db.close();
@@ -706,8 +1357,8 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
706
1357
  }
707
1358
  /**
708
1359
  * TAL-529: `sid` and each compression continuation the Agent rotated it to, down to the live tip (Python
709
- * `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or 20
710
- * hops. A missing or unreadable database is `[sid]`.
1360
+ * `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
1361
+ * `COMPRESSION_CHAIN_CAP` hops. A missing or unreadable database is `[sid]`.
711
1362
  */
712
1363
  export function stateDbCompressionLineage(dbPath, sid) {
713
1364
  const lineage = [sid.trim()];
@@ -721,14 +1372,19 @@ export function stateDbCompressionLineage(dbPath, sid) {
721
1372
  return lineage;
722
1373
  }
723
1374
  try {
1375
+ db.exec('BEGIN');
1376
+ const local = localLineages(db, lineage[0]).get(lineage[0]);
1377
+ if (local)
1378
+ return [...local.ids];
724
1379
  const sessionCols = tableColumns(db, 'sessions');
725
1380
  if (!['id', 'parent_session_id', 'end_reason', 'ended_at', 'started_at', 'source'].every((c) => sessionCols.has(c)))
726
1381
  return lineage;
727
1382
  const sessionSource = sessionCols.has('session_source') ? 'session_source' : 'NULL AS session_source';
728
- const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason`;
1383
+ const modelConfig = sessionCols.has('model_config') ? 'model_config' : 'NULL AS model_config';
1384
+ const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason, ${modelConfig}`;
729
1385
  let current = db.prepare(`SELECT ${cols} FROM sessions WHERE id = ?`).get(lineage[0]);
730
1386
  const children = db.prepare(`SELECT ${cols} FROM sessions WHERE parent_session_id = ?`);
731
- for (let hop = 0; hop < 20 && current; hop += 1) {
1387
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
732
1388
  const parent = current;
733
1389
  const next = children.all(str(parent.id)).filter((child) => isContinuationSession(parent, child));
734
1390
  if (next.length !== 1 || lineage.includes(str(next[0].id)))
@@ -745,6 +1401,93 @@ export function stateDbCompressionLineage(dbPath, sid) {
745
1401
  db.close();
746
1402
  }
747
1403
  }
1404
+ function lineageReportRow(row, role) {
1405
+ const ended = row.ended_at ?? null;
1406
+ return {
1407
+ session_id: row.id, role, title: row.title ?? null, source: row.source ?? null, started_at: row.started_at ?? null, updated_at: ended ?? row.started_at ?? null,
1408
+ end_reason: row.end_reason ?? null, active: ended === null, archived: false,
1409
+ };
1410
+ }
1411
+ const emptyLineageReport = (sid) => ({
1412
+ mutation: false, found: false, session_id: sid, lineage_key: sid, tip_session_id: sid, total_segments: 0, materialized_segments: 0, segments: [], children: [], manual_review: false,
1413
+ });
1414
+ /**
1415
+ * Python `read_session_lineage_report`: a read-only report of `sid`'s compression lineage. The walk follows up to
1416
+ * `maxHops` continuation parents (the tip first, the root last), then lists each segment's non-continuation children,
1417
+ * newest first. A cycle, a hop limit hit, or a continuation off the walked path marks `manual_review`. A missing,
1418
+ * unreadable, or too-old database, or an unknown id, answers `found: false`.
1419
+ */
1420
+ export function stateDbLineageReport(dbPath, rawSid, maxHops = COMPRESSION_CHAIN_CAP) {
1421
+ const sid = rawSid.trim();
1422
+ if (!sid || !existsSync(dbPath))
1423
+ return emptyLineageReport(sid);
1424
+ let db;
1425
+ try {
1426
+ db = openStateDbReadonly(dbPath);
1427
+ }
1428
+ catch {
1429
+ return emptyLineageReport(sid);
1430
+ }
1431
+ try {
1432
+ db.exec('BEGIN');
1433
+ const local = localLineages(db, sid).get(sid);
1434
+ const cols = tableColumns(db, 'sessions');
1435
+ if (!['id', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
1436
+ return emptyLineageReport(sid);
1437
+ const col = (name, fallback = 'NULL') => (cols.has(name) ? `s.${name}` : `${fallback} AS ${name}`);
1438
+ const select = `SELECT s.id, ${col('source')}, ${col('session_source')}, ${col('title')}, ${col('started_at', '0')}, s.parent_session_id, ${col('ended_at')}, s.end_reason FROM sessions s`;
1439
+ const byId = db.prepare(`${select} WHERE s.id = ?`);
1440
+ const fetch = (id) => (id ? byId.get(str(id)) ?? null : null);
1441
+ const target = fetch(sid);
1442
+ if (!target)
1443
+ return emptyLineageReport(sid);
1444
+ const segments = local ? [...local.rows].reverse().slice(0, Math.max(1, maxHops)) : [target];
1445
+ const seen = new Set(segments.map((row) => str(row.id)));
1446
+ let manualReview = local ? local.ids.length > maxHops : true;
1447
+ for (let hop = 0; !local && hop < maxHops; hop += 1) {
1448
+ const current = segments[segments.length - 1];
1449
+ const parentId = str(current.parent_session_id);
1450
+ const parent = fetch(parentId);
1451
+ if (!parent || seen.has(parentId)) {
1452
+ manualReview = Boolean(parentId && seen.has(parentId));
1453
+ break;
1454
+ }
1455
+ if (!isContinuationSession(parent, current)) {
1456
+ manualReview = false;
1457
+ break;
1458
+ }
1459
+ segments.push(parent);
1460
+ seen.add(parentId);
1461
+ }
1462
+ const segmentIds = segments.map((row) => str(row.id));
1463
+ const children = db.prepare(`${select} WHERE s.parent_session_id IN (${segmentIds.map(() => '?').join(', ')})`).all(...segmentIds);
1464
+ const childRows = [];
1465
+ for (const parent of segments) {
1466
+ const own = children.filter((child) => str(child.parent_session_id) === str(parent.id) && !seen.has(str(child.id)));
1467
+ own.sort((a, b) => (Number(b.started_at) || 0) - (Number(a.started_at) || 0));
1468
+ for (const child of own) {
1469
+ // A continuation off the walked path means a branched lineage or an older selected segment: flag it, list nothing.
1470
+ if (isContinuationSession(parent, child))
1471
+ manualReview = true;
1472
+ else
1473
+ childRows.push(child);
1474
+ }
1475
+ }
1476
+ return {
1477
+ mutation: false, found: true, session_id: sid, lineage_key: segments[segments.length - 1].id, tip_session_id: segments[0].id,
1478
+ total_segments: segments.length, materialized_segments: segments.length,
1479
+ segments: segments.map((row, idx) => lineageReportRow(row, idx === 0 ? 'tip' : 'hidden_segment')),
1480
+ children: childRows.map((row) => lineageReportRow(row, 'child_session')),
1481
+ manual_review: manualReview,
1482
+ };
1483
+ }
1484
+ catch {
1485
+ return emptyLineageReport(sid);
1486
+ }
1487
+ finally {
1488
+ db.close();
1489
+ }
1490
+ }
748
1491
  /** The `sessions` row for `sid` (source and lifecycle columns), or null. */
749
1492
  export function stateDbSessionRow(dbPath, sid) {
750
1493
  const id = sid.trim();
@@ -821,4 +1564,78 @@ export function insightsSessionRows(dbPath, cutoff) {
821
1564
  db.close();
822
1565
  }
823
1566
  }
1567
+ /** A state.db `timestamp` in unix seconds: a number, or an ISO-8601 string (Python `fromisoformat`); null when unreadable. */
1568
+ export function stateDbTimestampSeconds(value) {
1569
+ if (typeof value === 'number')
1570
+ return Number.isFinite(value) ? value : null;
1571
+ if (typeof value !== 'string' || !value.trim())
1572
+ return null;
1573
+ const ms = Date.parse(value.trim());
1574
+ return Number.isFinite(ms) ? ms / 1000 : null;
1575
+ }
1576
+ /** Python `CONVERSATION_ROUND_THRESHOLD`: the handoff dock is offered from this many rounds. */
1577
+ export const CONVERSATION_ROUND_THRESHOLD = 10;
1578
+ /**
1579
+ * Python `count_conversation_rounds` (TAL-258): a round is a user message answered by the assistant; consecutive user
1580
+ * messages merge into one. With `since`, only rows stamped after it count (an unreadable stamp still counts). A missing
1581
+ * or unreadable database is 0.
1582
+ */
1583
+ export function countConversationRounds(dbPath, sid, since = null) {
1584
+ if (!existsSync(dbPath))
1585
+ return 0;
1586
+ let rows;
1587
+ try {
1588
+ const db = openStateDbReadonly(dbPath);
1589
+ try {
1590
+ rows = db.prepare('SELECT role, timestamp FROM messages WHERE session_id = ? ORDER BY timestamp ASC').all(sid);
1591
+ }
1592
+ finally {
1593
+ db.close();
1594
+ }
1595
+ }
1596
+ catch {
1597
+ return 0;
1598
+ }
1599
+ let rounds = 0;
1600
+ let seenUser = false;
1601
+ let answered = false;
1602
+ for (const row of rows) {
1603
+ if (since !== null) {
1604
+ const ts = stateDbTimestampSeconds(row.timestamp);
1605
+ if (ts !== null && ts <= since)
1606
+ continue;
1607
+ }
1608
+ const role = lower(row.role);
1609
+ if (role === 'user') {
1610
+ if (seenUser && answered) {
1611
+ rounds += 1;
1612
+ answered = false;
1613
+ }
1614
+ seenUser = true;
1615
+ }
1616
+ else if (role === 'assistant' && seenUser) {
1617
+ answered = true;
1618
+ }
1619
+ }
1620
+ return seenUser && answered ? rounds + 1 : rounds;
1621
+ }
1622
+ /** The content of the session's newest row when that row is a `tool` row (the handoff marker's tail dedupe, TAL-258); else null. */
1623
+ export function stateDbTailToolContent(dbPath, sid) {
1624
+ if (!existsSync(dbPath))
1625
+ return null;
1626
+ try {
1627
+ const db = openStateDbReadonly(dbPath);
1628
+ try {
1629
+ const row = db.prepare('SELECT role, content FROM messages WHERE session_id = ? ORDER BY rowid DESC LIMIT 1').get(sid);
1630
+ const content = row?.role === 'tool' ? decodeStateDbContent(row.content) : null;
1631
+ return typeof content === 'string' ? content : null;
1632
+ }
1633
+ finally {
1634
+ db.close();
1635
+ }
1636
+ }
1637
+ catch {
1638
+ return null;
1639
+ }
1640
+ }
824
1641
  //# sourceMappingURL=state-db.js.map