coding-tool-x 3.8.4 → 3.8.5

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 (434) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +86 -15
  3. package/dist/web/assets/AgentsDrawer-BG6P3DX-.js +1 -0
  4. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
  5. package/dist/web/assets/Analytics--z_FmL5P.js +1 -0
  6. package/dist/web/assets/Analytics-CyHhR126.css +1 -0
  7. package/dist/web/assets/AssetPathField-CrJtRKHy.js +1 -0
  8. package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
  9. package/dist/web/assets/ChatHistoryDrawer-BiuEryJj.js +1 -0
  10. package/dist/web/assets/CollapseItem-BAYEsL_Q.js +1 -0
  11. package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
  12. package/dist/web/assets/CommandsDrawer-YJ-U7BXN.js +1 -0
  13. package/dist/web/assets/ConfigExportDrawer-Ce_hAknW.js +1 -0
  14. package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
  15. package/dist/web/assets/ConfigTemplates-Bvr1NN6K.js +1 -0
  16. package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
  17. package/dist/web/assets/ConfigTemplatesDrawer-BjIeHfF-.js +1 -0
  18. package/dist/web/assets/ConfigTemplatesPanel-AV00tm_D.js +1 -0
  19. package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
  20. package/dist/web/assets/Divider-D28g1gwj.js +1 -0
  21. package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
  22. package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
  23. package/dist/web/assets/FavoritesDrawer-Cz95o4CD.js +1 -0
  24. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
  25. package/dist/web/assets/GatewayConvertDrawer-DLlbMz27.js +1 -0
  26. package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
  27. package/dist/web/assets/Home-D7CCOFQC.js +1 -0
  28. package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
  29. package/dist/web/assets/MarkdownEditor-rEZ-c72L.js +1 -0
  30. package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
  31. package/dist/web/assets/McpDrawer-DfoCyxU2.js +1 -0
  32. package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
  33. package/dist/web/assets/McpFormDrawer-SKu8HNUR.js +1 -0
  34. package/dist/web/assets/PluginManager-BbeK-3rV.js +1 -0
  35. package/dist/web/assets/PluginsDrawer-CWVJ7yjS.js +1 -0
  36. package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
  37. package/dist/web/assets/PluginsPanel-Cm2N3StG.js +1 -0
  38. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
  39. package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
  40. package/dist/web/assets/ProjectList-CsjnsNPv.js +1 -0
  41. package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
  42. package/dist/web/assets/PromptsDrawer-EGd3djFa.js +1 -0
  43. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
  44. package/dist/web/assets/RecentSessionsDrawer-D1vtoLAs.js +1 -0
  45. package/dist/web/assets/SessionCard-BgLg49U3.js +1 -0
  46. package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
  47. package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
  48. package/dist/web/assets/SessionList-TRiTklWk.js +1 -0
  49. package/dist/web/assets/SettingsDrawer-d5liXjHq.css +1 -0
  50. package/dist/web/assets/SettingsDrawer-rz2WejGY.js +1 -0
  51. package/dist/web/assets/SkillManager-BTjTUbHq.js +1 -0
  52. package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
  53. package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
  54. package/dist/web/assets/SkillsDrawer-pN0hbkK3.js +1 -0
  55. package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
  56. package/dist/web/assets/SkillsPanel-gmBWEoiF.js +1 -0
  57. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
  58. package/dist/web/assets/SpeedTestDrawer-DcNzuVJk.js +1 -0
  59. package/dist/web/assets/Upload-v5rV0qTT.js +1 -0
  60. package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
  61. package/dist/web/assets/WorkspaceDrawer-DxmicM4U.js +1 -0
  62. package/dist/web/assets/WorkspaceManager-CJHCjARN.js +1 -0
  63. package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
  64. package/dist/web/assets/WorkspacePanel-D8_Zq-du.js +1 -0
  65. package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
  66. package/dist/web/assets/client-UDwXBP9i.js +1 -0
  67. package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
  68. package/dist/web/assets/headers-cf-QapUq.js +1 -0
  69. package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
  70. package/dist/web/assets/index-C0EHRym0.css +1 -0
  71. package/dist/web/assets/index-DnEocV0M.js +2 -0
  72. package/dist/web/assets/light-Cp0t7v32.js +1 -0
  73. package/dist/web/assets/markdown-u082bbtj.js +1 -0
  74. package/dist/web/assets/platforms-CBkB45JP.js +1 -0
  75. package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
  76. package/dist/web/assets/project-config-B2baPzYL.js +1 -0
  77. package/dist/web/assets/sessions-Cmy31slD.js +1 -0
  78. package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
  79. package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
  80. package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
  81. package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
  82. package/dist/web/assets/useResponsiveDrawer-B3uojyvV.js +1 -0
  83. package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
  84. package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
  85. package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
  86. package/dist/web/assets/xml-CgY3RMlX.js +1 -0
  87. package/dist/web/index.html +12 -8
  88. package/docs/native-log-memory-2026-09-16.md +43 -0
  89. package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
  90. package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
  91. package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
  92. package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
  93. package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
  94. package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
  95. package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
  96. package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
  97. package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
  98. package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
  99. package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
  100. package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
  101. package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
  102. package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
  103. package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
  104. package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
  105. package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
  106. package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
  107. package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
  108. package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
  109. package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
  110. package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
  111. package/package.json +8 -4
  112. package/src/commands/channels.js +88 -191
  113. package/src/commands/cli-type.js +19 -11
  114. package/src/commands/daemon.js +94 -32
  115. package/src/commands/doctor.js +16 -9
  116. package/src/commands/export-config.js +23 -5
  117. package/src/commands/list.js +130 -52
  118. package/src/commands/logs.js +44 -27
  119. package/src/commands/platform-command-registry.js +78 -0
  120. package/src/commands/plugin.js +11 -5
  121. package/src/commands/port-config.js +34 -52
  122. package/src/commands/proxy-control.js +207 -145
  123. package/src/commands/proxy.js +2 -2
  124. package/src/commands/resume.js +87 -91
  125. package/src/commands/search.js +137 -67
  126. package/src/commands/stats.js +30 -22
  127. package/src/commands/switch.js +1 -1
  128. package/src/commands/toggle-proxy.js +94 -111
  129. package/src/commands/ui.js +67 -6
  130. package/src/commands/workspace.js +90 -8
  131. package/src/config/default.js +6 -53
  132. package/src/config/loader.js +61 -24
  133. package/src/config/model-metadata.js +49 -59
  134. package/src/config/model-metadata.json +4095 -99
  135. package/src/config/paths.js +181 -8
  136. package/src/index.js +134 -71
  137. package/src/platforms/access.js +196 -0
  138. package/src/{server/services → platforms}/agents-service.js +354 -197
  139. package/src/platforms/channel-auth-registry.js +162 -0
  140. package/src/platforms/channel-auth-service.js +82 -0
  141. package/src/{server/services → platforms}/commands-service.js +195 -115
  142. package/src/platforms/driver-registry.js +59 -0
  143. package/src/platforms/drivers/claude/api-operations.js +62 -0
  144. package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
  145. package/src/platforms/drivers/claude/channels.js +39 -0
  146. package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
  147. package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
  148. package/src/platforms/drivers/claude/health.js +22 -0
  149. package/src/platforms/drivers/claude/mcp.js +9 -0
  150. package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
  151. package/src/platforms/drivers/claude/native-config.js +46 -0
  152. package/src/platforms/drivers/claude/native-logs.js +72 -0
  153. package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
  154. package/src/platforms/drivers/claude/project-config.js +21 -0
  155. package/src/platforms/drivers/claude/projects.js +62 -0
  156. package/src/platforms/drivers/claude/prompts.js +9 -0
  157. package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
  158. package/src/platforms/drivers/claude/proxy.js +16 -0
  159. package/src/platforms/drivers/claude/resource-sync.js +9 -0
  160. package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
  161. package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
  162. package/src/platforms/drivers/claude/sessions.js +76 -0
  163. package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
  164. package/src/platforms/drivers/claude/statistics.js +16 -0
  165. package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
  166. package/src/platforms/drivers/codex/api-operations.js +9 -0
  167. package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
  168. package/src/platforms/drivers/codex/channels.js +45 -0
  169. package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
  170. package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
  171. package/src/platforms/drivers/codex/mcp.js +9 -0
  172. package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
  173. package/src/platforms/drivers/codex/native-config.js +47 -0
  174. package/src/platforms/drivers/codex/native-logs.js +166 -0
  175. package/src/platforms/drivers/codex/project-config.js +21 -0
  176. package/src/platforms/drivers/codex/projects.js +26 -0
  177. package/src/platforms/drivers/codex/prompts.js +9 -0
  178. package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
  179. package/src/platforms/drivers/codex/proxy.js +16 -0
  180. package/src/platforms/drivers/codex/resource-sync.js +9 -0
  181. package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
  182. package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
  183. package/src/platforms/drivers/codex/sessions.js +36 -0
  184. package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
  185. package/src/platforms/drivers/codex/statistics.js +16 -0
  186. package/src/platforms/drivers/gemini/api-operations.js +9 -0
  187. package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
  188. package/src/platforms/drivers/gemini/channels.js +39 -0
  189. package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
  190. package/src/platforms/drivers/gemini/mcp.js +9 -0
  191. package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
  192. package/src/platforms/drivers/gemini/native-config.js +39 -0
  193. package/src/platforms/drivers/gemini/native-logs.js +180 -0
  194. package/src/platforms/drivers/gemini/project-config.js +21 -0
  195. package/src/platforms/drivers/gemini/projects.js +26 -0
  196. package/src/platforms/drivers/gemini/prompts.js +9 -0
  197. package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
  198. package/src/platforms/drivers/gemini/proxy.js +16 -0
  199. package/src/platforms/drivers/gemini/resource-sync.js +9 -0
  200. package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
  201. package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
  202. package/src/platforms/drivers/gemini/sessions.js +47 -0
  203. package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
  204. package/src/platforms/drivers/gemini/statistics.js +16 -0
  205. package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
  206. package/src/platforms/drivers/generic-filesystem.js +166 -0
  207. package/src/platforms/drivers/generic-jsonl.js +141 -0
  208. package/src/platforms/drivers/generic-mcp.js +44 -0
  209. package/src/platforms/drivers/generic-openai-compatible.js +45 -0
  210. package/src/platforms/drivers/generic-prompt.js +41 -0
  211. package/src/platforms/drivers/legacy.js +677 -0
  212. package/src/platforms/drivers/native-config-snapshot.js +125 -0
  213. package/src/platforms/drivers/native-log-utils.js +1292 -0
  214. package/src/platforms/drivers/notification-hooks.js +578 -0
  215. package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
  216. package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
  217. package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
  218. package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
  219. package/src/platforms/drivers/omp/channels.js +58 -0
  220. package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
  221. package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
  222. package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
  223. package/src/platforms/drivers/omp/mcp.js +9 -0
  224. package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
  225. package/src/platforms/drivers/omp/native-config.js +46 -0
  226. package/src/platforms/drivers/omp/native-logs.js +52 -0
  227. package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
  228. package/src/platforms/drivers/omp/project-config.js +29 -0
  229. package/src/platforms/drivers/omp/projects.js +20 -0
  230. package/src/platforms/drivers/omp/prompts.js +9 -0
  231. package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
  232. package/src/platforms/drivers/omp/proxy.js +16 -0
  233. package/src/platforms/drivers/omp/resource-sync.js +9 -0
  234. package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
  235. package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
  236. package/src/platforms/drivers/omp/sessions.js +36 -0
  237. package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
  238. package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
  239. package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
  240. package/src/platforms/drivers/omp/statistics.js +16 -0
  241. package/src/platforms/drivers/opencode/api-operations.js +9 -0
  242. package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
  243. package/src/platforms/drivers/opencode/channels.js +40 -0
  244. package/src/platforms/drivers/opencode/conversion.js +41 -0
  245. package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
  246. package/src/platforms/drivers/opencode/mcp.js +9 -0
  247. package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
  248. package/src/platforms/drivers/opencode/native-config.js +26 -0
  249. package/src/platforms/drivers/opencode/native-logs.js +133 -0
  250. package/src/platforms/drivers/opencode/project-config.js +98 -0
  251. package/src/platforms/drivers/opencode/projects.js +20 -0
  252. package/src/platforms/drivers/opencode/prompts.js +9 -0
  253. package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
  254. package/src/platforms/drivers/opencode/proxy.js +16 -0
  255. package/src/platforms/drivers/opencode/resource-sync.js +9 -0
  256. package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
  257. package/src/platforms/drivers/opencode/sessions.js +48 -0
  258. package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
  259. package/src/platforms/drivers/opencode/statistics.js +16 -0
  260. package/src/platforms/drivers/secure-file-driver.js +272 -0
  261. package/src/platforms/drivers/unsupported.js +7 -0
  262. package/src/platforms/manifest-schema.js +398 -0
  263. package/src/platforms/manifests/claude.json +489 -0
  264. package/src/platforms/manifests/codex.json +448 -0
  265. package/src/platforms/manifests/gemini.json +447 -0
  266. package/src/platforms/manifests/omp.json +496 -0
  267. package/src/platforms/manifests/opencode.json +422 -0
  268. package/src/{server/services → platforms}/native-oauth-adapters.js +87 -417
  269. package/src/{server/services → platforms}/notification-hooks.js +140 -1023
  270. package/src/platforms/oauth-credentials-service.js +506 -0
  271. package/src/platforms/path-resolver.js +179 -0
  272. package/src/platforms/platform-path-context.js +305 -0
  273. package/src/platforms/registry.js +248 -0
  274. package/src/platforms/remote-notification-providers.js +678 -0
  275. package/src/platforms/runtime.js +259 -0
  276. package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
  277. package/src/plugins/plugin-loader.js +2 -2
  278. package/src/plugins/plugin-manager.js +2 -2
  279. package/src/plugins/registry.js +27 -10
  280. package/src/reset-config.js +21 -5
  281. package/src/server/api/agents.js +46 -72
  282. package/src/server/api/commands.js +16 -6
  283. package/src/server/api/config-registry.js +206 -59
  284. package/src/server/api/config-sync.js +3 -2
  285. package/src/server/api/config-templates.js +20 -4
  286. package/src/server/api/config.js +111 -102
  287. package/src/server/api/convert.js +54 -178
  288. package/src/server/api/dashboard.js +168 -102
  289. package/src/server/api/hooks.js +1 -1
  290. package/src/server/api/mcp.js +224 -84
  291. package/src/server/api/platform-api-config.js +102 -0
  292. package/src/server/api/platform-route-factory.js +215 -0
  293. package/src/server/api/platform-routes.js +26 -0
  294. package/src/server/api/platforms.js +110 -0
  295. package/src/server/api/plugins.js +57 -57
  296. package/src/server/api/project-config.js +168 -0
  297. package/src/server/api/prompts.js +58 -45
  298. package/src/server/api/settings.js +55 -8
  299. package/src/server/api/skills.js +262 -133
  300. package/src/server/api/ui-config.js +2 -2
  301. package/src/server/api/validation-errors.js +18 -4
  302. package/src/server/api/workspaces.js +47 -12
  303. package/src/server/index.js +164 -213
  304. package/src/server/platform-context.js +81 -0
  305. package/src/server/services/channel-balance.js +11 -20
  306. package/src/server/services/channel-health.js +21 -6
  307. package/src/server/services/channel-scheduler.js +29 -39
  308. package/src/server/services/channel-sync-utils.js +23 -13
  309. package/src/server/services/config-export-service.js +350 -319
  310. package/src/server/services/config-registry-service.js +280 -54
  311. package/src/server/services/config-sync-manager.js +172 -944
  312. package/src/server/services/config-templates-service.js +419 -353
  313. package/src/server/services/control-manifest-store.js +206 -0
  314. package/src/server/services/dashboard-snapshot-worker.js +656 -100
  315. package/src/server/services/effective-control-service.js +557 -0
  316. package/src/server/services/local-resource-index.js +314 -0
  317. package/src/server/services/mcp-service.js +607 -977
  318. package/src/server/services/model-detector.js +15 -10
  319. package/src/server/services/native-log-channel-resolver.js +214 -0
  320. package/src/server/services/native-log-observer.js +291 -0
  321. package/src/server/services/network-access.js +1 -1
  322. package/src/server/services/platform-catalog.js +82 -0
  323. package/src/server/services/platform-resolution.js +32 -29
  324. package/src/server/services/plugins-service.js +376 -122
  325. package/src/server/services/project-config-service.js +550 -0
  326. package/src/server/services/project-path-validation.js +31 -0
  327. package/src/server/services/prompts-service.js +267 -233
  328. package/src/server/services/proxy-runtime.js +7 -4
  329. package/src/server/services/remote-credential-cache.js +43 -0
  330. package/src/server/services/repo-scanner-base.js +182 -75
  331. package/src/server/services/request-logger.js +7 -4
  332. package/src/server/services/session-history-index.js +780 -213
  333. package/src/server/services/session-history-worker.js +228 -33
  334. package/src/server/services/session-snapshot-worker.js +124 -34
  335. package/src/server/services/session-snapshots.js +4 -2
  336. package/src/server/services/skill-artifact-store.js +355 -0
  337. package/src/server/services/skill-format-adapters.js +65 -0
  338. package/src/server/services/skill-projection-service.js +323 -0
  339. package/src/server/services/skill-refresh-task-service.js +275 -0
  340. package/src/server/services/skill-service.js +2020 -677
  341. package/src/server/services/snapshot-cache.js +30 -8
  342. package/src/server/services/speed-test.js +8 -8
  343. package/src/server/services/sqlite-connection.js +19 -3
  344. package/src/server/services/statistics-service.js +240 -57
  345. package/src/server/services/ui-config.js +107 -76
  346. package/src/server/services/usage-log-utils.js +98 -0
  347. package/src/server/services/web-build.js +1 -1
  348. package/src/server/services/workspace-service.js +59 -29
  349. package/src/server/utils/pricing.js +10 -19
  350. package/src/server/websocket-server.js +153 -28
  351. package/src/{server/services/base → shared}/base-channel-service.js +156 -28
  352. package/src/shared/capability-driver.js +104 -0
  353. package/src/shared/channel-auth-policy.js +27 -0
  354. package/src/shared/channel-driver.js +161 -0
  355. package/src/shared/driver-factories/api.js +180 -0
  356. package/src/shared/driver-factories/mcp.js +244 -0
  357. package/src/shared/driver-factories/projects.js +121 -0
  358. package/src/shared/driver-factories/prompts.js +58 -0
  359. package/src/shared/driver-factories/resource-sync.js +375 -0
  360. package/src/shared/driver-result.js +78 -0
  361. package/src/shared/mcp-format.js +189 -0
  362. package/src/shared/platforms.js +104 -167
  363. package/src/shared/project-config.js +512 -0
  364. package/src/shared/proxy-driver.js +57 -0
  365. package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
  366. package/src/ui/menu.js +91 -67
  367. package/src/utils/port-helper.js +35 -19
  368. package/src/utils/session.js +1 -1
  369. package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
  370. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  371. package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
  372. package/dist/web/assets/Home-DCEyLzwX.js +0 -1
  373. package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
  374. package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
  375. package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
  376. package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
  377. package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
  378. package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
  379. package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
  380. package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
  381. package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
  382. package/dist/web/assets/icons-nGdSKQn2.js +0 -1
  383. package/dist/web/assets/index-D5hyHPj7.js +0 -2
  384. package/dist/web/assets/index-bJRG3RXH.css +0 -1
  385. package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
  386. package/dist/web/assets/markdown-BfC0goYb.css +0 -10
  387. package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
  388. package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
  389. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
  390. package/src/config/model-pricing.js +0 -35
  391. package/src/server/api/channels.js +0 -439
  392. package/src/server/api/claude-hooks.js +0 -56
  393. package/src/server/api/claude-statistics.js +0 -57
  394. package/src/server/api/codex-channels.js +0 -491
  395. package/src/server/api/codex-projects.js +0 -117
  396. package/src/server/api/codex-proxy.js +0 -293
  397. package/src/server/api/codex-sessions.js +0 -652
  398. package/src/server/api/codex-statistics.js +0 -57
  399. package/src/server/api/gemini-channels.js +0 -392
  400. package/src/server/api/gemini-projects.js +0 -103
  401. package/src/server/api/gemini-proxy.js +0 -231
  402. package/src/server/api/gemini-sessions.js +0 -550
  403. package/src/server/api/gemini-statistics.js +0 -57
  404. package/src/server/api/health-check.js +0 -31
  405. package/src/server/api/oauth-credentials.js +0 -198
  406. package/src/server/api/omp-channels.js +0 -575
  407. package/src/server/api/omp-projects.js +0 -88
  408. package/src/server/api/omp-proxy.js +0 -125
  409. package/src/server/api/omp-sessions.js +0 -386
  410. package/src/server/api/omp-statistics.js +0 -40
  411. package/src/server/api/opencode-channels.js +0 -471
  412. package/src/server/api/opencode-projects.js +0 -110
  413. package/src/server/api/opencode-proxy.js +0 -354
  414. package/src/server/api/opencode-sessions.js +0 -466
  415. package/src/server/api/opencode-statistics.js +0 -57
  416. package/src/server/api/projects.js +0 -120
  417. package/src/server/api/proxy.js +0 -330
  418. package/src/server/api/sessions.js +0 -810
  419. package/src/server/services/oauth-credentials-service.js +0 -945
  420. package/src/server/services/omp-session-log-observer.js +0 -138
  421. package/src/server/services/opencode-gateway-adapters.js +0 -43
  422. package/src/server/services/project-snapshots.js +0 -59
  423. package/src/server/services/proxy-log-helper.js +0 -298
  424. package/src/server/services/session-cache.js +0 -127
  425. package/src/server/services/session-converter.js +0 -582
  426. package/src/server/services/session-history-adapters/claude.js +0 -235
  427. /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
  428. /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
  429. /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
  430. /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
  431. /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
  432. /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
  433. /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
  434. /package/src/{server/services/base → shared}/proxy-utils.js +0 -0
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const { openDatabase, closeDatabase } = require('./sqlite-connection');
4
+
4
5
  const { PATHS } = require('../../config/paths');
6
+ const platformRuntime = require('../../platforms/runtime');
5
7
  const fs = require('fs');
6
8
  const path = require('path');
7
9
 
@@ -12,11 +14,93 @@ const path = require('path');
12
14
  /** @type {number} Maximum time a source inventory is considered fresh */
13
15
  const INDEX_INVENTORY_TTL_MS = 30000;
14
16
 
15
- /** @type {number} Max wait for stale-ok on a cold source */
16
- const COLD_STALE_WAIT_MS = 2500;
17
17
 
18
- /** @type {number} Hard timeout for worker processes */
19
- const WORKER_TIMEOUT_MS = 180000;
18
+ /** @type {number} Maximum time a cold stale-ok read waits for inventory */
19
+ const INDEX_COLD_WAIT_MS = 1500;
20
+ /** @type {number} Maximum age for a lock whose owner cannot be inspected */
21
+ const INVENTORY_LOCK_STALE_MS = 30 * 60 * 1000;
22
+ /** @type {number} Number of files parsed before results are released */
23
+ const INVENTORY_PARSE_BATCH_SIZE = 8;
24
+ /** @type {number} Maximum number of files parsed concurrently */
25
+ const INVENTORY_PARSE_CONCURRENCY = 2;
26
+ const BUILTIN_SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'omp']);
27
+ // Keep synchronized with the parserVersion exposed by the built-in session drivers.
28
+ const BUILTIN_SESSION_PARSER_VERSIONS = Object.freeze({ claude: 2, codex: 2, gemini: 2, omp: 2 });
29
+ function _parserVersionForDriver(driver) {
30
+ const version = Number(driver?.parserVersion);
31
+ return Number.isInteger(version) && version >= 1 ? version : 1;
32
+ }
33
+ function _isUsableRuntime(runtime) {
34
+ return !!runtime && typeof runtime === 'object' && typeof runtime.getDriver === 'function';
35
+ }
36
+
37
+ function _inventoryLockPath(dbPath, source) {
38
+ const sourceKey = Buffer.from(String(source), 'utf8').toString('hex');
39
+ return `${dbPath}.${sourceKey}.inventory.lock`;
40
+ }
41
+
42
+ function _isProcessAlive(pid) {
43
+ if (!Number.isInteger(pid) || pid <= 0) return false;
44
+ try {
45
+ process.kill(pid, 0);
46
+ return true;
47
+ } catch (error) {
48
+ return error && error.code === 'EPERM';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Acquire a durable per-database/source lock. The in-memory single-flight
54
+ * map only protects requests inside one Node process; production indexing is
55
+ * deliberately performed in child processes, so it cannot prevent two
56
+ * workers from rebuilding the same source after a parent restart.
57
+ */
58
+ function _tryAcquireInventoryLock(dbPath, source) {
59
+ const lockPath = _inventoryLockPath(dbPath, source);
60
+
61
+ for (let attempt = 0; attempt < 2; attempt++) {
62
+ let fd;
63
+ try {
64
+ fd = fs.openSync(lockPath, 'wx');
65
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), 'utf8');
66
+ return { fd, lockPath };
67
+ } catch (error) {
68
+ if (fd !== undefined) {
69
+ try { fs.closeSync(fd); } catch (_err) {}
70
+ }
71
+ if (error?.code !== 'EEXIST') throw error;
72
+ }
73
+
74
+ let owner = null;
75
+ let ageMs = 0;
76
+ try {
77
+ owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
78
+ ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
79
+ } catch (error) {
80
+ if (error?.code === 'ENOENT') continue;
81
+ }
82
+
83
+ const ownerPid = Number(owner?.pid);
84
+ if (_isProcessAlive(ownerPid) || (!Number.isFinite(ageMs) || ageMs < INVENTORY_LOCK_STALE_MS)) {
85
+ return null;
86
+ }
87
+
88
+ try {
89
+ fs.unlinkSync(lockPath);
90
+ } catch (error) {
91
+ if (error?.code !== 'ENOENT') return null;
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ function _releaseInventoryLock(lock) {
99
+ if (!lock) return;
100
+ try { fs.closeSync(lock.fd); } catch (_err) {}
101
+ try { fs.unlinkSync(lock.lockPath); } catch (_err) {}
102
+ }
103
+
20
104
 
21
105
  // ---------------------------------------------------------------------------
22
106
  // Schema
@@ -49,6 +133,7 @@ CREATE TABLE IF NOT EXISTS session_file (
49
133
  message_count INTEGER NOT NULL DEFAULT 0,
50
134
  usage_json TEXT,
51
135
  extra_json TEXT,
136
+ parser_version INTEGER NOT NULL DEFAULT 0,
52
137
  PRIMARY KEY (source, file_path),
53
138
  UNIQUE (source, session_id)
54
139
  );
@@ -80,6 +165,9 @@ CREATE TABLE IF NOT EXISTS session_message (
80
165
 
81
166
  CREATE INDEX IF NOT EXISTS idx_session_message_sid_ord
82
167
  ON session_message(source, session_id, ordinal);
168
+
169
+ CREATE INDEX IF NOT EXISTS idx_session_message_user_number
170
+ ON session_message(source, session_id, user_message_number, ordinal);
83
171
  `;
84
172
 
85
173
  const FTS_SETUP_SQL = `
@@ -144,27 +232,197 @@ function _ftsQuote(s) {
144
232
  return '"' + String(s).replace(/"/g, '""') + '"';
145
233
  }
146
234
 
147
- // ---------------------------------------------------------------------------
148
- // Core: createSessionHistoryIndex
235
+ function _normalizeTypedSessionsResult(result, defaultOperation = 'resolve-driver') {
236
+ if (!result || typeof result !== 'object') {
237
+ return null;
238
+ }
239
+ if (result.status !== 'failed' && result.status !== 'unsupported') {
240
+ return null;
241
+ }
242
+ if (typeof result.platform !== 'string' || typeof result.capability !== 'string') {
243
+ return null;
244
+ }
245
+ return {
246
+ ...result,
247
+ operation: typeof result.operation === 'string' ? result.operation : defaultOperation
248
+ };
249
+ }
250
+
251
+ function _isTypedSessionsResult(result) {
252
+ return !!_normalizeTypedSessionsResult(result);
253
+ }
254
+
255
+ function _typedSessionsFailure(source, error, operation = 'resolve-driver') {
256
+ const result = {
257
+ status: 'failed',
258
+ platform: source,
259
+ capability: 'sessions',
260
+ operation,
261
+ error: error && error.message ? error.message : String(error)
262
+ };
263
+ if (error) {
264
+ Object.defineProperty(result, 'cause', { value: error, enumerable: false });
265
+ }
266
+ return result;
267
+ }
268
+
269
+ function _typedSessionsUnsupported(source, error, operation = 'resolve-driver') {
270
+ const result = {
271
+ status: 'unsupported',
272
+ platform: source,
273
+ capability: 'sessions',
274
+ operation,
275
+ error: error && error.message ? error.message : String(error)
276
+ };
277
+ if (error) {
278
+ Object.defineProperty(result, 'cause', { value: error, enumerable: false });
279
+ }
280
+ return result;
281
+ }
282
+
283
+ function _getRuntimeSessionsDriver(runtime, source, config = {}) {
284
+ if (!runtime || typeof runtime.getDriver !== 'function') {
285
+ return null;
286
+ }
287
+
288
+ try {
289
+ const driver = runtime.getDriver(source, 'sessions', { config });
290
+ const typedResult = _normalizeTypedSessionsResult(driver);
291
+ if (typedResult) {
292
+ return typedResult;
293
+ }
294
+ if (!driver || typeof driver !== 'object') {
295
+ return null;
296
+ }
297
+ if (typeof driver.inventory !== 'function' || typeof driver.parse !== 'function') {
298
+ return null;
299
+ }
300
+ return driver;
301
+ } catch (error) {
302
+ return _typedSessionsFailure(source, error);
303
+ }
304
+ }
305
+
306
+ function _safeParseJson(value, fallback) {
307
+ if (!value) return fallback;
308
+ try {
309
+ return JSON.parse(value);
310
+ } catch (_) {
311
+ return fallback;
312
+ }
313
+ }
314
+
315
+ function _safeParseObject(value) {
316
+ const parsed = _safeParseJson(value, {});
317
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
318
+ return {};
319
+ }
320
+ return parsed;
321
+ }
322
+
323
+
324
+ function _normalizeRuntimeParseResult(result, descriptor = {}, { preservePayloadUpdatedAt = false } = {}) {
325
+ if (!result || typeof result !== 'object') {
326
+ return result;
327
+ }
328
+
329
+ if (result.session && Array.isArray(result.messages)) {
330
+ const sessionExtra = _safeParseObject(result.session.extraJson);
331
+ const extraJson = JSON.stringify({
332
+ ...sessionExtra,
333
+ projectHint: result.session.projectHint || descriptor.projectHint || null,
334
+ mtimeMs: descriptor.mtimeMs ?? sessionExtra.mtimeMs ?? null
335
+ });
336
+ return {
337
+ ...result,
338
+ session: {
339
+ ...result.session,
340
+ projectHint: result.session.projectHint || descriptor.projectHint || '',
341
+ projectName: result.session.projectName || result.session.projectHint || descriptor.projectHint || '',
342
+ projectDisplayName: result.session.projectDisplayName || result.session.projectName || result.session.projectHint || descriptor.projectHint || '',
343
+ projectFullPath: result.session.projectFullPath || result.session.projectPath || '',
344
+ updatedAt: preservePayloadUpdatedAt ? (result.session.updatedAt ?? descriptor.mtimeMs ?? null) : descriptor.mtimeMs ?? null,
345
+ extraJson
346
+ }
347
+ };
348
+ }
349
+
350
+ if (result.sessionId) {
351
+ const projectHint = descriptor.projectHint || result.projectHint || result.projectName || '';
352
+ const projectName = result.projectName || projectHint || '';
353
+ const projectDisplayName = result.projectDisplayName || result.projectName || projectHint || '';
354
+ const resultExtra = _safeParseObject(result.extraJson);
355
+ const descriptorMtime = descriptor.mtimeMs ?? null;
356
+
357
+ return {
358
+ session: {
359
+ sessionId: result.sessionId,
360
+ projectHint,
361
+ projectName,
362
+ projectDisplayName,
363
+ projectFullPath: result.projectFullPath || result.projectPath || '',
364
+ firstMessage: result.firstMessage || null,
365
+ gitBranch: result.gitBranch || null,
366
+ provider: result.provider || null,
367
+ model: result.model || null,
368
+ startedAt: result.startedAt || null,
369
+ updatedAt: descriptorMtime ?? result.updatedAt ?? null,
370
+ usageJson: result.usageJson || null,
371
+ extraJson: JSON.stringify({
372
+ ...resultExtra,
373
+ projectHint: projectHint || null,
374
+ mtimeMs: descriptor.mtimeMs ?? resultExtra.mtimeMs ?? null
375
+ })
376
+ },
377
+ messages: Array.isArray(result.messages) ? result.messages : []
378
+ };
379
+ }
380
+
381
+ return result;
382
+ }
383
+
384
+ function _adaptRuntimeSessionsDriver(driver) {
385
+ if (!driver) return null;
386
+ const unwrap = result => result && result.status === 'ok' ? result.data : result;
387
+ return {
388
+ parserVersion: driver.parserVersion,
389
+ inventory: async (...args) => unwrap(await driver.inventory(...args)),
390
+ parse: async (descriptor, ...args) => _normalizeRuntimeParseResult(
391
+ await unwrap(await driver.parse(descriptor, ...args)),
392
+ descriptor,
393
+ { preservePayloadUpdatedAt: driver.preservePayloadUpdatedAt === true }
394
+ )
395
+ };
396
+ }
149
397
  // ---------------------------------------------------------------------------
150
398
 
151
399
  /**
152
400
  * @param {object} [opts]
153
401
  * @param {string} [opts.dbPath]
154
402
  * @param {object} [opts.adapterRegistry] - source → { inventory, parse }
403
+ * @param {object} [opts.runtime] - runtime with getDriver(source, capability)
155
404
  * @param {Function} [opts.workerRunner] - (source, indexDbPath) => Promise<void>
156
405
  * @param {boolean|null} [opts.ftsEnabledOverride] - override FTS detection for tests
157
406
  * @returns {object} index API
158
407
  */
159
408
  function createSessionHistoryIndex(opts = {}) {
160
409
  const dbPath = opts.dbPath || PATHS?.sessionHistoryIndex || path.join(PATHS?.base || process.cwd(), 'session-history.sqlite');
161
- const adapters = opts.adapterRegistry || require('./session-history-adapters');
410
+ const explicitProjectsDir = opts.projectsDir;
411
+ const indexConfig = opts.config || (opts.projectsDir ? { projectsDir: opts.projectsDir } : {});
412
+ const explicitAdapters = opts.adapterRegistry || null;
413
+ const adapters = explicitAdapters || {};
414
+ const runtimeProvided = !explicitAdapters && process.env.NODE_ENV === 'test' && _isUsableRuntime(opts.runtime);
415
+ const runtime = explicitAdapters ? null : (runtimeProvided ? opts.runtime : platformRuntime.getPlatformRuntime());
162
416
  const workerRunner = opts.workerRunner || _defaultWorkerRunner;
163
417
  const ftsEnabled = opts.ftsEnabledOverride !== undefined
164
418
  ? opts.ftsEnabledOverride
165
419
  : null;
166
420
  const shouldUseWorker = process.env.NODE_ENV !== 'test' && process.env.CC_TOOL_SESSION_HISTORY_CHILD !== '1';
167
421
  let _db = null;
422
+ /** @type {Map<string, {size: number, mtimeMs: number, checkedAt: number, filePath: string}>} */
423
+ const fileVersions = new Map();
424
+ const fileChecks = new Map();
425
+ const sourceFreshness = new Map();
168
426
  let _ftsAvailable = null;
169
427
  /** @type {Map<string, Promise<void>>} */
170
428
  const _inflight = new Map();
@@ -183,10 +441,68 @@ function createSessionHistoryIndex(opts = {}) {
183
441
  }
184
442
  return _db;
185
443
  }
444
+ function _parserVersionForSource(source, adapter = null) {
445
+ if (adapter) return _parserVersionForDriver(adapter);
446
+ if (explicitAdapters) return _parserVersionForDriver(adapters[source]);
447
+ const runtimeDriver = _getRuntimeSessionsDriver(runtime, source, indexConfig);
448
+ return _parserVersionForDriver(runtimeDriver);
449
+ }
450
+
451
+ /**
452
+ * Check only the persisted inventory timestamp. Inventory owns file checks.
453
+ *
454
+ * @param {string} source
455
+ * @returns {boolean}
456
+ */
457
+ function _isSourceFresh(source) {
458
+ const row = _getDb().prepare(
459
+ 'SELECT last_inventory_ms FROM source_state WHERE source = ?'
460
+ ).get(source);
461
+ return Boolean(
462
+ row?.last_inventory_ms
463
+ && Date.now() - Number(row.last_inventory_ms) < INDEX_INVENTORY_TTL_MS
464
+ && !_hasKnownParserMigrationPending(source)
465
+ && (!explicitAdapters || !_hasParserMigrationPending(source))
466
+ );
467
+ }
468
+
469
+ function _hasKnownParserMigrationPending(source) {
470
+ if (explicitAdapters || runtimeProvided) return false;
471
+ const parserVersion = BUILTIN_SESSION_PARSER_VERSIONS[source];
472
+ if (!parserVersion) return false;
473
+ const row = _getDb().prepare(
474
+ 'SELECT 1 AS pending FROM session_file WHERE source = ? AND parser_version <> ? LIMIT 1'
475
+ ).get(source, parserVersion);
476
+ return Boolean(row);
477
+ }
478
+
479
+ function _hasParserMigrationPending(source) {
480
+ const row = _getDb().prepare(
481
+ 'SELECT 1 AS pending FROM session_file WHERE source = ? AND parser_version <> ? LIMIT 1'
482
+ ).get(source, _parserVersionForSource(source));
483
+ return Boolean(row);
484
+ }
485
+
486
+ function _hasIndexedData(source) {
487
+ const row = _getDb().prepare(
488
+ 'SELECT 1 AS indexed FROM session_file WHERE source = ? LIMIT 1'
489
+ ).get(source);
490
+ return Boolean(row);
491
+ }
492
+
493
+ function _hasUsableIndexedData(source) {
494
+ return _hasIndexedData(source)
495
+ && !_hasKnownParserMigrationPending(source)
496
+ && (!explicitAdapters || !_hasParserMigrationPending(source));
497
+ }
186
498
 
187
499
  function _initSchema(db) {
188
500
  db.exec('PRAGMA journal_mode = WAL');
189
501
  db.exec(SCHEMA_SQL);
502
+ const columns = db.prepare('PRAGMA table_info(session_file)').all();
503
+ if (!columns.some((column) => column.name === 'parser_version')) {
504
+ db.exec('ALTER TABLE session_file ADD COLUMN parser_version INTEGER NOT NULL DEFAULT 0');
505
+ }
190
506
  }
191
507
 
192
508
  function _initFts(db) {
@@ -199,28 +515,41 @@ function createSessionHistoryIndex(opts = {}) {
199
515
 
200
516
  /**
201
517
  * @param {string} source
202
- * @param {{ force?: boolean, consistency?: string }} [options]
518
+ * @param {{ force?: boolean, consistency?: string, allowStaleData?: boolean }} [options]
203
519
  * @returns {Promise<void>}
204
520
  */
205
521
  async function ensureSourceIndexed(source, options = {}) {
206
522
  const consistency = options.consistency || 'stale-ok';
207
523
  const force = options.force === true;
208
-
524
+ const allowStaleData = options.allowStaleData === true;
209
525
  const key = `ensure:${source}`;
526
+
527
+ if (!force && _isSourceFresh(source)) {
528
+ return;
529
+ }
530
+
210
531
  if (_inflight.has(key)) {
211
532
  if (consistency === 'complete') {
212
533
  return _inflight.get(key);
213
534
  }
214
- const inflight = _inflight.get(key);
215
- const timeout = new Promise((resolve) => setTimeout(() => resolve('timeout'), COLD_STALE_WAIT_MS));
216
- const result = await Promise.race([inflight, timeout]);
217
- if (result === 'timeout') return;
218
- return result;
535
+ // The project directory can safely use the previous rows while a
536
+ // parser migration or inventory refresh is running. Session reads
537
+ // retain their existing behavior and wait for migration completion.
538
+ if (allowStaleData ? _hasIndexedData(source) : _hasUsableIndexedData(source)) {
539
+ return;
540
+ }
541
+ return _inflight.get(key);
219
542
  }
220
543
 
221
- const promise = shouldUseWorker
222
- ? workerRunner(source, dbPath, { force })
223
- : _runInventory(source, { force });
544
+ const useWorker = shouldUseWorker && !runtimeProvided && !explicitAdapters;
545
+ const workerOptions = { force };
546
+ const workerProjectsDir = source === 'claude'
547
+ ? (options.config?.projectsDir || explicitProjectsDir)
548
+ : explicitProjectsDir;
549
+ if (workerProjectsDir) workerOptions.projectsDir = workerProjectsDir;
550
+ const promise = useWorker
551
+ ? workerRunner(source, dbPath, workerOptions)
552
+ : _runInventory(source, { force, config: options.config || indexConfig });
224
553
  _inflight.set(key, promise);
225
554
  promise.finally(() => {
226
555
  if (_inflight.get(key) === promise) {
@@ -228,166 +557,298 @@ function createSessionHistoryIndex(opts = {}) {
228
557
  }
229
558
  }).catch(() => {});
230
559
 
560
+ if (consistency === 'stale-ok' && (allowStaleData ? _hasIndexedData(source) : _hasUsableIndexedData(source))) {
561
+ return;
562
+ }
563
+
231
564
  if (consistency === 'stale-ok') {
232
- const timeout = new Promise((resolve) => setTimeout(() => resolve('timeout'), COLD_STALE_WAIT_MS));
233
- const result = await Promise.race([promise, timeout]);
234
- if (result === 'timeout') {
235
- return;
236
- }
565
+ await Promise.race([
566
+ promise,
567
+ new Promise(resolve => setTimeout(resolve, INDEX_COLD_WAIT_MS))
568
+ ]);
569
+ return;
237
570
  }
238
571
 
239
572
  await promise;
240
573
  }
574
+ function _isTypedFailureResult(result) {
575
+ return _isTypedSessionsResult(result);
576
+ }
577
+ function _typedFailureToError(result) {
578
+ const fallbackMessage = result.status === 'unsupported'
579
+ ? `unsupported ${result.capability}`
580
+ : 'inventory failed';
581
+ const cause = result.cause || (result.error instanceof Error ? result.error : new Error(String(result.error || fallbackMessage)));
582
+ const error = new Error(`Runtime ${result.capability} ${result.operation} ${result.status} on ${result.platform}: ${cause.message}`);
583
+ error.status = result.status;
584
+ error.platform = result.platform;
585
+ error.capability = result.capability;
586
+ error.operation = result.operation;
587
+ error.context = result;
588
+ error.cause = cause;
589
+ error.failure = result;
590
+ return error;
591
+ }
241
592
 
242
- async function _runInventory(source, { force = false } = {}) {
593
+ function _recordSourceState(db, source, lastInventoryMs, lastError) {
594
+ db.prepare(
595
+ 'INSERT INTO source_state(source, last_inventory_ms, last_error) VALUES(?, ?, ?) ON CONFLICT(source) DO UPDATE SET last_inventory_ms = excluded.last_inventory_ms, last_error = excluded.last_error'
596
+ ).run(source, lastInventoryMs, lastError);
597
+ }
598
+
599
+ async function _runInventory(source, { force = false, config = indexConfig } = {}) {
243
600
  const db = _getDb();
244
- const adapter = adapters[source];
245
- if (!adapter || !adapter.inventory) {
601
+ const inventoryLock = _tryAcquireInventoryLock(dbPath, source);
602
+ if (!inventoryLock) {
603
+ // Another process is already doing the expensive work. Existing rows
604
+ // remain readable, and the next scheduled inventory will observe the
605
+ // new source_state timestamp once that worker finishes.
246
606
  return;
247
607
  }
608
+ let errorMsg = null;
609
+ let stateToRecord = null;
248
610
 
249
- if (!force) {
250
- const row = db.prepare('SELECT last_inventory_ms FROM source_state WHERE source = ?').get(source);
251
- if (row && row.last_inventory_ms && (Date.now() - row.last_inventory_ms) < INDEX_INVENTORY_TTL_MS) {
611
+ try {
612
+ if (!force && _isSourceFresh(source)) {
252
613
  return;
253
614
  }
254
- }
615
+ let adapter = null;
255
616
 
256
- const descriptors = await adapter.inventory();
257
- if (!Array.isArray(descriptors)) return;
617
+ if (explicitAdapters) {
618
+ adapter = adapters[source];
619
+ } else {
620
+ const runtimeDriver = _getRuntimeSessionsDriver(runtime, source, config);
621
+ if (_isTypedFailureResult(runtimeDriver)) {
622
+ throw _typedFailureToError(runtimeDriver);
623
+ }
258
624
 
259
- const seen = new Map();
260
- for (const d of descriptors) {
261
- const prev = seen.get(d.sessionId);
262
- if (!prev || d.mtimeMs > prev.mtimeMs || (d.mtimeMs === prev.mtimeMs && d.filePath < prev.filePath)) {
263
- seen.set(d.sessionId, d);
625
+ if (runtimeDriver) {
626
+ adapter = _adaptRuntimeSessionsDriver(runtimeDriver);
627
+ } else if (BUILTIN_SESSION_SOURCES.has(source) && adapters[source]) {
628
+ adapter = adapters[source];
629
+ } else {
630
+ throw _typedFailureToError(_typedSessionsUnsupported(source, new Error(`unsupported sessions source: ${source}`)));
631
+ }
264
632
  }
265
- }
266
-
267
- const indexedFiles = new Map();
268
- const indexedRows = db.prepare('SELECT file_path, size, mtime_ms FROM session_file WHERE source = ?').all(source);
269
- for (const r of indexedRows) {
270
- indexedFiles.set(r.file_path, r);
271
- }
272
-
273
- const toParse = [];
274
- const activePaths = new Set();
275
- for (const d of seen.values()) {
276
- activePaths.add(d.filePath);
277
- const idx = indexedFiles.get(d.filePath);
278
- if (!idx || idx.size !== d.size || idx.mtime_ms !== d.mtimeMs) {
279
- toParse.push(d);
633
+ const parserVersion = _parserVersionForSource(source, adapter);
634
+ const inventoryResult = await adapter.inventory({
635
+ projectsDir: source === 'claude' ? config.projectsDir : explicitProjectsDir
636
+ });
637
+ if (_isTypedFailureResult(inventoryResult)) {
638
+ throw _typedFailureToError(inventoryResult);
280
639
  }
281
- }
282
640
 
283
- for (const filePath of indexedFiles.keys()) {
284
- if (!activePaths.has(filePath)) {
285
- db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?').run(source, filePath);
641
+ const indexedFiles = new Map();
642
+ for (const row of db.prepare('SELECT file_path, size, mtime_ms, session_id, parser_version FROM session_file WHERE source = ?').all(source)) {
643
+ indexedFiles.set(row.file_path, {
644
+ size: row.size,
645
+ mtime_ms: row.mtime_ms,
646
+ sessionId: row.session_id,
647
+ parserVersion: row.parser_version
648
+ });
286
649
  }
287
- }
288
650
 
289
- let errorMsg = null;
290
- for (const d of toParse) {
291
- try {
292
- const preStat = { size: d.size, mtimeMs: d.mtimeMs };
293
- const { session, messages } = await adapter.parse(d);
294
-
295
- try {
296
- const postStat = fs.statSync(d.filePath);
297
- if (postStat.size !== preStat.size || postStat.mtimeMs !== preStat.mtimeMs) {
298
- const retry = await adapter.parse(d);
299
- const retryStat = fs.statSync(d.filePath);
300
- if (retryStat.size !== postStat.size || retryStat.mtimeMs !== postStat.mtimeMs) {
301
- const existing = db.prepare('SELECT 1 FROM session_file WHERE source = ? AND file_path = ?').get(source, d.filePath);
302
- if (existing) continue;
303
- continue;
304
- }
305
- _upsertSession(db, source, d, retry.session, retry.messages);
306
- continue;
307
- }
308
- } catch (_statErr) {
309
- continue;
651
+ const winnersBySessionId = new Map();
652
+ for (const descriptor of inventoryResult) {
653
+ const current = winnersBySessionId.get(descriptor.sessionId);
654
+ if (!current
655
+ || descriptor.mtimeMs > current.mtimeMs
656
+ || (descriptor.mtimeMs === current.mtimeMs && descriptor.filePath < current.filePath)) {
657
+ winnersBySessionId.set(descriptor.sessionId, descriptor);
310
658
  }
659
+ }
311
660
 
312
- _upsertSession(db, source, d, session, messages);
313
- } catch (err) {
314
- const existing = db.prepare('SELECT 1 FROM session_file WHERE source = ? AND file_path = ?').get(source, d.filePath);
315
- if (!existing) {
316
- errorMsg = (errorMsg ? errorMsg + '; ' : '') + `${d.filePath}: ${err.message}`;
661
+ const toParse = [];
662
+ const activePaths = new Set();
663
+ for (const d of winnersBySessionId.values()) {
664
+ activePaths.add(d.filePath);
665
+ const idx = indexedFiles.get(d.filePath);
666
+ if (!idx
667
+ || idx.size !== d.size
668
+ || idx.mtime_ms !== d.mtimeMs
669
+ || idx.parserVersion !== parserVersion) {
670
+ toParse.push(d);
317
671
  }
318
672
  }
319
- }
320
-
321
- db.prepare(
322
- 'INSERT INTO source_state(source, last_inventory_ms, last_error) VALUES(?, ?, ?) ON CONFLICT(source) DO UPDATE SET last_inventory_ms = excluded.last_inventory_ms, last_error = excluded.last_error'
323
- ).run(source, Date.now(), errorMsg);
324
- }
673
+ const parseDescriptor = async (d) => {
674
+ const preFingerprint = { size: d.size, mtimeMs: d.mtimeMs };
675
+ const parseResult = await adapter.parse({ ...d, projectsDir: config.projectsDir });
676
+ if (_isTypedFailureResult(parseResult)) throw _typedFailureToError(parseResult);
677
+ if (!parseResult || typeof parseResult !== 'object' || !parseResult.session || typeof parseResult.session !== 'object' || typeof parseResult.session.sessionId !== 'string' || !parseResult.session.sessionId.trim() || !Array.isArray(parseResult.messages)) {
678
+ throw new Error('invalid parsed session result');
679
+ }
325
680
 
326
- function _upsertSession(db, source, descriptor, session, messages) {
327
- try {
328
- db.exec('BEGIN IMMEDIATE');
329
- db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, session.sessionId);
681
+ let postStat;
682
+ try {
683
+ postStat = await fs.promises.stat(d.filePath);
684
+ } catch (_) {
685
+ return null;
686
+ }
687
+ if (postStat.size === preFingerprint.size && postStat.mtimeMs === preFingerprint.mtimeMs) {
688
+ return { descriptor: d, parseResult };
689
+ }
690
+ const retryDescriptor = { ...d, size: postStat.size, mtimeMs: postStat.mtimeMs };
691
+ const retryResult = await adapter.parse(retryDescriptor);
692
+ if (_isTypedFailureResult(retryResult)) {
693
+ throw _typedFailureToError(retryResult);
694
+ }
695
+ if (!retryResult || typeof retryResult !== 'object' || !retryResult.session || typeof retryResult.session !== 'object' || typeof retryResult.session.sessionId !== 'string' || !retryResult.session.sessionId.trim() || !Array.isArray(retryResult.messages)) {
696
+ throw new Error('invalid parsed session result');
697
+ }
698
+ let retryStat;
699
+ try {
700
+ retryStat = await fs.promises.stat(d.filePath);
701
+ } catch (_) {
702
+ return null;
703
+ }
704
+ if (retryStat.size !== postStat.size || retryStat.mtimeMs !== postStat.mtimeMs) {
705
+ return null;
706
+ }
707
+ return {
708
+ descriptor: { ...retryDescriptor, size: retryStat.size, mtimeMs: retryStat.mtimeMs },
709
+ parseResult: retryResult
710
+ };
711
+ };
330
712
 
331
- db.prepare(`
713
+ const deletePath = db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?');
714
+ const deleteSession = db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?');
715
+ const insertFile = db.prepare(`
332
716
  INSERT INTO session_file(
333
717
  source, file_path, size, mtime_ms, session_id,
334
718
  project_name, project_display_name, project_full_path,
335
719
  first_message, git_branch, provider, model,
336
- started_at, updated_at, message_count, usage_json, extra_json
337
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
338
- `).run(
339
- source,
340
- descriptor.filePath,
341
- descriptor.size,
342
- descriptor.mtimeMs,
343
- session.sessionId,
344
- session.projectName || '',
345
- session.projectDisplayName || null,
346
- session.projectFullPath || null,
347
- session.firstMessage || null,
348
- session.gitBranch || null,
349
- session.provider || null,
350
- session.model || null,
351
- session.startedAt || null,
352
- session.updatedAt || null,
353
- messages.length,
354
- session.usageJson || null,
355
- session.extraJson || null
356
- );
720
+ started_at, updated_at, message_count, usage_json, extra_json,
721
+ parser_version
722
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
723
+ `);
724
+ const insertMessage = db.prepare(`
725
+ INSERT INTO session_message(
726
+ source, session_id, ordinal, message_id, role, type, subtype,
727
+ content, timestamp, model, provider, user_message_number, extra_json
728
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
729
+ `);
357
730
 
358
- if (messages.length > 0) {
359
- const stmt = db.prepare(`
360
- INSERT INTO session_message(
361
- source, session_id, ordinal, message_id, role, type, subtype,
362
- content, timestamp, model, provider, user_message_number, extra_json
363
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
364
- `);
731
+ db.exec('BEGIN IMMEDIATE');
732
+ try {
733
+ for (const filePath of indexedFiles.keys()) {
734
+ if (!activePaths.has(filePath)) deletePath.run(source, filePath);
735
+ }
736
+ db.exec('COMMIT');
737
+ } catch (error) {
738
+ try { db.exec('ROLLBACK'); } catch (_) {}
739
+ throw error;
740
+ }
365
741
 
366
- let ordinal = 0;
367
- for (const msg of messages) {
368
- stmt.run(
369
- source,
370
- session.sessionId,
371
- ordinal,
372
- msg.messageId || null,
373
- msg.role || null,
374
- msg.type || null,
375
- msg.subtype || null,
376
- msg.content || null,
377
- msg.timestamp || null,
378
- msg.model || null,
379
- msg.provider || null,
380
- msg.userMessageNumber != null ? msg.userMessageNumber : null,
381
- msg.extraJson || null
382
- );
383
- ordinal++;
742
+ const recordParseError = (descriptor, error) => {
743
+ const detail = `${descriptor.filePath}: ${error.message}`;
744
+ errorMsg = errorMsg ? `${errorMsg}; ${detail}` : detail;
745
+ };
746
+
747
+ for (let batchStart = 0; batchStart < toParse.length; batchStart += INVENTORY_PARSE_BATCH_SIZE) {
748
+ const batch = toParse.slice(batchStart, batchStart + INVENTORY_PARSE_BATCH_SIZE);
749
+ const parsed = new Array(batch.length);
750
+ let next = 0;
751
+ const parseWorker = async () => {
752
+ while (true) {
753
+ const index = next++;
754
+ if (index >= batch.length) return;
755
+ const descriptor = batch[index];
756
+ try {
757
+ parsed[index] = { descriptor, value: await parseDescriptor(descriptor) };
758
+ } catch (error) {
759
+ parsed[index] = { descriptor, error };
760
+ }
761
+ }
762
+ };
763
+ await Promise.all(Array.from({ length: Math.min(INVENTORY_PARSE_CONCURRENCY, batch.length) }, parseWorker));
764
+
765
+ for (const item of parsed) {
766
+ if (item?.error) recordParseError(item.descriptor, item.error);
384
767
  }
768
+
769
+ db.exec('BEGIN IMMEDIATE');
770
+ try {
771
+ for (const item of parsed) {
772
+ if (item?.value) {
773
+ const { descriptor, parseResult } = item.value;
774
+ _insertSession({ deletePath, deleteSession, insertFile, insertMessage }, source, descriptor, parseResult.session, parseResult.messages, parserVersion);
775
+ }
776
+ }
777
+ db.exec('COMMIT');
778
+ } catch (error) {
779
+ try { db.exec('ROLLBACK'); } catch (_) {}
780
+ throw error;
781
+ }
782
+ // Drop the batch references before parsing the next group. This is
783
+ // the key difference from the old whole-inventory array: large
784
+ // session payloads no longer accumulate until the final COMMIT.
385
785
  }
386
786
 
387
- db.exec('COMMIT');
787
+ stateToRecord = {
788
+ lastInventoryMs: Date.now(),
789
+ lastError: errorMsg
790
+ };
388
791
  } catch (err) {
389
- try { db.exec('ROLLBACK'); } catch (_) {}
792
+ errorMsg = err && err.message ? err.message : String(err);
793
+ stateToRecord = {
794
+ lastInventoryMs: null,
795
+ lastError: errorMsg
796
+ };
390
797
  throw err;
798
+ } finally {
799
+ if (stateToRecord) {
800
+ try {
801
+ _recordSourceState(db, source, stateToRecord.lastInventoryMs, stateToRecord.lastError);
802
+ } catch (_err) {}
803
+ }
804
+ _releaseInventoryLock(inventoryLock);
805
+ }
806
+ }
807
+
808
+
809
+ function _insertSession(statements, source, descriptor, session, messages, parserVersion) {
810
+ const { deletePath, deleteSession, insertFile, insertMessage } = statements;
811
+ deletePath.run(source, descriptor.filePath);
812
+ deleteSession.run(source, session.sessionId);
813
+ insertFile.run(
814
+ source,
815
+ descriptor.filePath,
816
+ descriptor.size,
817
+ descriptor.mtimeMs,
818
+ session.sessionId,
819
+ session.projectName || '',
820
+ session.projectDisplayName || null,
821
+ session.projectFullPath || null,
822
+ session.firstMessage || null,
823
+ session.gitBranch || null,
824
+ session.provider || null,
825
+ session.model || null,
826
+ session.startedAt || null,
827
+ session.updatedAt || null,
828
+ messages.length,
829
+ session.usageJson || null,
830
+ session.extraJson || null,
831
+ parserVersion
832
+ );
833
+
834
+ let ordinal = 0;
835
+ for (const msg of messages) {
836
+ insertMessage.run(
837
+ source,
838
+ session.sessionId,
839
+ ordinal,
840
+ msg.messageId || null,
841
+ msg.role || null,
842
+ msg.type || null,
843
+ msg.subtype || null,
844
+ msg.content || null,
845
+ msg.timestamp || null,
846
+ msg.model || null,
847
+ msg.provider || null,
848
+ msg.userMessageNumber != null ? msg.userMessageNumber : null,
849
+ msg.extraJson || null
850
+ );
851
+ ordinal++;
391
852
  }
392
853
  }
393
854
 
@@ -401,7 +862,12 @@ function createSessionHistoryIndex(opts = {}) {
401
862
  * @returns {Promise<Array<{name, displayName, fullPath, path, sessionCount, lastUsed, latestSession, source}>>}
402
863
  */
403
864
  async function listProjects(source, options = {}) {
404
- await ensureSourceIndexed(source, { consistency: options.consistency || 'stale-ok' });
865
+ await ensureSourceIndexed(source, {
866
+ force: options.force === true,
867
+ consistency: options.consistency || 'stale-ok',
868
+ allowStaleData: true,
869
+ config: options.config
870
+ });
405
871
  const db = _getDb();
406
872
 
407
873
  const rows = db.prepare(`
@@ -436,7 +902,11 @@ function createSessionHistoryIndex(opts = {}) {
436
902
  * @returns {Promise<Array>}
437
903
  */
438
904
  async function listSessions(source, projectName, options = {}) {
439
- await ensureSourceIndexed(source, { consistency: 'stale-ok' });
905
+ await ensureSourceIndexed(source, {
906
+ force: options.force === true,
907
+ consistency: options.consistency || 'stale-ok',
908
+ config: options.config
909
+ });
440
910
 
441
911
  const db = _getDb();
442
912
  const rows = db.prepare(`
@@ -445,25 +915,30 @@ function createSessionHistoryIndex(opts = {}) {
445
915
  ORDER BY updated_at DESC
446
916
  `).all(source, projectName);
447
917
 
448
- return rows.map(r => ({
449
- sessionId: r.session_id,
450
- filePath: r.file_path,
451
- size: r.size,
452
- mtime: r.mtime_ms,
453
- firstMessage: r.first_message,
454
- gitBranch: r.git_branch,
455
- provider: r.provider,
456
- model: r.model,
457
- messageCount: r.message_count,
458
- tokens: r.usage_json ? JSON.parse(r.usage_json) : null,
459
- extra: r.extra_json ? JSON.parse(r.extra_json) : {},
460
- source: r.source,
461
- projectName: r.project_name,
462
- projectDisplayName: r.project_display_name,
463
- projectFullPath: r.project_full_path,
464
- startedAt: r.started_at,
465
- updatedAt: r.updated_at
466
- }));
918
+ return rows.map(r => {
919
+ const extra = _safeParseObject(r.extra_json);
920
+
921
+ return {
922
+ sessionId: r.session_id,
923
+ filePath: r.file_path,
924
+ size: r.size,
925
+ mtime: r.mtime_ms,
926
+ firstMessage: r.first_message,
927
+ gitBranch: r.git_branch,
928
+ provider: r.provider,
929
+ model: r.model,
930
+ messageCount: r.message_count,
931
+ tokens: _safeParseJson(r.usage_json, null),
932
+ extra,
933
+ source: r.source,
934
+ projectName: r.project_name,
935
+ projectHint: extra.projectHint || r.project_name,
936
+ projectDisplayName: r.project_display_name,
937
+ projectFullPath: r.project_full_path,
938
+ startedAt: r.started_at,
939
+ updatedAt: r.updated_at
940
+ };
941
+ });
467
942
  }
468
943
 
469
944
  /**
@@ -473,23 +948,13 @@ function createSessionHistoryIndex(opts = {}) {
473
948
  * @returns {Promise<{sessionId, lastModified, size, filePath}|null>}
474
949
  */
475
950
  async function getSessionStatus(source, sessionId, options = {}) {
476
- await ensureSourceIndexed(source, { consistency: 'stale-ok' });
951
+ await _ensureSessionCurrent(source, sessionId);
477
952
  const db = _getDb();
478
953
  const row = db.prepare(
479
954
  'SELECT session_id, mtime_ms, size, file_path FROM session_file WHERE source = ? AND session_id = ?'
480
955
  ).get(source, sessionId);
481
-
482
956
  if (!row) return null;
483
957
 
484
- // Verify file still exists
485
- try {
486
- await fs.promises.stat(row.file_path);
487
- } catch (_) {
488
- // File gone - invalidate and return null
489
- db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, sessionId);
490
- return null;
491
- }
492
-
493
958
  return {
494
959
  sessionId: row.session_id,
495
960
  lastModified: row.mtime_ms,
@@ -575,7 +1040,7 @@ function createSessionHistoryIndex(opts = {}) {
575
1040
  model: r.model,
576
1041
  provider: r.provider,
577
1042
  userMessageNumber: r.user_message_number,
578
- extra: r.extra_json ? JSON.parse(r.extra_json) : {}
1043
+ extra: _safeParseObject(r.extra_json)
579
1044
  })),
580
1045
  metadata: _buildMessageMetadata(sf),
581
1046
  pagination: {
@@ -594,7 +1059,11 @@ function createSessionHistoryIndex(opts = {}) {
594
1059
  * @returns {Promise<Array>}
595
1060
  */
596
1061
  async function getRecentSessions(source, limit = 5, options = {}) {
597
- await ensureSourceIndexed(source, { consistency: 'stale-ok' });
1062
+ await ensureSourceIndexed(source, {
1063
+ force: options.force === true,
1064
+ consistency: options.consistency || 'stale-ok',
1065
+ config: options.config
1066
+ });
598
1067
  const db = _getDb();
599
1068
 
600
1069
  const rows = db.prepare(`
@@ -629,7 +1098,12 @@ function createSessionHistoryIndex(opts = {}) {
629
1098
  * @returns {Promise<Array>}
630
1099
  */
631
1100
  async function searchSessions(source, keyword, options = {}) {
632
- await ensureSourceIndexed(source, { consistency: options.consistency || 'complete' });
1101
+ if (!String(keyword || '').trim()) return [];
1102
+ await ensureSourceIndexed(source, {
1103
+ force: options.force === true,
1104
+ consistency: options.consistency || 'complete',
1105
+ config: options.config
1106
+ });
633
1107
  const db = _getDb();
634
1108
  const contextLength = options.contextLength || 35;
635
1109
  const projectName = options.projectName || null;
@@ -641,13 +1115,15 @@ function createSessionHistoryIndex(opts = {}) {
641
1115
  const needle = String(keyword);
642
1116
  if ([...needle].length >= 3) {
643
1117
  const ftsRows = db.prepare(`
644
- SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp
1118
+ SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
1119
+ sf.project_name, sf.project_display_name, sf.project_full_path,
1120
+ sf.file_path, sf.first_message, sf.updated_at
645
1121
  FROM session_message_fts fts
646
1122
  JOIN session_message sm ON sm.rowid = fts.rowid
647
1123
  JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
648
1124
  WHERE sm.source = ? AND session_message_fts MATCH ?
649
1125
  ${projectName ? 'AND sf.project_name = ?' : ''}
650
- ORDER BY sf.updated_at DESC
1126
+ ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
651
1127
  LIMIT 500
652
1128
  `);
653
1129
  const params = [source, _ftsQuote(needle)];
@@ -680,20 +1156,26 @@ function createSessionHistoryIndex(opts = {}) {
680
1156
  idx += lowerKeyword.length;
681
1157
  }
682
1158
  if (count === 0) continue;
683
-
684
- if (!matchMap.has(c.session_id)) {
685
- const sf = db.prepare('SELECT * FROM session_file WHERE source = ? AND session_id = ?').get(source, c.session_id);
686
- if (!sf) continue;
687
- matchMap.set(c.session_id, {
688
- session: sf,
1159
+ let entry = matchMap.get(c.session_id);
1160
+ if (!entry) {
1161
+ entry = {
1162
+ session: {
1163
+ source: c.source,
1164
+ session_id: c.session_id,
1165
+ project_name: c.project_name,
1166
+ project_display_name: c.project_display_name,
1167
+ project_full_path: c.project_full_path,
1168
+ file_path: c.file_path,
1169
+ first_message: c.first_message,
1170
+ updated_at: c.updated_at
1171
+ },
689
1172
  matchCount: 0,
690
1173
  messages: []
691
- });
1174
+ };
1175
+ matchMap.set(c.session_id, entry);
692
1176
  }
693
1177
 
694
- const entry = matchMap.get(c.session_id);
695
- // Claude: count every occurrence; Codex/Gemini/OMP: at most 1 per message
696
- entry.matchCount += (source === 'claude') ? count : 1;
1178
+ entry.matchCount += source === 'claude' ? count : 1;
697
1179
  entry.messages.push({
698
1180
  ordinal: c.ordinal,
699
1181
  content,
@@ -724,6 +1206,7 @@ function createSessionHistoryIndex(opts = {}) {
724
1206
 
725
1207
  // Sort: matchCount DESC, then updated_at DESC
726
1208
  results.sort((a, b) => {
1209
+
727
1210
  const cm = b.matchCount - a.matchCount;
728
1211
  if (cm !== 0) return cm;
729
1212
  const aRow = matchMap.get(a.sessionId);
@@ -737,20 +1220,50 @@ function createSessionHistoryIndex(opts = {}) {
737
1220
  }
738
1221
 
739
1222
  function _scanMessagesRelational(db, source, keyword, projectName = null) {
740
- const lowerKeyword = String(keyword).toLocaleLowerCase();
741
- // Use instr for case-insensitive substring matching
742
- let sql = `
743
- SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp
1223
+ const useSqlMatch = /^[\x00-\x7F]*$/.test(String(keyword));
1224
+ const select = `
1225
+ SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
1226
+ sf.project_name, sf.project_display_name, sf.project_full_path,
1227
+ sf.file_path, sf.first_message, sf.updated_at
744
1228
  FROM session_message sm
745
1229
  JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
746
- WHERE sm.source = ? AND instr(lower(sm.content), lower(?)) > 0
1230
+ WHERE sm.source = ?
747
1231
  ${projectName ? 'AND sf.project_name = ?' : ''}
748
- ORDER BY sf.updated_at DESC
749
- LIMIT 500
1232
+ ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
750
1233
  `;
751
- const params = [source, keyword];
752
- if (projectName) params.push(projectName);
753
- return db.prepare(sql).all(...params);
1234
+
1235
+ if (useSqlMatch) {
1236
+ const sql = `${select.replace('WHERE sm.source = ?', 'WHERE sm.source = ? AND instr(lower(sm.content), lower(?)) > 0')}\nLIMIT 500`;
1237
+ const params = [source, keyword];
1238
+ if (projectName) params.push(projectName);
1239
+ return db.prepare(sql).all(...params);
1240
+ }
1241
+
1242
+ // SQLite's lower() is ASCII-oriented. Page the broad joined query and
1243
+ // apply the same locale-aware JavaScript matcher used by searchSessions,
1244
+ // collecting at most the 500 matching candidates (not 500 raw rows).
1245
+ const candidates = [];
1246
+ const pageSize = 500;
1247
+ let offset = 0;
1248
+ while (candidates.length < pageSize) {
1249
+ const params = [source];
1250
+ if (projectName) params.push(projectName);
1251
+ params.push(pageSize, offset);
1252
+ const batch = db.prepare(`${select}\nLIMIT ? OFFSET ?`).all(...params);
1253
+ if (batch.length === 0) break;
1254
+ for (const row of batch) {
1255
+ if (_containsLocaleMatch(row.content, keyword)) candidates.push(row);
1256
+ if (candidates.length >= pageSize) break;
1257
+ }
1258
+ if (batch.length < pageSize) break;
1259
+ offset += batch.length;
1260
+ }
1261
+ return candidates;
1262
+ }
1263
+
1264
+ function _containsLocaleMatch(content, keyword) {
1265
+ const lowerKeyword = String(keyword).toLocaleLowerCase();
1266
+ return String(content || '').toLocaleLowerCase().indexOf(lowerKeyword) !== -1;
754
1267
  }
755
1268
 
756
1269
  function _extractContext(content, position, contextLength, keywordLength) {
@@ -778,8 +1291,8 @@ function createSessionHistoryIndex(opts = {}) {
778
1291
  provider: sf.provider,
779
1292
  model: sf.model,
780
1293
  messageCount: sf.message_count,
781
- usage: sf.usage_json ? JSON.parse(sf.usage_json) : null,
782
- extra: sf.extra_json ? JSON.parse(sf.extra_json) : {}
1294
+ usage: _safeParseObject(sf.usage_json),
1295
+ extra: _safeParseObject(sf.extra_json)
783
1296
  };
784
1297
  }
785
1298
 
@@ -794,17 +1307,41 @@ function createSessionHistoryIndex(opts = {}) {
794
1307
  ).get(source, sessionId);
795
1308
  if (!row) return;
796
1309
 
797
- let currentStat;
798
- try {
799
- currentStat = await fs.promises.stat(row.file_path);
800
- } catch (_) {
801
- // File gone, session stays indexed as-is
1310
+ const fileKey = `${source}:${row.file_path}`;
1311
+ const now = Date.now();
1312
+ const cached = fileVersions.get(fileKey);
1313
+ if (cached && now - cached.checkedAt < INDEX_INVENTORY_TTL_MS) return;
1314
+
1315
+ const activeCheck = fileChecks.get(fileKey);
1316
+ if (activeCheck) {
1317
+ await activeCheck;
802
1318
  return;
803
1319
  }
804
1320
 
805
- if (currentStat.size !== row.size || currentStat.mtimeMs !== row.mtime_ms) {
806
- // File changed; force re-index
807
- await ensureSourceIndexed(source, { force: true, consistency: 'complete' });
1321
+ const check = (async () => {
1322
+ let currentStat;
1323
+ try {
1324
+ currentStat = await fs.promises.stat(row.file_path);
1325
+ } catch (_) {
1326
+ fileVersions.set(fileKey, { size: -1, mtimeMs: -1, checkedAt: Date.now(), missing: true });
1327
+ db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?').run(source, row.file_path);
1328
+ return;
1329
+ }
1330
+
1331
+ fileVersions.set(fileKey, {
1332
+ size: currentStat.size,
1333
+ mtimeMs: currentStat.mtimeMs,
1334
+ checkedAt: Date.now()
1335
+ });
1336
+ if (currentStat.size !== row.size || currentStat.mtimeMs !== row.mtime_ms) {
1337
+ await ensureSourceIndexed(source, { force: true, consistency: 'complete' });
1338
+ }
1339
+ })();
1340
+ fileChecks.set(fileKey, check);
1341
+ try {
1342
+ await check;
1343
+ } finally {
1344
+ if (fileChecks.get(fileKey) === check) fileChecks.delete(fileKey);
808
1345
  }
809
1346
  }
810
1347
 
@@ -819,16 +1356,38 @@ function createSessionHistoryIndex(opts = {}) {
819
1356
  if (options.deleted && options.sessionId) {
820
1357
  db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, options.sessionId);
821
1358
  }
1359
+ if (options.projectName) {
1360
+ db.prepare('DELETE FROM session_file WHERE source = ? AND project_name = ?').run(source, options.projectName);
1361
+ }
822
1362
 
823
1363
  // Mark state as stale
824
1364
  db.prepare(
825
1365
  'UPDATE source_state SET last_inventory_ms = NULL WHERE source = ?'
826
1366
  ).run(source);
1367
+ for (const key of fileVersions.keys()) {
1368
+ if (key.startsWith(`${source}:`)) fileVersions.delete(key);
1369
+ }
1370
+ }
1371
+ function getSourceIndexMeta(source) {
1372
+ const row = _getDb().prepare(
1373
+ 'SELECT last_inventory_ms, last_error FROM source_state WHERE source = ?'
1374
+ ).get(source);
1375
+ const generatedAt = row?.last_inventory_ms ? Number(row.last_inventory_ms) : null;
1376
+ const stale = !generatedAt || Date.now() - generatedAt >= INDEX_INVENTORY_TTL_MS;
1377
+ const refreshing = _inflight.has(`ensure:${source}`);
1378
+ return {
1379
+ generatedAt,
1380
+ stale,
1381
+ refreshing,
1382
+ fallback: Boolean(row?.last_error && !_hasUsableIndexedData(source)),
1383
+ error: row?.last_error || null
1384
+ };
827
1385
  }
828
-
829
1386
  function closeSessionHistoryIndex() {
830
1387
  closeDatabase(dbPath);
831
1388
  _db = null;
1389
+ fileVersions.clear();
1390
+ fileChecks.clear();
832
1391
  }
833
1392
 
834
1393
  // Build the API object
@@ -842,6 +1401,7 @@ function createSessionHistoryIndex(opts = {}) {
842
1401
  getRecentSessions,
843
1402
  searchSessions,
844
1403
  invalidateSource,
1404
+ getSourceIndexMeta,
845
1405
  closeSessionHistoryIndex,
846
1406
  };
847
1407
 
@@ -858,9 +1418,11 @@ function createSessionHistoryIndex(opts = {}) {
858
1418
 
859
1419
  let _defaultIndex = null;
860
1420
 
861
- function _getDefaultIndex() {
1421
+ function _getDefaultIndex(options = {}) {
862
1422
  if (!_defaultIndex) {
863
- _defaultIndex = createSessionHistoryIndex();
1423
+ _defaultIndex = Object.keys(options).length > 0
1424
+ ? createSessionHistoryIndex({ config: options })
1425
+ : createSessionHistoryIndex();
864
1426
  }
865
1427
  return _defaultIndex;
866
1428
  }
@@ -870,7 +1432,7 @@ async function ensureSourceIndexed(source, options) {
870
1432
  }
871
1433
 
872
1434
  async function listProjects(source, options) {
873
- return _getDefaultIndex().listProjects(source, options);
1435
+ return _getDefaultIndex(options).listProjects(source, options);
874
1436
  }
875
1437
 
876
1438
  async function listSessions(source, projectName, options) {
@@ -897,6 +1459,10 @@ async function searchSessions(source, keyword, options) {
897
1459
  return _getDefaultIndex().searchSessions(source, keyword, options);
898
1460
  }
899
1461
 
1462
+ function getSourceIndexMeta(source) {
1463
+ return _getDefaultIndex().getSourceIndexMeta(source);
1464
+ }
1465
+
900
1466
  function invalidateSource(source, options) {
901
1467
  _getDefaultIndex().invalidateSource(source, options);
902
1468
  }
@@ -926,5 +1492,6 @@ module.exports = {
926
1492
  getRecentSessions,
927
1493
  searchSessions,
928
1494
  invalidateSource,
929
- closeSessionHistoryIndex
1495
+ getSourceIndexMeta,
1496
+ closeSessionHistoryIndex,
930
1497
  };