@maudecode/talaria-web 1.18.0 → 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 (567) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.js +16 -16
  3. package/dist/api/auth-raw.js.map +1 -1
  4. package/dist/api/automation-raw.d.ts +2 -2
  5. package/dist/api/automation-raw.js +32 -32
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +102 -22
  8. package/dist/api/automation-router.js +15 -13
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1205 -34
  11. package/dist/api/chat-router.js +158 -78
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +2 -2
  14. package/dist/api/git-router.js +29 -29
  15. package/dist/api/git-router.js.map +1 -1
  16. package/dist/api/raw-routes.d.ts +3 -3
  17. package/dist/api/raw-routes.js +63 -61
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +6 -6
  20. package/dist/api/router.js +22 -22
  21. package/dist/api/router.js.map +1 -1
  22. package/dist/api/session-visibility.d.ts +4 -4
  23. package/dist/api/session-visibility.js +26 -16
  24. package/dist/api/session-visibility.js.map +1 -1
  25. package/dist/api/sessions-router.d.ts +1928 -124
  26. package/dist/api/sessions-router.js +177 -173
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +427 -9
  29. package/dist/api/settings-router.js +48 -32
  30. package/dist/api/settings-router.js.map +1 -1
  31. package/dist/api/sse-routes.d.ts +2 -2
  32. package/dist/api/sse-routes.js +63 -63
  33. package/dist/api/sse-routes.js.map +1 -1
  34. package/dist/api/tools-raw.d.ts +5 -0
  35. package/dist/api/tools-raw.js +58 -33
  36. package/dist/api/tools-raw.js.map +1 -1
  37. package/dist/api/tools-router.d.ts +663 -91
  38. package/dist/api/tools-router.js +309 -52
  39. package/dist/api/tools-router.js.map +1 -1
  40. package/dist/api/transcribe-stream.d.ts +16 -0
  41. package/dist/api/transcribe-stream.js +172 -0
  42. package/dist/api/transcribe-stream.js.map +1 -0
  43. package/dist/app.d.ts +4 -1
  44. package/dist/app.js +66 -47
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/gate.js +10 -9
  47. package/dist/auth/gate.js.map +1 -1
  48. package/dist/auth/passkeys.d.ts +15 -14
  49. package/dist/auth/passkeys.js +89 -91
  50. package/dist/auth/passkeys.js.map +1 -1
  51. package/dist/auth/store.d.ts +17 -7
  52. package/dist/auth/store.js +93 -52
  53. package/dist/auth/store.js.map +1 -1
  54. package/dist/bin/talaria-web.js +12 -81
  55. package/dist/bin/talaria-web.js.map +1 -1
  56. package/dist/cli/ctl.js +2 -1
  57. package/dist/cli/ctl.js.map +1 -1
  58. package/dist/cli/dotenv.d.ts +5 -0
  59. package/dist/cli/dotenv.js +14 -2
  60. package/dist/cli/dotenv.js.map +1 -1
  61. package/dist/cli/launcher.js +5 -5
  62. package/dist/cli/launcher.js.map +1 -1
  63. package/dist/cli/main.d.ts +1 -0
  64. package/dist/cli/main.js +85 -0
  65. package/dist/cli/main.js.map +1 -0
  66. package/dist/cli/supervise.d.ts +2 -0
  67. package/dist/cli/supervise.js +67 -4
  68. package/dist/cli/supervise.js.map +1 -1
  69. package/dist/config/agent-config.d.ts +34 -11
  70. package/dist/config/agent-config.js +113 -58
  71. package/dist/config/agent-config.js.map +1 -1
  72. package/dist/config.d.ts +2 -0
  73. package/dist/config.js +14 -5
  74. package/dist/config.js.map +1 -1
  75. package/dist/fs/atomic.d.ts +1 -0
  76. package/dist/fs/atomic.js +4 -1
  77. package/dist/fs/atomic.js.map +1 -1
  78. package/dist/fs/stat.d.ts +1 -0
  79. package/dist/fs/stat.js +11 -0
  80. package/dist/fs/stat.js.map +1 -0
  81. package/dist/http/context.d.ts +14 -9
  82. package/dist/http/context.js +29 -20
  83. package/dist/http/context.js.map +1 -1
  84. package/dist/onboarding.d.ts +2 -2
  85. package/dist/onboarding.js +10 -8
  86. package/dist/onboarding.js.map +1 -1
  87. package/dist/profiles/profiles.d.ts +10 -4
  88. package/dist/profiles/profiles.js +77 -30
  89. package/dist/profiles/profiles.js.map +1 -1
  90. package/dist/projects.d.ts +15 -6
  91. package/dist/projects.js +55 -47
  92. package/dist/projects.js.map +1 -1
  93. package/dist/providers/catalog.d.ts +78 -12
  94. package/dist/providers/catalog.js +445 -141
  95. package/dist/providers/catalog.js.map +1 -1
  96. package/dist/providers/env-file.d.ts +10 -3
  97. package/dist/providers/env-file.js +24 -75
  98. package/dist/providers/env-file.js.map +1 -1
  99. package/dist/providers/runtime-env.d.ts +4 -2
  100. package/dist/providers/runtime-env.js +9 -6
  101. package/dist/providers/runtime-env.js.map +1 -1
  102. package/dist/providers/tables.d.ts +3 -1
  103. package/dist/providers/tables.js +4 -2
  104. package/dist/providers/tables.js.map +1 -1
  105. package/dist/redact.js +18 -9
  106. package/dist/redact.js.map +1 -1
  107. package/dist/release.d.ts +7 -0
  108. package/dist/release.js +14 -0
  109. package/dist/release.js.map +1 -1
  110. package/dist/runtime.d.ts +5 -1
  111. package/dist/runtime.js +233 -90
  112. package/dist/runtime.js.map +1 -1
  113. package/dist/server.js +10 -0
  114. package/dist/server.js.map +1 -1
  115. package/dist/sessions/anchor.d.ts +9 -3
  116. package/dist/sessions/anchor.js +28 -36
  117. package/dist/sessions/anchor.js.map +1 -1
  118. package/dist/sessions/background-events.d.ts +2 -0
  119. package/dist/sessions/background-events.js +10 -0
  120. package/dist/sessions/background-events.js.map +1 -0
  121. package/dist/sessions/background-tasks.d.ts +26 -20
  122. package/dist/sessions/background-tasks.js +88 -86
  123. package/dist/sessions/background-tasks.js.map +1 -1
  124. package/dist/sessions/background-updates.d.ts +15 -8
  125. package/dist/sessions/background-updates.js +101 -25
  126. package/dist/sessions/background-updates.js.map +1 -1
  127. package/dist/sessions/cli-sessions.d.ts +15 -5
  128. package/dist/sessions/cli-sessions.js +62 -43
  129. package/dist/sessions/cli-sessions.js.map +1 -1
  130. package/dist/sessions/completions.d.ts +30 -2
  131. package/dist/sessions/completions.js +294 -53
  132. package/dist/sessions/completions.js.map +1 -1
  133. package/dist/sessions/compress.d.ts +8 -0
  134. package/dist/sessions/compress.js +2 -2
  135. package/dist/sessions/compress.js.map +1 -1
  136. package/dist/sessions/drafts.d.ts +14 -10
  137. package/dist/sessions/drafts.js +38 -43
  138. package/dist/sessions/drafts.js.map +1 -1
  139. package/dist/sessions/gateway-sessions.d.ts +46 -0
  140. package/dist/sessions/gateway-sessions.js +378 -0
  141. package/dist/sessions/gateway-sessions.js.map +1 -0
  142. package/dist/sessions/goals.d.ts +31 -0
  143. package/dist/sessions/goals.js +75 -0
  144. package/dist/sessions/goals.js.map +1 -0
  145. package/dist/sessions/journal.d.ts +5 -79
  146. package/dist/sessions/journal.js +15 -606
  147. package/dist/sessions/journal.js.map +1 -1
  148. package/dist/sessions/list.d.ts +27 -18
  149. package/dist/sessions/list.js +133 -131
  150. package/dist/sessions/list.js.map +1 -1
  151. package/dist/sessions/merge.d.ts +68 -10
  152. package/dist/sessions/merge.js +259 -51
  153. package/dist/sessions/merge.js.map +1 -1
  154. package/dist/sessions/notices.d.ts +22 -0
  155. package/dist/sessions/notices.js +72 -0
  156. package/dist/sessions/notices.js.map +1 -0
  157. package/dist/sessions/pending.d.ts +10 -4
  158. package/dist/sessions/pending.js +83 -7
  159. package/dist/sessions/pending.js.map +1 -1
  160. package/dist/sessions/prefix-index.d.ts +82 -0
  161. package/dist/sessions/prefix-index.js +232 -0
  162. package/dist/sessions/prefix-index.js.map +1 -0
  163. package/dist/sessions/recovery.d.ts +4 -7
  164. package/dist/sessions/recovery.js +97 -281
  165. package/dist/sessions/recovery.js.map +1 -1
  166. package/dist/sessions/relay.d.ts +20 -20
  167. package/dist/sessions/relay.js +205 -127
  168. package/dist/sessions/relay.js.map +1 -1
  169. package/dist/sessions/service.d.ts +162 -59
  170. package/dist/sessions/service.js +1133 -521
  171. package/dist/sessions/service.js.map +1 -1
  172. package/dist/sessions/session.d.ts +18 -2
  173. package/dist/sessions/session.js +29 -5
  174. package/dist/sessions/session.js.map +1 -1
  175. package/dist/sessions/shares.d.ts +7 -6
  176. package/dist/sessions/shares.js +22 -45
  177. package/dist/sessions/shares.js.map +1 -1
  178. package/dist/sessions/source-kind.js +1 -0
  179. package/dist/sessions/source-kind.js.map +1 -1
  180. package/dist/sessions/state-db-identity.d.ts +18 -0
  181. package/dist/sessions/state-db-identity.js +67 -0
  182. package/dist/sessions/state-db-identity.js.map +1 -0
  183. package/dist/sessions/state-db-worker.js +3 -1
  184. package/dist/sessions/state-db-worker.js.map +1 -1
  185. package/dist/sessions/state-db.d.ts +53 -8
  186. package/dist/sessions/state-db.js +576 -50
  187. package/dist/sessions/state-db.js.map +1 -1
  188. package/dist/sessions/storage/backend.d.ts +162 -0
  189. package/dist/sessions/storage/backend.js +19 -0
  190. package/dist/sessions/storage/backend.js.map +1 -0
  191. package/dist/sessions/storage/conformance.d.ts +20 -0
  192. package/dist/sessions/storage/conformance.js +237 -0
  193. package/dist/sessions/storage/conformance.js.map +1 -0
  194. package/dist/sessions/storage/document.d.ts +16 -0
  195. package/dist/sessions/storage/document.js +44 -0
  196. package/dist/sessions/storage/document.js.map +1 -0
  197. package/dist/sessions/storage/json-documents.d.ts +23 -0
  198. package/dist/sessions/storage/json-documents.js +233 -0
  199. package/dist/sessions/storage/json-documents.js.map +1 -0
  200. package/dist/sessions/storage/json-files.d.ts +12 -0
  201. package/dist/sessions/storage/json-files.js +124 -0
  202. package/dist/sessions/storage/json-files.js.map +1 -0
  203. package/dist/sessions/storage/json-installation.d.ts +3 -0
  204. package/dist/sessions/storage/json-installation.js +31 -0
  205. package/dist/sessions/storage/json-installation.js.map +1 -0
  206. package/dist/sessions/storage/json-journal.d.ts +114 -0
  207. package/dist/sessions/storage/json-journal.js +686 -0
  208. package/dist/sessions/storage/json-journal.js.map +1 -0
  209. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  210. package/dist/sessions/storage/json-recovery.js +195 -0
  211. package/dist/sessions/storage/json-recovery.js.map +1 -0
  212. package/dist/sessions/storage/json.d.ts +68 -0
  213. package/dist/sessions/storage/json.js +258 -0
  214. package/dist/sessions/storage/json.js.map +1 -0
  215. package/dist/sessions/storage/row.d.ts +6 -0
  216. package/dist/sessions/storage/row.js +49 -0
  217. package/dist/sessions/storage/row.js.map +1 -0
  218. package/dist/sessions/storage/search-text.d.ts +4 -0
  219. package/dist/sessions/storage/search-text.js +16 -0
  220. package/dist/sessions/storage/search-text.js.map +1 -0
  221. package/dist/sessions/store.d.ts +63 -73
  222. package/dist/sessions/store.js +305 -611
  223. package/dist/sessions/store.js.map +1 -1
  224. package/dist/sessions/streams.d.ts +4 -0
  225. package/dist/sessions/streams.js +9 -1
  226. package/dist/sessions/streams.js.map +1 -1
  227. package/dist/sessions/titles.js +1 -1
  228. package/dist/sessions/titles.js.map +1 -1
  229. package/dist/sessions/todo.d.ts +5 -0
  230. package/dist/sessions/todo.js +28 -21
  231. package/dist/sessions/todo.js.map +1 -1
  232. package/dist/sessions/tool-display.d.ts +5 -4
  233. package/dist/sessions/tool-display.js +22 -16
  234. package/dist/sessions/tool-display.js.map +1 -1
  235. package/dist/sessions/turn-context.d.ts +10 -1
  236. package/dist/sessions/turn-context.js +7 -4
  237. package/dist/sessions/turn-context.js.map +1 -1
  238. package/dist/sessions/turn.d.ts +87 -17
  239. package/dist/sessions/turn.js +795 -347
  240. package/dist/sessions/turn.js.map +1 -1
  241. package/dist/sessions/wakeup-pause.d.ts +8 -13
  242. package/dist/sessions/wakeup-pause.js +31 -49
  243. package/dist/sessions/wakeup-pause.js.map +1 -1
  244. package/dist/sessions/window.js +13 -12
  245. package/dist/sessions/window.js.map +1 -1
  246. package/dist/settings.d.ts +19 -7
  247. package/dist/settings.js +179 -183
  248. package/dist/settings.js.map +1 -1
  249. package/dist/sidecar/client.d.ts +17 -3
  250. package/dist/sidecar/client.js +78 -8
  251. package/dist/sidecar/client.js.map +1 -1
  252. package/dist/sidecar/discover.d.ts +2 -1
  253. package/dist/sidecar/discover.js +93 -10
  254. package/dist/sidecar/discover.js.map +1 -1
  255. package/dist/sidecar/env.d.ts +4 -0
  256. package/dist/sidecar/env.js +3 -0
  257. package/dist/sidecar/env.js.map +1 -0
  258. package/dist/sidecar/fake.js +19 -2
  259. package/dist/sidecar/fake.js.map +1 -1
  260. package/dist/test/env-file.d.ts +5 -0
  261. package/dist/test/env-file.js +88 -0
  262. package/dist/test/env-file.js.map +1 -0
  263. package/dist/test/harness.d.ts +1 -0
  264. package/dist/test/harness.js +1 -1
  265. package/dist/test/harness.js.map +1 -1
  266. package/dist/test/passkeys.d.ts +22 -0
  267. package/dist/test/passkeys.js +62 -0
  268. package/dist/test/passkeys.js.map +1 -0
  269. package/dist/test/storage.d.ts +15 -0
  270. package/dist/test/storage.js +26 -0
  271. package/dist/test/storage.js.map +1 -0
  272. package/dist/tools/commands.d.ts +6 -1
  273. package/dist/tools/commands.js +8 -2
  274. package/dist/tools/commands.js.map +1 -1
  275. package/dist/tools/connectors.d.ts +33 -0
  276. package/dist/tools/connectors.js +121 -0
  277. package/dist/tools/connectors.js.map +1 -0
  278. package/dist/tools/crons.d.ts +38 -1
  279. package/dist/tools/crons.js +82 -15
  280. package/dist/tools/crons.js.map +1 -1
  281. package/dist/tools/dashboard-plugins.d.ts +5 -5
  282. package/dist/tools/dashboard-plugins.js +14 -14
  283. package/dist/tools/dashboard-plugins.js.map +1 -1
  284. package/dist/tools/extensions.d.ts +5 -4
  285. package/dist/tools/extensions.js +213 -207
  286. package/dist/tools/extensions.js.map +1 -1
  287. package/dist/tools/gateway-restart.d.ts +62 -0
  288. package/dist/tools/gateway-restart.js +142 -0
  289. package/dist/tools/gateway-restart.js.map +1 -0
  290. package/dist/tools/health.d.ts +15 -3
  291. package/dist/tools/health.js +97 -34
  292. package/dist/tools/health.js.map +1 -1
  293. package/dist/tools/hygiene.d.ts +4 -3
  294. package/dist/tools/hygiene.js +23 -9
  295. package/dist/tools/hygiene.js.map +1 -1
  296. package/dist/tools/kanban.d.ts +4 -2
  297. package/dist/tools/kanban.js +28 -19
  298. package/dist/tools/kanban.js.map +1 -1
  299. package/dist/tools/memory-masks.d.ts +5 -0
  300. package/dist/tools/memory-masks.js +256 -0
  301. package/dist/tools/memory-masks.js.map +1 -0
  302. package/dist/tools/memory.d.ts +29 -3
  303. package/dist/tools/memory.js +69 -27
  304. package/dist/tools/memory.js.map +1 -1
  305. package/dist/tools/plugin-settings.d.ts +15 -0
  306. package/dist/tools/plugin-settings.js +90 -0
  307. package/dist/tools/plugin-settings.js.map +1 -0
  308. package/dist/tools/skills.d.ts +9 -4
  309. package/dist/tools/skills.js +44 -13
  310. package/dist/tools/skills.js.map +1 -1
  311. package/dist/tools/update-notifications.d.ts +34 -15
  312. package/dist/tools/update-notifications.js +141 -57
  313. package/dist/tools/update-notifications.js.map +1 -1
  314. package/dist/tools/update-rollback.d.ts +11 -0
  315. package/dist/tools/update-rollback.js +28 -0
  316. package/dist/tools/update-rollback.js.map +1 -0
  317. package/dist/tools/updates.d.ts +93 -33
  318. package/dist/tools/updates.js +1051 -345
  319. package/dist/tools/updates.js.map +1 -1
  320. package/dist/util.d.ts +2 -0
  321. package/dist/util.js +5 -0
  322. package/dist/util.js.map +1 -1
  323. package/dist/workspace/git.d.ts +16 -1
  324. package/dist/workspace/git.js +291 -11
  325. package/dist/workspace/git.js.map +1 -1
  326. package/dist/workspace/media.d.ts +12 -9
  327. package/dist/workspace/media.js +31 -20
  328. package/dist/workspace/media.js.map +1 -1
  329. package/dist/workspace/rollback.d.ts +2 -2
  330. package/dist/workspace/rollback.js +5 -5
  331. package/dist/workspace/rollback.js.map +1 -1
  332. package/dist/workspace/workspaces.d.ts +15 -19
  333. package/dist/workspace/workspaces.js +55 -127
  334. package/dist/workspace/workspaces.js.map +1 -1
  335. package/dist/workspace/worktrees.js +170 -32
  336. package/dist/workspace/worktrees.js.map +1 -1
  337. package/package.json +5 -3
  338. package/sidecar/agent_dependency.json +4 -4
  339. package/sidecar/agent_support.json +26 -0
  340. package/sidecar/scripts/record-fixtures.py +12 -3
  341. package/sidecar/scripts/replay_sidecar.py +8 -0
  342. package/sidecar/scripts/suite_runner.py +302 -0
  343. package/sidecar/scripts/test.sh +12 -4
  344. package/sidecar/talaria_sidecar/__init__.py +6 -1
  345. package/sidecar/talaria_sidecar/__main__.py +4 -2
  346. package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
  347. package/sidecar/talaria_sidecar/agent_services.py +1053 -0
  348. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  349. package/sidecar/talaria_sidecar/home.py +43 -1
  350. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  351. package/sidecar/talaria_sidecar/methods/aux.py +139 -31
  352. package/sidecar/talaria_sidecar/methods/chat.py +847 -134
  353. package/sidecar/talaria_sidecar/methods/commands.py +213 -4
  354. package/sidecar/talaria_sidecar/methods/config.py +123 -4
  355. package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
  356. package/sidecar/talaria_sidecar/methods/cron.py +366 -34
  357. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  358. package/sidecar/talaria_sidecar/methods/gateway.py +194 -54
  359. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
  360. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  361. package/sidecar/talaria_sidecar/methods/kanban.py +80 -27
  362. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  363. package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
  364. package/sidecar/talaria_sidecar/methods/process.py +160 -11
  365. package/sidecar/talaria_sidecar/methods/profiles.py +44 -6
  366. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  367. package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
  368. package/sidecar/talaria_sidecar/methods/skills.py +67 -4
  369. package/sidecar/talaria_sidecar/methods/state_db.py +277 -70
  370. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  371. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  372. package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
  373. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  374. package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
  375. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  376. package/sidecar/talaria_sidecar/rpc.py +45 -4
  377. package/sidecar/talaria_sidecar/runtime.py +81 -12
  378. package/sidecar/talaria_sidecar/speed.py +35 -0
  379. package/static/dist/FILES.txt +126 -127
  380. package/static/dist/assets/AppShell-cey33v9s.js +8 -0
  381. package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
  382. package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
  383. package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
  384. package/static/dist/assets/Field-j8uqoigz.js +9 -0
  385. package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
  386. package/static/dist/assets/Markdown-lahuichm.js +598 -0
  387. package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
  388. package/static/dist/assets/Select-gqatm2e0.js +1 -0
  389. package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
  390. package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
  391. package/static/dist/assets/States-e7dkgtxf.js +1 -0
  392. package/static/dist/assets/Toaster-bfqqy786.js +1 -0
  393. package/static/dist/assets/_app-n1n6pfx1.js +1 -0
  394. package/static/dist/assets/{_app.ext._extensionId-gfbzpoou.js → _app.ext._extensionId-buxjxav3.js} +1 -1
  395. package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
  396. package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
  397. package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
  398. package/static/dist/assets/_app.logs-dm873z90.js +1 -0
  399. package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
  400. package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
  401. package/static/dist/assets/{_app.session._sessionId-fn1bhde6.js → _app.session._sessionId-g2vzghvc.js} +1 -1
  402. package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
  403. package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
  404. package/static/dist/assets/_app.skills-cii852yq.js +1 -0
  405. package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
  406. package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
  407. package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
  408. package/static/dist/assets/{abnfDiagram-VCTEODGH-c8kdixke.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
  409. package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
  410. package/static/dist/assets/{architectureDiagram-5GKGNRK7-cku06x1p.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
  411. package/static/dist/assets/archive-b1j004vn.js +1 -0
  412. package/static/dist/assets/back-g74gkozu.js +1 -0
  413. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
  414. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
  415. package/static/dist/assets/channel-es95o4y8.js +1 -0
  416. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
  417. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
  418. package/static/dist/assets/{chunk-DU6HZSFF-ofajdixh.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
  419. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
  420. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
  421. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
  422. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
  423. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
  424. package/static/dist/assets/{chunk-OSK3NFVY-e7gkikcw.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
  425. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
  426. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
  427. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
  428. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
  429. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
  430. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
  431. package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
  432. package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
  433. package/static/dist/assets/copied-bli9tbcx.js +1 -0
  434. package/static/dist/assets/{copy-magtofyp.js → copy-jamtd1pf.js} +1 -1
  435. package/static/dist/assets/{cynefin-OW5HDTMX-siqlulkl.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
  436. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-i648tah9.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
  437. package/static/dist/assets/{dagre-GXQ25YYZ-mwn6uc17.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
  438. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
  439. package/static/dist/assets/{diagram-UQ7AKVKN-mekq7zgq.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
  440. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
  441. package/static/dist/assets/{diagram-VX7I27RA-xe6r5qfw.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
  442. package/static/dist/assets/{diagram-Z3DM3KII-nq07dg7k.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
  443. package/static/dist/assets/{ebnfDiagram-PWID7BFC-c62ipm3g.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
  444. package/static/dist/assets/{erDiagram-RLTQ6QDP-h39kmnzy.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
  445. package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
  446. package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
  447. package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
  448. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
  449. package/static/dist/assets/{gitGraph-4MIJSDKK-fr2i7caa.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
  450. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
  451. package/static/dist/assets/{highlighted-body-KPVGNVTW-jwpop9p8.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
  452. package/static/dist/assets/host-hzm8n85n.js +1 -0
  453. package/static/dist/assets/index-drdcaqeq.css +2 -0
  454. package/static/dist/assets/index-jzm844he.js +11 -0
  455. package/static/dist/assets/{info-A6RAGUB7-h11x0dxo.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
  456. package/static/dist/assets/{infoDiagram-27XIBGKW-dfh47dw9.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
  457. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
  458. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
  459. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
  460. package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
  461. package/static/dist/assets/{line-meizqsvn.js → line-h0ojzsvs.js} +1 -1
  462. package/static/dist/assets/link-k01ryaf9.js +1 -0
  463. package/static/dist/assets/login-bsae49ew.js +1 -0
  464. package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
  465. package/static/dist/assets/{mermaid-parser.core-ntne45i9.js → mermaid-parser.core-bnre27u8.js} +3 -3
  466. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
  467. package/static/dist/assets/onboarding-c65tysya.js +1 -0
  468. package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
  469. package/static/dist/assets/{packet-AYTQ26CC-l6jdka8p.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
  470. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  471. package/static/dist/assets/{pegDiagram-XKGWAZYB-g1er3bxj.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
  472. package/static/dist/assets/{pie-WAS4IAKB-i0ylcrt9.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
  473. package/static/dist/assets/{pieDiagram-E7YTZNPT-fbz1e55z.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
  474. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
  475. package/static/dist/assets/{radar-RG4KPBEZ-f9b70aa4.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
  476. package/static/dist/assets/{railroad-74A4TZTK-lgsuoeda.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
  477. package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
  478. package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
  479. package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
  480. package/static/dist/assets/{railroadDiagram-O6MQD6OU-eqpy9fnz.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
  481. package/static/dist/assets/{refresh-cw-hlwb02qv.js → refresh-cw-mqbkv2a3.js} +1 -1
  482. package/static/dist/assets/refresh-l29ljp76.js +1 -0
  483. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ogf6puhi.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
  484. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
  485. package/static/dist/assets/saved-ndd4t2xb.js +1 -0
  486. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
  487. package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
  488. package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
  489. package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
  490. package/static/dist/assets/{stateDiagram-D77RDMKH-fyoznpcs.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
  491. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
  492. package/static/dist/assets/{swimlanes-42K2YHIH-oefldhfy.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
  493. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
  494. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
  495. package/static/dist/assets/toast-i3h34v6o.js +64 -0
  496. package/static/dist/assets/{treeView-Q6P3EWNA-f7hcci5o.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
  497. package/static/dist/assets/{treemap-WGGIJYW6-nl85071a.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
  498. package/static/dist/assets/untitled-oaoya4qj.js +1 -0
  499. package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
  500. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-gjc70qru.js} +1 -1
  501. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-e6ae539u.js} +1 -1
  502. package/static/dist/assets/{vennDiagram-4TSXK5OY-h5hzbk0n.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
  503. package/static/dist/assets/{wardley-WFR3VGLG-jftvpm3e.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
  504. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hedh85ge.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
  505. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
  506. package/static/dist/index.html +1 -1
  507. package/static/dist/sw.js +1 -1
  508. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  509. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  510. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  511. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  512. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  513. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  514. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  515. package/static/dist/assets/Select-kddq75qc.js +0 -1
  516. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  517. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  518. package/static/dist/assets/States-eq6fe29m.js +0 -1
  519. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  520. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  521. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  522. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  523. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  524. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  525. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  526. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  527. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  528. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  529. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  530. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  531. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  532. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  533. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  534. package/static/dist/assets/back-h7aun7ew.js +0 -1
  535. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  536. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  537. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  538. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  539. package/static/dist/assets/copied-hcplv46p.js +0 -1
  540. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  541. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  542. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  543. package/static/dist/assets/host-i828xuo7.js +0 -1
  544. package/static/dist/assets/index-eh8zilnv.css +0 -2
  545. package/static/dist/assets/index-r648f4o1.js +0 -11
  546. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  547. package/static/dist/assets/link-oot2yn73.js +0 -1
  548. package/static/dist/assets/login-gu28l9ou.js +0 -1
  549. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  550. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  551. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  552. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  553. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  554. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  555. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  556. package/static/dist/assets/saved-girnabkf.js +0 -1
  557. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  558. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  559. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  560. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  561. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  562. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  563. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  564. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  565. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  566. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  567. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
@@ -7,13 +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';
10
11
  import { Worker } from 'node:worker_threads';
11
12
  import { str } from '../util.js';
13
+ import { stateDbMessageFingerprint, stateDbMessageKey } from './state-db-identity.js';
12
14
  import { MESSAGING_SOURCES } from './source-kind.js';
13
15
  import { looksLikeDefaultCliTitle, normalizeSourceName } from './titles.js';
14
16
  export const CLI_MIN_UNTITLED_USER_MESSAGE_COUNT = 2;
15
- 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;
16
21
  const SOURCE_LABELS = {
22
+ bluebubbles: 'BlueBubbles', dingtalk: 'DingTalk', feishu: 'Feishu', qqbot: 'QQBot', yuanbao: 'Yuanbao', whatsapp_cloud: 'WhatsApp Cloud',
17
23
  acp: 'ACP', api_server: 'API', cli: 'CLI', cron: 'Cron', discord: 'Discord', email: 'Email', kanban: 'Kanban', wecom: 'WeCom', wecom_callback: 'WeCom Callback',
18
24
  slack: 'Slack', telegram: 'Telegram', tool: 'Tool', tui: 'TUI', webhook: 'Webhook', webui: 'WebUI', weixin: 'Weixin', matrix: 'Matrix', signal: 'Signal',
19
25
  };
@@ -90,6 +96,8 @@ export function isCliSessionRow(row) {
90
96
  export function isCliSessionRowVisible(row) {
91
97
  if (!isCliSessionRow(row))
92
98
  return true;
99
+ if (row._lineage_root_id && row._lineage_tip_id)
100
+ return true;
93
101
  const actual = positiveInt(row.actual_message_count);
94
102
  const messageCount = actual || positiveInt(row.message_count);
95
103
  if (messageCount <= 0)
@@ -108,9 +116,13 @@ export function isCliSessionRowVisible(row) {
108
116
  return countUserTurns(row) >= CLI_MIN_UNTITLED_USER_MESSAGE_COUNT;
109
117
  }
110
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;
111
121
  function isContinuationSession(parent, child) {
112
122
  if (!parent || !child)
113
123
  return false;
124
+ if (lower(child.source) === 'tool' || explicitForkMarkers(child, true).length > 0)
125
+ return false;
114
126
  if (lower(child.session_source) === 'fork')
115
127
  return false;
116
128
  const parentSource = lower(parent.source);
@@ -203,6 +215,10 @@ export function projectAgentSessionRows(rows) {
203
215
  };
204
216
  const projected = [];
205
217
  for (const row of rows) {
218
+ if (row._lineage_root_id && row._lineage_tip_id) {
219
+ projected.push({ ...row });
220
+ continue;
221
+ }
206
222
  if (continuationChildIds.has(str(row.id)))
207
223
  continue;
208
224
  let segments = 1;
@@ -247,13 +263,170 @@ export function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOU
247
263
  const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
248
264
  return db;
249
265
  }
266
+ /** The table's columns; a failed read throws, so an unreadable database is never mistaken for an older schema (TAL-785). */
250
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;
251
369
  try {
252
- return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
370
+ db = openStateDbReadonly(dbPath);
253
371
  }
254
372
  catch {
255
- return new Set();
373
+ return sid;
256
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;
380
+ }
381
+ catch {
382
+ return sid;
383
+ }
384
+ finally {
385
+ db.close();
386
+ }
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;
257
430
  }
258
431
  const warnedNoSourceColumn = new Set();
259
432
  let readDelayMs = 0;
@@ -277,6 +450,8 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
277
450
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, readDelayMs);
278
451
  const db = openStateDbReadonly(dbPath);
279
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);
280
455
  const sessionCols = tableColumns(db, 'sessions');
281
456
  const messageCols = tableColumns(db, 'messages');
282
457
  if (!sessionCols.has('source')) {
@@ -337,14 +512,26 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
337
512
  let candidateOrder = 'ORDER BY s.started_at DESC';
338
513
  const where = ['s.source IS NOT NULL'];
339
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';
340
527
  const included = (include ?? []).map(String).filter(Boolean);
341
528
  if (included.length) {
342
- where.push(`s.source IN (${included.map(() => '?').join(', ')})`);
529
+ where.push(`${sourceSql} IN (${included.map(() => '?').join(', ')})`);
343
530
  params.push(...included);
344
531
  }
345
532
  const excluded = (exclude ?? []).map(String).filter(Boolean);
346
533
  if (excluded.length) {
347
- where.push(`s.source NOT IN (${excluded.map(() => '?').join(', ')})`);
534
+ where.push(`${sourceSql} NOT IN (${excluded.map(() => '?').join(', ')})`);
348
535
  params.push(...excluded);
349
536
  }
350
537
  const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
@@ -357,7 +544,7 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
357
544
  orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
358
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';
359
546
  }
360
- 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`;
361
548
  let raw;
362
549
  if (limit !== null) {
363
550
  const resultLimit = Math.max(0, Math.trunc(limit));
@@ -372,8 +559,21 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
372
559
  else {
373
560
  raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
374
561
  }
375
- let projected = projectAgentSessionRows(raw.map((r) => ({ ...r })));
376
- 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
+ });
377
577
  projected = projected.filter(isCliSessionRowVisible);
378
578
  if (limit === null)
379
579
  return projected;
@@ -475,15 +675,138 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
475
675
  db.close();
476
676
  }
477
677
  }
478
- /**
479
- * TAL-358: the `sessions.source` owner of each present id, in one chunked read. A missing state.db (or one without a
480
- * `source` column) owns nothing; an unreadable one answers null, so callers fail closed on an unknown owner.
481
- */
482
- 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) {
483
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) {
484
807
  const ids = [...new Set([...sessionIds].map((s) => s.trim()).filter(Boolean))];
485
808
  if (!ids.length || !existsSync(dbPath))
486
- return sources;
809
+ return new Map();
487
810
  let db;
488
811
  try {
489
812
  db = openStateDbReadonly(dbPath);
@@ -492,15 +815,8 @@ export function stateDbSessionSources(dbPath, sessionIds) {
492
815
  return null;
493
816
  }
494
817
  try {
495
- const cols = tableColumns(db, 'sessions');
496
- if (!cols.has('id') || !cols.has('source'))
497
- return sources;
498
- for (let i = 0; i < ids.length; i += 500) {
499
- const chunk = ids.slice(i, i + 500);
500
- for (const row of db.prepare(`SELECT id, source FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
501
- sources.set(row.id, str(row.source));
502
- }
503
- return sources;
818
+ db.exec('BEGIN');
819
+ return sessionSources(db, tableColumns(db, 'sessions'), ids);
504
820
  }
505
821
  catch {
506
822
  return null;
@@ -509,6 +825,76 @@ export function stateDbSessionSources(dbPath, sessionIds) {
509
825
  db.close();
510
826
  }
511
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
+ }
512
898
  /** Python `state_db_has_session`: true only when `sid` is a row of the sessions table; a missing or unreadable db is false. */
513
899
  export function stateDbHasSession(dbPath, sid) {
514
900
  const id = sid.trim();
@@ -541,7 +927,7 @@ export function stateDbHasSession(dbPath, sid) {
541
927
  export function cheapChangeFingerprint(dbPath, onError) {
542
928
  try {
543
929
  const parts = [];
544
- const db = openStateDbReadonly(dbPath);
930
+ const db = openStateDbReadonly(dbPath, STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS);
545
931
  try {
546
932
  for (const table of ['sessions', 'messages']) {
547
933
  try {
@@ -656,6 +1042,15 @@ export async function cheapChangeFingerprintAsync(dbPath, onError) {
656
1042
  onError?.(reason, new Error(message));
657
1043
  return reply.value;
658
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
+ }
659
1054
  /** `stateDbSessionSources` on the listing worker; a worker failure is an unreadable state.db (null), so owners stay locked. */
660
1055
  export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
661
1056
  try {
@@ -696,16 +1091,23 @@ export async function stopStateDbWorker() {
696
1091
  // ── message reader ────────────────────────────────────────────────────────
697
1092
  const STATE_DB_CONTENT_JSON_PREFIX = '\0json:';
698
1093
  // `display_kind` / `display_metadata`: the Agent's own tag for rows it writes itself (steers, delivered notifications; TAL-371).
699
- 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'];
700
- 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']);
701
1096
  /** Python `_decode_state_db_content`: the Agent's sentinel-prefixed structured content, left untouched otherwise. */
702
1097
  function decodeStateDbContent(value) {
703
1098
  const text = Buffer.isBuffer(value) ? value.toString('utf8') : value;
704
1099
  if (typeof text !== 'string' || !text.startsWith(STATE_DB_CONTENT_JSON_PREFIX))
705
1100
  return text;
706
1101
  try {
707
- const decoded = JSON.parse(text.slice(STATE_DB_CONTENT_JSON_PREFIX.length));
708
- 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;
709
1111
  }
710
1112
  catch {
711
1113
  return text;
@@ -730,11 +1132,13 @@ function projectStateDbMessage(row, hasId) {
730
1132
  const value = row[col];
731
1133
  if (value === null || value === undefined || value === '')
732
1134
  continue;
733
- 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;
734
1136
  }
735
1137
  // TAL-493: the merge tells rows committed after the session's last read by this id.
736
1138
  if (hasId && row.id !== null && row.id !== undefined)
737
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;
738
1142
  if (msg.role === 'tool' && msg.tool_name && !msg.name)
739
1143
  msg.name = msg.tool_name;
740
1144
  return msg;
@@ -753,6 +1157,74 @@ function stateDbGeneration(dbPath) {
753
1157
  return null;
754
1158
  }
755
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
+ }
756
1228
  export function stateDbSessionMessages(dbPath, sid, opts = {}) {
757
1229
  return stateDbSessionRead(dbPath, sid, opts).rows;
758
1230
  }
@@ -761,7 +1233,14 @@ export function stateDbSessionMessages(dbPath, sid, opts = {}) {
761
1233
  * (TAL-493: an empty id-capable read is a baseline of 0; a missing database or a failed read says nothing).
762
1234
  */
763
1235
  export function stateDbSessionRead(dbPath, sid, opts = {}) {
764
- 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 };
765
1244
  const id = sid.trim();
766
1245
  if (!id || !existsSync(dbPath))
767
1246
  return none;
@@ -773,20 +1252,23 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
773
1252
  return none;
774
1253
  }
775
1254
  try {
1255
+ db.exec('BEGIN'); // The live rows and their archived predecessors must describe one snapshot.
776
1256
  const available = tableColumns(db, 'messages');
777
1257
  if (!['role', 'content', 'timestamp'].every((c) => available.has(c)))
778
1258
  return { ...none, ok: available.size > 0 };
779
1259
  const hasId = available.has('id');
780
- 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))];
781
1261
  // TAL-529: an explicit lineage (the session and its compression continuations) replaces the parent walk.
782
- const chain = opts.lineage?.length ? [...opts.lineage] : [id];
783
- 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)) {
784
1265
  const sessionCols = tableColumns(db, 'sessions');
785
1266
  if (['parent_session_id', 'end_reason', 'started_at', 'source'].every((c) => sessionCols.has(c))) {
786
- 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 = ?`);
787
1269
  let current = select.get(id);
788
1270
  const seen = new Set([id]);
789
- for (let hop = 0; hop < 20 && current; hop += 1) {
1271
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
790
1272
  const parentId = str(current.parent_session_id);
791
1273
  if (!parentId || seen.has(parentId))
792
1274
  break;
@@ -800,8 +1282,12 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
800
1282
  }
801
1283
  }
802
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];
803
1289
  const order = hasId ? 'id' : 'timestamp';
804
- 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);
805
1291
  const projected = rows.map((row) => projectStateDbMessage(row, hasId));
806
1292
  // TAL-709: an in-place compaction archives the live rows and re-inserts each as a copy that keeps its role and timestamp
807
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
@@ -813,7 +1299,7 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
813
1299
  // compactions it holds.
814
1300
  const sameCall = available.has('tool_call_id') ? " AND COALESCE(a.tool_call_id, '') = COALESCE(m.tool_call_id, '')" : '';
815
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 ');
816
- 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}) GROUP BY m.id`).all(...chain);
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);
817
1303
  const twinOf = new Map(twins.map((row) => [row.id, row.twin]));
818
1304
  for (const [i, row] of rows.entries()) {
819
1305
  const twin = twinOf.get(row.id);
@@ -821,16 +1307,49 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
821
1307
  projected[i]._state_db_row_id = twin;
822
1308
  }
823
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
+ }
824
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);
825
1326
  if (hasId) {
826
- const generation = stateDbGeneration(dbPath);
827
1327
  for (const m of projected)
828
1328
  m._state_db_generation = generation;
829
1329
  }
830
- 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 } : {}) };
831
1350
  }
832
- catch {
833
- return none;
1351
+ catch (error) {
1352
+ return { ...none, reason: stateDbReadFailure(error).reason };
834
1353
  }
835
1354
  finally {
836
1355
  db.close();
@@ -838,8 +1357,8 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
838
1357
  }
839
1358
  /**
840
1359
  * TAL-529: `sid` and each compression continuation the Agent rotated it to, down to the live tip (Python
841
- * `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or 20
842
- * 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]`.
843
1362
  */
844
1363
  export function stateDbCompressionLineage(dbPath, sid) {
845
1364
  const lineage = [sid.trim()];
@@ -853,14 +1372,19 @@ export function stateDbCompressionLineage(dbPath, sid) {
853
1372
  return lineage;
854
1373
  }
855
1374
  try {
1375
+ db.exec('BEGIN');
1376
+ const local = localLineages(db, lineage[0]).get(lineage[0]);
1377
+ if (local)
1378
+ return [...local.ids];
856
1379
  const sessionCols = tableColumns(db, 'sessions');
857
1380
  if (!['id', 'parent_session_id', 'end_reason', 'ended_at', 'started_at', 'source'].every((c) => sessionCols.has(c)))
858
1381
  return lineage;
859
1382
  const sessionSource = sessionCols.has('session_source') ? 'session_source' : 'NULL AS session_source';
860
- 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}`;
861
1385
  let current = db.prepare(`SELECT ${cols} FROM sessions WHERE id = ?`).get(lineage[0]);
862
1386
  const children = db.prepare(`SELECT ${cols} FROM sessions WHERE parent_session_id = ?`);
863
- for (let hop = 0; hop < 20 && current; hop += 1) {
1387
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
864
1388
  const parent = current;
865
1389
  const next = children.all(str(parent.id)).filter((child) => isContinuationSession(parent, child));
866
1390
  if (next.length !== 1 || lineage.includes(str(next[0].id)))
@@ -893,7 +1417,7 @@ const emptyLineageReport = (sid) => ({
893
1417
  * newest first. A cycle, a hop limit hit, or a continuation off the walked path marks `manual_review`. A missing,
894
1418
  * unreadable, or too-old database, or an unknown id, answers `found: false`.
895
1419
  */
896
- export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
1420
+ export function stateDbLineageReport(dbPath, rawSid, maxHops = COMPRESSION_CHAIN_CAP) {
897
1421
  const sid = rawSid.trim();
898
1422
  if (!sid || !existsSync(dbPath))
899
1423
  return emptyLineageReport(sid);
@@ -905,6 +1429,8 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
905
1429
  return emptyLineageReport(sid);
906
1430
  }
907
1431
  try {
1432
+ db.exec('BEGIN');
1433
+ const local = localLineages(db, sid).get(sid);
908
1434
  const cols = tableColumns(db, 'sessions');
909
1435
  if (!['id', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
910
1436
  return emptyLineageReport(sid);
@@ -915,10 +1441,10 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
915
1441
  const target = fetch(sid);
916
1442
  if (!target)
917
1443
  return emptyLineageReport(sid);
918
- const segments = [target];
919
- const seen = new Set([sid]);
920
- let manualReview = true;
921
- for (let hop = 0; hop < maxHops; hop += 1) {
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) {
922
1448
  const current = segments[segments.length - 1];
923
1449
  const parentId = str(current.parent_session_id);
924
1450
  const parent = fetch(parentId);