coding-tool-x 3.8.4 → 3.8.6
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.
- package/CHANGELOG.md +15 -0
- package/README.md +86 -15
- package/dist/web/assets/AgentsDrawer-Db9wAyaZ.js +1 -0
- package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +1 -0
- package/dist/web/assets/Analytics-C4gMzv7g.js +1 -0
- package/dist/web/assets/Analytics-CyHhR126.css +1 -0
- package/dist/web/assets/AssetPathField-Bd_nMFu3.js +1 -0
- package/dist/web/assets/ChatHistoryDrawer-B2qBQs9L.css +1 -0
- package/dist/web/assets/ChatHistoryDrawer-CwzJHyNP.js +1 -0
- package/dist/web/assets/CollapseItem-CTCfSiue.js +1 -0
- package/dist/web/assets/CommandsDrawer-BZJxlPvV.css +1 -0
- package/dist/web/assets/CommandsDrawer-vDghB1U9.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-C-XDvlHc.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-dwruRKWr.css +1 -0
- package/dist/web/assets/ConfigTemplates-CYY8XRED.js +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-BfDi8yO3.css +1 -0
- package/dist/web/assets/ConfigTemplatesDrawer-CbsJp76X.js +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-CWyPtk67.css +1 -0
- package/dist/web/assets/ConfigTemplatesPanel-liMSTh7s.js +1 -0
- package/dist/web/assets/Divider-D28g1gwj.js +1 -0
- package/dist/web/assets/DrawerContent-ChXgZ1Xm.js +1 -0
- package/dist/web/assets/FavoritesDrawer-CG9eI9Ay.css +1 -0
- package/dist/web/assets/FavoritesDrawer-D6QSiYaL.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +1 -0
- package/dist/web/assets/GatewayConvertDrawer-RvET6iNG.js +1 -0
- package/dist/web/assets/Home-CSuFuJ9Y.css +1 -0
- package/dist/web/assets/Home-P8eseZcf.js +1 -0
- package/dist/web/assets/MarkdownEditor-CYmLGYju.js +1 -0
- package/dist/web/assets/MarkdownEditor-qfVgSre_.css +1 -0
- package/dist/web/assets/McpDrawer-C0dnsZhQ.css +1 -0
- package/dist/web/assets/McpDrawer-Ch3bkKIq.js +1 -0
- package/dist/web/assets/McpFormDrawer-BRbDcawn.css +1 -0
- package/dist/web/assets/McpFormDrawer-BcTlgSpN.js +1 -0
- package/dist/web/assets/PluginManager-Dh2o0xkZ.js +1 -0
- package/dist/web/assets/PluginsDrawer-DM3QjprE.css +1 -0
- package/dist/web/assets/PluginsDrawer-deKQ5u3I.js +1 -0
- package/dist/web/assets/PluginsPanel-BofsgQgj.js +1 -0
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +1 -0
- package/dist/web/assets/ProjectList-677RLZ8O.css +1 -0
- package/dist/web/assets/ProjectList-DhSWJHTA.js +1 -0
- package/dist/web/assets/PromptsDrawer-BIgiYCEQ.css +1 -0
- package/dist/web/assets/PromptsDrawer-Cfb2FeeU.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-3eXamJhr.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +1 -0
- package/dist/web/assets/SessionCard-CW0eHURI.js +1 -0
- package/dist/web/assets/SessionCard-aM2NHu6i.css +1 -0
- package/dist/web/assets/SessionList-BpYRz1cX.css +1 -0
- package/dist/web/assets/SessionList-DQfyDGWU.js +1 -0
- package/dist/web/assets/SettingsDrawer-Dw5NUgHC.css +1 -0
- package/dist/web/assets/SettingsDrawer-u_vwUuPS.js +1 -0
- package/dist/web/assets/SkillManager-C0GCT2Kb.css +1 -0
- package/dist/web/assets/SkillManager-DHMprJLA.js +1 -0
- package/dist/web/assets/SkillsDrawer-BCNme-aJ.css +1 -0
- package/dist/web/assets/SkillsDrawer-CkF3BpyC.js +1 -0
- package/dist/web/assets/SkillsPanel-B3Nm5j32.css +1 -0
- package/dist/web/assets/SkillsPanel-DWE4pFGx.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-BgfG1Ked.js +1 -0
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +1 -0
- package/dist/web/assets/Upload-CNwaGOws.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-Br5CeCzL.js +1 -0
- package/dist/web/assets/WorkspaceDrawer-CGnMu9p1.css +1 -0
- package/dist/web/assets/WorkspaceManager-B2O8-qIw.js +1 -0
- package/dist/web/assets/WorkspacePanel-5oDKit3c.js +1 -0
- package/dist/web/assets/WorkspacePanel-D0-zHTHP.css +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BeLZJZij.js +1 -0
- package/dist/web/assets/client-UDwXBP9i.js +1 -0
- package/dist/web/assets/clipboard-B46yyoz_.js +1 -0
- package/dist/web/assets/headers-cf-QapUq.js +1 -0
- package/dist/web/assets/icons-B2Vg-IOC.js +1 -0
- package/dist/web/assets/index-BMAUPq5K.js +2 -0
- package/dist/web/assets/index-Bi1MQHYX.css +1 -0
- package/dist/web/assets/light-Cp0t7v32.js +1 -0
- package/dist/web/assets/markdown-u082bbtj.js +1 -0
- package/dist/web/assets/platforms-CBkB45JP.js +1 -0
- package/dist/web/assets/platforms-FTpjxAT6.js +1 -0
- package/dist/web/assets/project-config-B2baPzYL.js +1 -0
- package/dist/web/assets/sessions-Cmy31slD.js +1 -0
- package/dist/web/assets/sessions-yC_oVVh8.js +1 -0
- package/dist/web/assets/skill-repo-input-B_fkGlUK.js +1 -0
- package/dist/web/assets/use-lock-html-scroll-DXIqmUW4.js +1 -0
- package/dist/web/assets/use-message-DoLM8C1C.js +1 -0
- package/dist/web/assets/useResponsiveDrawer-jbmqZuJs.js +1 -0
- package/dist/web/assets/vendors-D0c_0KLP.js +1 -0
- package/dist/web/assets/vue-vendor-BqZbbXHA.js +1 -0
- package/dist/web/assets/workspaces-mjyPVGS8.js +1 -0
- package/dist/web/assets/xml-CgY3RMlX.js +1 -0
- package/dist/web/index.html +12 -8
- package/docs/native-log-memory-2026-09-16.md +43 -0
- package/docs/superpowers/plans/2026-08-26-configuration-driven-cli-platform.md +1578 -0
- package/docs/superpowers/plans/2026-08-30-project-scoped-config-control.md +1260 -0
- package/docs/superpowers/plans/2026-09-01-omp-skill-refresh-boundary.md +213 -0
- package/docs/superpowers/plans/2026-09-01-realtime-logs-panel.md +125 -0
- package/docs/superpowers/plans/2026-09-02-config-driven-platform-boundary.md +86 -0
- package/docs/superpowers/plans/2026-09-02-platform-implementation-drivers.md +508 -0
- package/docs/superpowers/plans/2026-09-03-cli-platform-correctness-boundary.md +760 -0
- package/docs/superpowers/plans/2026-09-03-dependency-security-remediation.md +133 -0
- package/docs/superpowers/plans/2026-09-04-model-catalog-pricing-sync.md +1078 -0
- package/docs/superpowers/plans/2026-09-05-codex-project-scan-500.md +128 -0
- package/docs/superpowers/plans/2026-09-06-project-skill-path-fix.md +97 -0
- package/docs/superpowers/specs/2026-08-26-configuration-driven-cli-platform-design.md +408 -0
- package/docs/superpowers/specs/2026-08-30-project-scoped-config-control-design.md +467 -0
- package/docs/superpowers/specs/2026-09-01-claude-session-content-repair-design.md +111 -0
- package/docs/superpowers/specs/2026-09-01-omp-skill-refresh-boundary-design.md +83 -0
- package/docs/superpowers/specs/2026-09-01-realtime-logs-panel-design.md +27 -0
- package/docs/superpowers/specs/2026-09-02-config-driven-platform-boundary-design.md +64 -0
- package/docs/superpowers/specs/2026-09-02-platform-implementation-driver-boundary-design.md +123 -0
- package/docs/superpowers/specs/2026-09-03-cli-platform-correctness-boundary-design.md +249 -0
- package/docs/superpowers/specs/2026-09-03-dependency-security-remediation-design.md +87 -0
- package/docs/superpowers/specs/2026-09-04-model-catalog-pricing-sync-design.md +301 -0
- package/docs/superpowers/specs/2026-09-05-codex-project-scan-500-design.md +24 -0
- package/package.json +8 -4
- package/src/commands/channels.js +88 -191
- package/src/commands/cli-type.js +19 -11
- package/src/commands/daemon.js +104 -32
- package/src/commands/doctor.js +16 -9
- package/src/commands/export-config.js +23 -5
- package/src/commands/list.js +130 -52
- package/src/commands/logs.js +44 -27
- package/src/commands/platform-command-registry.js +78 -0
- package/src/commands/plugin.js +11 -5
- package/src/commands/port-config.js +34 -52
- package/src/commands/proxy-control.js +207 -145
- package/src/commands/proxy.js +2 -2
- package/src/commands/resume.js +87 -91
- package/src/commands/search.js +137 -67
- package/src/commands/stats.js +30 -22
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +94 -111
- package/src/commands/ui.js +67 -6
- package/src/commands/workspace.js +90 -8
- package/src/config/default.js +6 -53
- package/src/config/loader.js +61 -24
- package/src/config/model-metadata.js +49 -59
- package/src/config/model-metadata.json +4095 -99
- package/src/config/paths.js +181 -8
- package/src/index.js +134 -71
- package/src/platforms/access.js +196 -0
- package/src/{server/services → platforms}/agents-service.js +354 -197
- package/src/platforms/channel-auth-registry.js +165 -0
- package/src/platforms/channel-auth-service.js +107 -0
- package/src/{server/services → platforms}/commands-service.js +195 -115
- package/src/platforms/driver-registry.js +59 -0
- package/src/platforms/drivers/claude/api-operations.js +62 -0
- package/src/{server/services/channels.js → platforms/drivers/claude/channels-implementation.js} +131 -33
- package/src/platforms/drivers/claude/channels.js +39 -0
- package/src/{server/services/config-sync-service.js → platforms/drivers/claude/config-sync.js} +29 -5
- package/src/{server/services → platforms/drivers/claude}/health-check.js +4 -4
- package/src/platforms/drivers/claude/health.js +22 -0
- package/src/platforms/drivers/claude/mcp.js +9 -0
- package/src/{server/services/settings-manager.js → platforms/drivers/claude/native-config-implementation.js} +13 -4
- package/src/platforms/drivers/claude/native-config.js +46 -0
- package/src/platforms/drivers/claude/native-logs.js +72 -0
- package/src/{server/services/claude-openai-gateway.js → platforms/drivers/claude/openai-gateway.js} +8 -65
- package/src/platforms/drivers/claude/project-config.js +21 -0
- package/src/platforms/drivers/claude/projects.js +62 -0
- package/src/platforms/drivers/claude/prompts.js +9 -0
- package/src/{server/proxy-server.js → platforms/drivers/claude/proxy-implementation.js} +41 -278
- package/src/platforms/drivers/claude/proxy.js +16 -0
- package/src/platforms/drivers/claude/resource-sync.js +9 -0
- package/src/platforms/drivers/claude/session-history-adapter.js +313 -0
- package/src/{server/services/sessions.js → platforms/drivers/claude/sessions-implementation.js} +145 -35
- package/src/platforms/drivers/claude/sessions.js +76 -0
- package/src/{server/services/claude-statistics-service.js → platforms/drivers/claude/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/claude/statistics.js +16 -0
- package/src/{server/services/claude-wire.js → platforms/drivers/claude/wire.js} +1 -1
- package/src/platforms/drivers/codex/api-operations.js +9 -0
- package/src/{server/services/codex-channels.js → platforms/drivers/codex/channels-implementation.js} +149 -14
- package/src/platforms/drivers/codex/channels.js +45 -0
- package/src/{server/services/codex-config.js → platforms/drivers/codex/config.js} +12 -2
- package/src/{server/services/codex-env-manager.js → platforms/drivers/codex/env-manager.js} +1 -1
- package/src/platforms/drivers/codex/mcp.js +9 -0
- package/src/{server/services/codex-settings-manager.js → platforms/drivers/codex/native-config-implementation.js} +15 -6
- package/src/platforms/drivers/codex/native-config.js +47 -0
- package/src/platforms/drivers/codex/native-logs.js +166 -0
- package/src/platforms/drivers/codex/project-config.js +21 -0
- package/src/platforms/drivers/codex/projects.js +26 -0
- package/src/platforms/drivers/codex/prompts.js +9 -0
- package/src/{server/codex-proxy-server.js → platforms/drivers/codex/proxy-implementation.js} +30 -233
- package/src/platforms/drivers/codex/proxy.js +16 -0
- package/src/platforms/drivers/codex/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/codex.js → platforms/drivers/codex/session-history-adapter.js} +37 -23
- package/src/{server/services/codex-sessions.js → platforms/drivers/codex/sessions-implementation.js} +81 -75
- package/src/platforms/drivers/codex/sessions.js +36 -0
- package/src/{server/services/codex-statistics-service.js → platforms/drivers/codex/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/codex/statistics.js +16 -0
- package/src/platforms/drivers/gemini/api-operations.js +9 -0
- package/src/{server/services/gemini-channels.js → platforms/drivers/gemini/channels-implementation.js} +202 -37
- package/src/platforms/drivers/gemini/channels.js +39 -0
- package/src/{server/services/gemini-config.js → platforms/drivers/gemini/config.js} +12 -2
- package/src/platforms/drivers/gemini/mcp.js +9 -0
- package/src/{server/services/gemini-settings-manager.js → platforms/drivers/gemini/native-config-implementation.js} +14 -5
- package/src/platforms/drivers/gemini/native-config.js +39 -0
- package/src/platforms/drivers/gemini/native-logs.js +180 -0
- package/src/platforms/drivers/gemini/project-config.js +21 -0
- package/src/platforms/drivers/gemini/projects.js +26 -0
- package/src/platforms/drivers/gemini/prompts.js +9 -0
- package/src/{server/gemini-proxy-server.js → platforms/drivers/gemini/proxy-implementation.js} +36 -267
- package/src/platforms/drivers/gemini/proxy.js +16 -0
- package/src/platforms/drivers/gemini/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/gemini.js → platforms/drivers/gemini/session-history-adapter.js} +70 -18
- package/src/{server/services/gemini-sessions.js → platforms/drivers/gemini/sessions-implementation.js} +53 -37
- package/src/platforms/drivers/gemini/sessions.js +47 -0
- package/src/{server/services/gemini-statistics-service.js → platforms/drivers/gemini/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/gemini/statistics.js +16 -0
- package/src/{server/services/gemini-wire.js → platforms/drivers/gemini/wire.js} +1 -1
- package/src/platforms/drivers/generic-filesystem.js +166 -0
- package/src/platforms/drivers/generic-jsonl.js +141 -0
- package/src/platforms/drivers/generic-mcp.js +44 -0
- package/src/platforms/drivers/generic-openai-compatible.js +45 -0
- package/src/platforms/drivers/generic-prompt.js +41 -0
- package/src/platforms/drivers/legacy.js +677 -0
- package/src/platforms/drivers/native-config-snapshot.js +125 -0
- package/src/platforms/drivers/native-log-utils.js +1292 -0
- package/src/platforms/drivers/notification-hooks.js +578 -0
- package/src/{server/api/omp-config.js → platforms/drivers/omp/api-operations.js} +52 -64
- package/src/{server/services/omp-auth-provider-worker.js → platforms/drivers/omp/auth-provider-worker.js} +1 -1
- package/src/{server/services/omp-auth-providers.js → platforms/drivers/omp/auth-providers.js} +1 -1
- package/src/{server/services/omp-channels.js → platforms/drivers/omp/channels-implementation.js} +166 -39
- package/src/platforms/drivers/omp/channels.js +58 -0
- package/src/{server/services/omp-config.js → platforms/drivers/omp/config.js} +53 -13
- package/src/{server/services/omp-gateway-routing.js → platforms/drivers/omp/gateway-routing.js} +14 -7
- package/src/{server/services/omp-gateway.js → platforms/drivers/omp/gateway.js} +4 -262
- package/src/platforms/drivers/omp/mcp.js +9 -0
- package/src/{server/services/omp-settings-manager.js → platforms/drivers/omp/native-config-implementation.js} +75 -10
- package/src/platforms/drivers/omp/native-config.js +46 -0
- package/src/platforms/drivers/omp/native-logs.js +52 -0
- package/src/{server/services/omp-native-plugin-adapter.js → platforms/drivers/omp/native-plugin-adapter.js} +91 -4
- package/src/platforms/drivers/omp/project-config.js +29 -0
- package/src/platforms/drivers/omp/projects.js +20 -0
- package/src/platforms/drivers/omp/prompts.js +9 -0
- package/src/{server/omp-proxy-server.js → platforms/drivers/omp/proxy-implementation.js} +6 -15
- package/src/platforms/drivers/omp/proxy.js +16 -0
- package/src/platforms/drivers/omp/resource-sync.js +9 -0
- package/src/{server/services/session-history-adapters/omp.js → platforms/drivers/omp/session-history-adapter.js} +42 -18
- package/src/{server/services/omp-sessions.js → platforms/drivers/omp/sessions-implementation.js} +130 -94
- package/src/platforms/drivers/omp/sessions.js +36 -0
- package/src/{server/services/omp-skill-discovery.js → platforms/drivers/omp/skill-discovery.js} +159 -32
- package/src/{server/services/omp-skill-settings-service.js → platforms/drivers/omp/skill-settings.js} +1 -1
- package/src/{server/services/omp-statistics-service.js → platforms/drivers/omp/statistics-implementation.js} +3 -3
- package/src/platforms/drivers/omp/statistics.js +16 -0
- package/src/platforms/drivers/opencode/api-operations.js +9 -0
- package/src/{server/services/opencode-channels.js → platforms/drivers/opencode/channels-implementation.js} +28 -9
- package/src/platforms/drivers/opencode/channels.js +40 -0
- package/src/platforms/drivers/opencode/conversion.js +41 -0
- package/src/{server/services/opencode-gateway-converter.js → platforms/drivers/opencode/gateway-converter.js} +1 -1
- package/src/platforms/drivers/opencode/mcp.js +9 -0
- package/src/{server/services/opencode-settings-manager.js → platforms/drivers/opencode/native-config-implementation.js} +17 -6
- package/src/platforms/drivers/opencode/native-config.js +26 -0
- package/src/platforms/drivers/opencode/native-logs.js +133 -0
- package/src/platforms/drivers/opencode/project-config.js +98 -0
- package/src/platforms/drivers/opencode/projects.js +20 -0
- package/src/platforms/drivers/opencode/prompts.js +9 -0
- package/src/{server/opencode-proxy-server.js → platforms/drivers/opencode/proxy-implementation.js} +27 -362
- package/src/platforms/drivers/opencode/proxy.js +16 -0
- package/src/platforms/drivers/opencode/resource-sync.js +9 -0
- package/src/{server/services/opencode-sessions.js → platforms/drivers/opencode/sessions-implementation.js} +211 -64
- package/src/platforms/drivers/opencode/sessions.js +48 -0
- package/src/{server/services/opencode-statistics-service.js → platforms/drivers/opencode/statistics-implementation.js} +2 -2
- package/src/platforms/drivers/opencode/statistics.js +16 -0
- package/src/platforms/drivers/secure-file-driver.js +272 -0
- package/src/platforms/drivers/unsupported.js +7 -0
- package/src/platforms/manifest-schema.js +398 -0
- package/src/platforms/manifests/claude.json +489 -0
- package/src/platforms/manifests/codex.json +448 -0
- package/src/platforms/manifests/gemini.json +447 -0
- package/src/platforms/manifests/omp.json +496 -0
- package/src/platforms/manifests/opencode.json +422 -0
- package/src/{server/services → platforms}/native-oauth-adapters.js +98 -418
- package/src/{server/services → platforms}/notification-hooks.js +190 -1023
- package/src/platforms/oauth-credentials-service.js +635 -0
- package/src/platforms/path-resolver.js +179 -0
- package/src/platforms/platform-path-context.js +305 -0
- package/src/platforms/registry.js +248 -0
- package/src/platforms/remote-notification-providers.js +678 -0
- package/src/platforms/runtime.js +259 -0
- package/src/{server/services/session-history-adapters/index.js → platforms/session-history-adapters.js} +8 -1
- package/src/plugins/plugin-loader.js +2 -2
- package/src/plugins/plugin-manager.js +2 -2
- package/src/plugins/registry.js +27 -10
- package/src/reset-config.js +21 -5
- package/src/server/api/agents.js +46 -72
- package/src/server/api/commands.js +16 -6
- package/src/server/api/config-registry.js +206 -59
- package/src/server/api/config-sync.js +3 -2
- package/src/server/api/config-templates.js +20 -4
- package/src/server/api/config.js +111 -102
- package/src/server/api/convert.js +54 -178
- package/src/server/api/dashboard.js +168 -102
- package/src/server/api/hooks.js +1 -1
- package/src/server/api/mcp.js +224 -84
- package/src/server/api/platform-api-config.js +102 -0
- package/src/server/api/platform-route-factory.js +215 -0
- package/src/server/api/platform-routes.js +26 -0
- package/src/server/api/platforms.js +110 -0
- package/src/server/api/plugins.js +57 -57
- package/src/server/api/project-config.js +168 -0
- package/src/server/api/prompts.js +58 -45
- package/src/server/api/settings.js +55 -8
- package/src/server/api/skills.js +262 -133
- package/src/server/api/ui-config.js +2 -2
- package/src/server/api/validation-errors.js +18 -4
- package/src/server/api/workspaces.js +47 -12
- package/src/server/index.js +164 -213
- package/src/server/platform-context.js +81 -0
- package/src/server/services/channel-balance.js +133 -24
- package/src/server/services/channel-health.js +21 -6
- package/src/server/services/channel-scheduler.js +29 -39
- package/src/server/services/channel-sync-utils.js +23 -13
- package/src/server/services/config-export-service.js +350 -319
- package/src/server/services/config-registry-service.js +280 -54
- package/src/server/services/config-sync-manager.js +172 -944
- package/src/server/services/config-templates-service.js +419 -353
- package/src/server/services/control-manifest-store.js +206 -0
- package/src/server/services/dashboard-snapshot-worker.js +656 -100
- package/src/server/services/effective-control-service.js +557 -0
- package/src/server/services/local-resource-index.js +314 -0
- package/src/server/services/mcp-service.js +607 -977
- package/src/server/services/model-detector.js +15 -10
- package/src/server/services/native-log-channel-resolver.js +214 -0
- package/src/server/services/native-log-observer.js +291 -0
- package/src/server/services/network-access.js +12 -2
- package/src/server/services/platform-catalog.js +82 -0
- package/src/server/services/platform-resolution.js +32 -29
- package/src/server/services/plugins-service.js +376 -122
- package/src/server/services/project-config-service.js +550 -0
- package/src/server/services/project-path-validation.js +31 -0
- package/src/server/services/prompts-service.js +267 -233
- package/src/server/services/proxy-runtime.js +7 -4
- package/src/server/services/remote-credential-cache.js +43 -0
- package/src/server/services/repo-scanner-base.js +182 -75
- package/src/server/services/request-logger.js +7 -4
- package/src/server/services/session-history-index.js +876 -214
- package/src/server/services/session-history-worker.js +228 -33
- package/src/server/services/session-snapshot-worker.js +124 -34
- package/src/server/services/session-snapshots.js +4 -2
- package/src/server/services/skill-artifact-store.js +355 -0
- package/src/server/services/skill-format-adapters.js +65 -0
- package/src/server/services/skill-projection-service.js +323 -0
- package/src/server/services/skill-refresh-task-service.js +275 -0
- package/src/server/services/skill-service.js +2020 -677
- package/src/server/services/snapshot-cache.js +30 -8
- package/src/server/services/speed-test.js +8 -8
- package/src/server/services/sqlite-connection.js +19 -3
- package/src/server/services/statistics-service.js +240 -57
- package/src/server/services/ui-config.js +107 -76
- package/src/server/services/usage-log-utils.js +98 -0
- package/src/server/services/web-build.js +1 -1
- package/src/server/services/workspace-service.js +59 -29
- package/src/server/utils/pricing.js +10 -19
- package/src/server/websocket-server.js +153 -28
- package/src/{server/services/base → shared}/base-channel-service.js +156 -28
- package/src/shared/capability-driver.js +104 -0
- package/src/shared/channel-auth-policy.js +27 -0
- package/src/shared/channel-driver.js +161 -0
- package/src/shared/driver-factories/api.js +180 -0
- package/src/shared/driver-factories/mcp.js +244 -0
- package/src/shared/driver-factories/projects.js +121 -0
- package/src/shared/driver-factories/prompts.js +58 -0
- package/src/shared/driver-factories/resource-sync.js +375 -0
- package/src/shared/driver-result.js +78 -0
- package/src/shared/mcp-format.js +189 -0
- package/src/shared/platforms.js +104 -167
- package/src/shared/project-config.js +512 -0
- package/src/shared/proxy-driver.js +57 -0
- package/src/{server/services/base → shared}/response-usage-parser.js +8 -4
- package/src/ui/menu.js +91 -67
- package/src/utils/port-helper.js +35 -19
- package/src/utils/session.js +1 -1
- package/dist/web/assets/Analytics-COEUXmd4.js +0 -25
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/ConfigTemplates-DLLkS53H.js +0 -1
- package/dist/web/assets/Home-DCEyLzwX.js +0 -1
- package/dist/web/assets/Home-DYVy4cGZ.css +0 -1
- package/dist/web/assets/PluginManager-Oe1ZHoMO.js +0 -1
- package/dist/web/assets/ProjectList-CWjuAFof.js +0 -1
- package/dist/web/assets/ProjectList-VeMNXfgO.css +0 -1
- package/dist/web/assets/SessionList-C4chCPxK.css +0 -1
- package/dist/web/assets/SessionList-v3ehGg9U.js +0 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +0 -1
- package/dist/web/assets/SkillManager-DUF_qP5S.js +0 -1
- package/dist/web/assets/WorkspaceManager-CjAWBosO.js +0 -1
- package/dist/web/assets/icons-nGdSKQn2.js +0 -1
- package/dist/web/assets/index-D5hyHPj7.js +0 -2
- package/dist/web/assets/index-bJRG3RXH.css +0 -1
- package/dist/web/assets/markdown-BNJfEgCA.js +0 -1
- package/dist/web/assets/markdown-BfC0goYb.css +0 -10
- package/dist/web/assets/naive-ui-BALVpMgd.js +0 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +0 -7
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +0 -45
- package/src/config/model-pricing.js +0 -35
- package/src/server/api/channels.js +0 -439
- package/src/server/api/claude-hooks.js +0 -56
- package/src/server/api/claude-statistics.js +0 -57
- package/src/server/api/codex-channels.js +0 -491
- package/src/server/api/codex-projects.js +0 -117
- package/src/server/api/codex-proxy.js +0 -293
- package/src/server/api/codex-sessions.js +0 -652
- package/src/server/api/codex-statistics.js +0 -57
- package/src/server/api/gemini-channels.js +0 -392
- package/src/server/api/gemini-projects.js +0 -103
- package/src/server/api/gemini-proxy.js +0 -231
- package/src/server/api/gemini-sessions.js +0 -550
- package/src/server/api/gemini-statistics.js +0 -57
- package/src/server/api/health-check.js +0 -31
- package/src/server/api/oauth-credentials.js +0 -198
- package/src/server/api/omp-channels.js +0 -575
- package/src/server/api/omp-projects.js +0 -88
- package/src/server/api/omp-proxy.js +0 -125
- package/src/server/api/omp-sessions.js +0 -386
- package/src/server/api/omp-statistics.js +0 -40
- package/src/server/api/opencode-channels.js +0 -471
- package/src/server/api/opencode-projects.js +0 -110
- package/src/server/api/opencode-proxy.js +0 -354
- package/src/server/api/opencode-sessions.js +0 -466
- package/src/server/api/opencode-statistics.js +0 -57
- package/src/server/api/projects.js +0 -120
- package/src/server/api/proxy.js +0 -330
- package/src/server/api/sessions.js +0 -810
- package/src/server/services/oauth-credentials-service.js +0 -945
- package/src/server/services/omp-session-log-observer.js +0 -138
- package/src/server/services/opencode-gateway-adapters.js +0 -43
- package/src/server/services/project-snapshots.js +0 -59
- package/src/server/services/proxy-log-helper.js +0 -298
- package/src/server/services/session-cache.js +0 -127
- package/src/server/services/session-converter.js +0 -582
- package/src/server/services/session-history-adapters/claude.js +0 -235
- /package/src/{server/services/claude-token-recovery.js → platforms/drivers/claude/token-recovery.js} +0 -0
- /package/src/{server/services/codex-parser.js → platforms/drivers/codex/parser.js} +0 -0
- /package/src/{server/services/codex-speed-test-template.json → platforms/drivers/codex/speed-test-template.json} +0 -0
- /package/src/{server/services/codex-wire.js → platforms/drivers/codex/wire.js} +0 -0
- /package/src/{server/services/omp-auth-gateway-client.js → platforms/drivers/omp/auth-gateway-client.js} +0 -0
- /package/src/{server/services/opencode-normalization.js → platforms/drivers/opencode/normalization.js} +0 -0
- /package/src/{server/services → shared}/config-artifact-paths.js +0 -0
- /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,180 @@ 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}
|
|
19
|
-
const
|
|
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} Maximum time a competing inventory waits for the database writer */
|
|
23
|
+
const INVENTORY_LOCK_WAIT_MS = 30000;
|
|
24
|
+
/** @type {number} Number of files parsed before results are released */
|
|
25
|
+
const INVENTORY_PARSE_BATCH_SIZE = 8;
|
|
26
|
+
/** @type {number} Maximum number of files parsed concurrently */
|
|
27
|
+
const INVENTORY_PARSE_CONCURRENCY = 2;
|
|
28
|
+
const BUILTIN_SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'omp']);
|
|
29
|
+
// Keep synchronized with the parserVersion exposed by the built-in session drivers.
|
|
30
|
+
const BUILTIN_SESSION_PARSER_VERSIONS = Object.freeze({ claude: 2, codex: 2, gemini: 2, omp: 2 });
|
|
31
|
+
function _parserVersionForDriver(driver) {
|
|
32
|
+
const version = Number(driver?.parserVersion);
|
|
33
|
+
return Number.isInteger(version) && version >= 1 ? version : 1;
|
|
34
|
+
}
|
|
35
|
+
function _isUsableRuntime(runtime) {
|
|
36
|
+
return !!runtime && typeof runtime === 'object' && typeof runtime.getDriver === 'function';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _inventoryLockPath(dbPath) {
|
|
40
|
+
return `${path.resolve(String(dbPath))}.inventory.lock`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _isProcessAlive(pid) {
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0);
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return error && error.code === 'EPERM';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Acquire a durable database-wide inventory lock. The in-memory single-flight
|
|
55
|
+
* map only protects requests inside one Node process; production indexing is
|
|
56
|
+
* deliberately performed in child processes, so it cannot prevent two
|
|
57
|
+
* workers from writing the same database after a parent restart.
|
|
58
|
+
*/
|
|
59
|
+
function _tryAcquireInventoryLock(dbPath, source) {
|
|
60
|
+
const lockPath = _inventoryLockPath(dbPath);
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
63
|
+
let fd;
|
|
64
|
+
try {
|
|
65
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
66
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, source, startedAt: Date.now() }), 'utf8');
|
|
67
|
+
return { fd, lockPath };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (fd !== undefined) {
|
|
70
|
+
try { fs.closeSync(fd); } catch (_err) {}
|
|
71
|
+
}
|
|
72
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let owner = null;
|
|
76
|
+
let ageMs = 0;
|
|
77
|
+
try {
|
|
78
|
+
owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
79
|
+
ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error?.code === 'ENOENT') continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ownerPid = Number(owner?.pid);
|
|
85
|
+
if (_isProcessAlive(ownerPid)) {
|
|
86
|
+
return {
|
|
87
|
+
busy: true,
|
|
88
|
+
sameSource: owner?.source && String(owner.source) === String(source)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ((!Number.isInteger(ownerPid) || ownerPid <= 0) && ageMs < INVENTORY_LOCK_STALE_MS) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
fs.unlinkSync(lockPath);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code !== 'ENOENT') return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function _acquireInventoryLock(dbPath, source) {
|
|
107
|
+
const deadline = Date.now() + INVENTORY_LOCK_WAIT_MS;
|
|
108
|
+
while (true) {
|
|
109
|
+
const lock = _tryAcquireInventoryLock(dbPath, source);
|
|
110
|
+
if (lock?.busy) {
|
|
111
|
+
if (lock.sameSource) return null;
|
|
112
|
+
} else if (lock) {
|
|
113
|
+
return lock;
|
|
114
|
+
}
|
|
115
|
+
if (Date.now() >= deadline) return null;
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _releaseInventoryLock(lock) {
|
|
121
|
+
if (!lock) return;
|
|
122
|
+
try { fs.closeSync(lock.fd); } catch (_err) {}
|
|
123
|
+
try { fs.unlinkSync(lock.lockPath); } catch (_err) {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove inventory locks left by workers that are no longer alive.
|
|
128
|
+
*
|
|
129
|
+
* A normal worker releases its lock in `_runInventory`'s finally block. This
|
|
130
|
+
* fallback is needed for `ctx stop` when a worker is terminated before that
|
|
131
|
+
* block can run. Locks owned by a live PID are intentionally retained.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} [dbPath]
|
|
134
|
+
* @returns {{ removed: string[], retained: string[] }}
|
|
135
|
+
*/
|
|
136
|
+
function cleanupStaleInventoryLocks(dbPath = PATHS?.sessionHistoryIndex) {
|
|
137
|
+
const targetPath = dbPath ? path.resolve(String(dbPath)) : '';
|
|
138
|
+
if (!targetPath) return { removed: [], retained: [] };
|
|
139
|
+
|
|
140
|
+
const directory = path.dirname(targetPath);
|
|
141
|
+
const prefix = `${path.basename(targetPath)}.`;
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = fs.readdirSync(directory);
|
|
145
|
+
} catch (_) {
|
|
146
|
+
return { removed: [], retained: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const removed = [];
|
|
150
|
+
const retained = [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.startsWith(prefix) || !entry.endsWith('.inventory.lock')) continue;
|
|
153
|
+
const lockPath = path.join(directory, entry);
|
|
154
|
+
let owner = null;
|
|
155
|
+
let ageMs = 0;
|
|
156
|
+
try {
|
|
157
|
+
ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
|
|
158
|
+
} catch (_) {
|
|
159
|
+
retained.push(lockPath);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
164
|
+
} catch (_) {
|
|
165
|
+
// Keep malformed young locks; an old one is handled by the stale rule.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ownerPid = Number(owner?.pid);
|
|
169
|
+
if (_isProcessAlive(ownerPid)) {
|
|
170
|
+
retained.push(lockPath);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A valid but dead owner can be cleaned immediately. Unreadable lock
|
|
175
|
+
// files remain protected by the normal acquisition path's stale check.
|
|
176
|
+
if ((Number.isInteger(ownerPid) && ownerPid > 0) || ageMs >= INVENTORY_LOCK_STALE_MS) {
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(lockPath);
|
|
179
|
+
removed.push(lockPath);
|
|
180
|
+
} catch (_) {
|
|
181
|
+
retained.push(lockPath);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
retained.push(lockPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { removed, retained };
|
|
189
|
+
}
|
|
190
|
+
|
|
20
191
|
|
|
21
192
|
// ---------------------------------------------------------------------------
|
|
22
193
|
// Schema
|
|
@@ -49,6 +220,7 @@ CREATE TABLE IF NOT EXISTS session_file (
|
|
|
49
220
|
message_count INTEGER NOT NULL DEFAULT 0,
|
|
50
221
|
usage_json TEXT,
|
|
51
222
|
extra_json TEXT,
|
|
223
|
+
parser_version INTEGER NOT NULL DEFAULT 0,
|
|
52
224
|
PRIMARY KEY (source, file_path),
|
|
53
225
|
UNIQUE (source, session_id)
|
|
54
226
|
);
|
|
@@ -80,6 +252,9 @@ CREATE TABLE IF NOT EXISTS session_message (
|
|
|
80
252
|
|
|
81
253
|
CREATE INDEX IF NOT EXISTS idx_session_message_sid_ord
|
|
82
254
|
ON session_message(source, session_id, ordinal);
|
|
255
|
+
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_session_message_user_number
|
|
257
|
+
ON session_message(source, session_id, user_message_number, ordinal);
|
|
83
258
|
`;
|
|
84
259
|
|
|
85
260
|
const FTS_SETUP_SQL = `
|
|
@@ -144,27 +319,197 @@ function _ftsQuote(s) {
|
|
|
144
319
|
return '"' + String(s).replace(/"/g, '""') + '"';
|
|
145
320
|
}
|
|
146
321
|
|
|
147
|
-
|
|
148
|
-
|
|
322
|
+
function _normalizeTypedSessionsResult(result, defaultOperation = 'resolve-driver') {
|
|
323
|
+
if (!result || typeof result !== 'object') {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (result.status !== 'failed' && result.status !== 'unsupported') {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (typeof result.platform !== 'string' || typeof result.capability !== 'string') {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
...result,
|
|
334
|
+
operation: typeof result.operation === 'string' ? result.operation : defaultOperation
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function _isTypedSessionsResult(result) {
|
|
339
|
+
return !!_normalizeTypedSessionsResult(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _typedSessionsFailure(source, error, operation = 'resolve-driver') {
|
|
343
|
+
const result = {
|
|
344
|
+
status: 'failed',
|
|
345
|
+
platform: source,
|
|
346
|
+
capability: 'sessions',
|
|
347
|
+
operation,
|
|
348
|
+
error: error && error.message ? error.message : String(error)
|
|
349
|
+
};
|
|
350
|
+
if (error) {
|
|
351
|
+
Object.defineProperty(result, 'cause', { value: error, enumerable: false });
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function _typedSessionsUnsupported(source, error, operation = 'resolve-driver') {
|
|
357
|
+
const result = {
|
|
358
|
+
status: 'unsupported',
|
|
359
|
+
platform: source,
|
|
360
|
+
capability: 'sessions',
|
|
361
|
+
operation,
|
|
362
|
+
error: error && error.message ? error.message : String(error)
|
|
363
|
+
};
|
|
364
|
+
if (error) {
|
|
365
|
+
Object.defineProperty(result, 'cause', { value: error, enumerable: false });
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function _getRuntimeSessionsDriver(runtime, source, config = {}) {
|
|
371
|
+
if (!runtime || typeof runtime.getDriver !== 'function') {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const driver = runtime.getDriver(source, 'sessions', { config });
|
|
377
|
+
const typedResult = _normalizeTypedSessionsResult(driver);
|
|
378
|
+
if (typedResult) {
|
|
379
|
+
return typedResult;
|
|
380
|
+
}
|
|
381
|
+
if (!driver || typeof driver !== 'object') {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
if (typeof driver.inventory !== 'function' || typeof driver.parse !== 'function') {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return driver;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return _typedSessionsFailure(source, error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function _safeParseJson(value, fallback) {
|
|
394
|
+
if (!value) return fallback;
|
|
395
|
+
try {
|
|
396
|
+
return JSON.parse(value);
|
|
397
|
+
} catch (_) {
|
|
398
|
+
return fallback;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function _safeParseObject(value) {
|
|
403
|
+
const parsed = _safeParseJson(value, {});
|
|
404
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
405
|
+
return {};
|
|
406
|
+
}
|
|
407
|
+
return parsed;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
function _normalizeRuntimeParseResult(result, descriptor = {}, { preservePayloadUpdatedAt = false } = {}) {
|
|
412
|
+
if (!result || typeof result !== 'object') {
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (result.session && Array.isArray(result.messages)) {
|
|
417
|
+
const sessionExtra = _safeParseObject(result.session.extraJson);
|
|
418
|
+
const extraJson = JSON.stringify({
|
|
419
|
+
...sessionExtra,
|
|
420
|
+
projectHint: result.session.projectHint || descriptor.projectHint || null,
|
|
421
|
+
mtimeMs: descriptor.mtimeMs ?? sessionExtra.mtimeMs ?? null
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
...result,
|
|
425
|
+
session: {
|
|
426
|
+
...result.session,
|
|
427
|
+
projectHint: result.session.projectHint || descriptor.projectHint || '',
|
|
428
|
+
projectName: result.session.projectName || result.session.projectHint || descriptor.projectHint || '',
|
|
429
|
+
projectDisplayName: result.session.projectDisplayName || result.session.projectName || result.session.projectHint || descriptor.projectHint || '',
|
|
430
|
+
projectFullPath: result.session.projectFullPath || result.session.projectPath || '',
|
|
431
|
+
updatedAt: preservePayloadUpdatedAt ? (result.session.updatedAt ?? descriptor.mtimeMs ?? null) : descriptor.mtimeMs ?? null,
|
|
432
|
+
extraJson
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (result.sessionId) {
|
|
438
|
+
const projectHint = descriptor.projectHint || result.projectHint || result.projectName || '';
|
|
439
|
+
const projectName = result.projectName || projectHint || '';
|
|
440
|
+
const projectDisplayName = result.projectDisplayName || result.projectName || projectHint || '';
|
|
441
|
+
const resultExtra = _safeParseObject(result.extraJson);
|
|
442
|
+
const descriptorMtime = descriptor.mtimeMs ?? null;
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
session: {
|
|
446
|
+
sessionId: result.sessionId,
|
|
447
|
+
projectHint,
|
|
448
|
+
projectName,
|
|
449
|
+
projectDisplayName,
|
|
450
|
+
projectFullPath: result.projectFullPath || result.projectPath || '',
|
|
451
|
+
firstMessage: result.firstMessage || null,
|
|
452
|
+
gitBranch: result.gitBranch || null,
|
|
453
|
+
provider: result.provider || null,
|
|
454
|
+
model: result.model || null,
|
|
455
|
+
startedAt: result.startedAt || null,
|
|
456
|
+
updatedAt: descriptorMtime ?? result.updatedAt ?? null,
|
|
457
|
+
usageJson: result.usageJson || null,
|
|
458
|
+
extraJson: JSON.stringify({
|
|
459
|
+
...resultExtra,
|
|
460
|
+
projectHint: projectHint || null,
|
|
461
|
+
mtimeMs: descriptor.mtimeMs ?? resultExtra.mtimeMs ?? null
|
|
462
|
+
})
|
|
463
|
+
},
|
|
464
|
+
messages: Array.isArray(result.messages) ? result.messages : []
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function _adaptRuntimeSessionsDriver(driver) {
|
|
472
|
+
if (!driver) return null;
|
|
473
|
+
const unwrap = result => result && result.status === 'ok' ? result.data : result;
|
|
474
|
+
return {
|
|
475
|
+
parserVersion: driver.parserVersion,
|
|
476
|
+
inventory: async (...args) => unwrap(await driver.inventory(...args)),
|
|
477
|
+
parse: async (descriptor, ...args) => _normalizeRuntimeParseResult(
|
|
478
|
+
await unwrap(await driver.parse(descriptor, ...args)),
|
|
479
|
+
descriptor,
|
|
480
|
+
{ preservePayloadUpdatedAt: driver.preservePayloadUpdatedAt === true }
|
|
481
|
+
)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
149
484
|
// ---------------------------------------------------------------------------
|
|
150
485
|
|
|
151
486
|
/**
|
|
152
487
|
* @param {object} [opts]
|
|
153
488
|
* @param {string} [opts.dbPath]
|
|
154
489
|
* @param {object} [opts.adapterRegistry] - source → { inventory, parse }
|
|
490
|
+
* @param {object} [opts.runtime] - runtime with getDriver(source, capability)
|
|
155
491
|
* @param {Function} [opts.workerRunner] - (source, indexDbPath) => Promise<void>
|
|
156
492
|
* @param {boolean|null} [opts.ftsEnabledOverride] - override FTS detection for tests
|
|
157
493
|
* @returns {object} index API
|
|
158
494
|
*/
|
|
159
495
|
function createSessionHistoryIndex(opts = {}) {
|
|
160
496
|
const dbPath = opts.dbPath || PATHS?.sessionHistoryIndex || path.join(PATHS?.base || process.cwd(), 'session-history.sqlite');
|
|
161
|
-
const
|
|
497
|
+
const explicitProjectsDir = opts.projectsDir;
|
|
498
|
+
const indexConfig = opts.config || (opts.projectsDir ? { projectsDir: opts.projectsDir } : {});
|
|
499
|
+
const explicitAdapters = opts.adapterRegistry || null;
|
|
500
|
+
const adapters = explicitAdapters || {};
|
|
501
|
+
const runtimeProvided = !explicitAdapters && process.env.NODE_ENV === 'test' && _isUsableRuntime(opts.runtime);
|
|
502
|
+
const runtime = explicitAdapters ? null : (runtimeProvided ? opts.runtime : platformRuntime.getPlatformRuntime());
|
|
162
503
|
const workerRunner = opts.workerRunner || _defaultWorkerRunner;
|
|
163
504
|
const ftsEnabled = opts.ftsEnabledOverride !== undefined
|
|
164
505
|
? opts.ftsEnabledOverride
|
|
165
506
|
: null;
|
|
166
507
|
const shouldUseWorker = process.env.NODE_ENV !== 'test' && process.env.CC_TOOL_SESSION_HISTORY_CHILD !== '1';
|
|
167
508
|
let _db = null;
|
|
509
|
+
/** @type {Map<string, {size: number, mtimeMs: number, checkedAt: number, filePath: string}>} */
|
|
510
|
+
const fileVersions = new Map();
|
|
511
|
+
const fileChecks = new Map();
|
|
512
|
+
const sourceFreshness = new Map();
|
|
168
513
|
let _ftsAvailable = null;
|
|
169
514
|
/** @type {Map<string, Promise<void>>} */
|
|
170
515
|
const _inflight = new Map();
|
|
@@ -183,10 +528,68 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
183
528
|
}
|
|
184
529
|
return _db;
|
|
185
530
|
}
|
|
531
|
+
function _parserVersionForSource(source, adapter = null) {
|
|
532
|
+
if (adapter) return _parserVersionForDriver(adapter);
|
|
533
|
+
if (explicitAdapters) return _parserVersionForDriver(adapters[source]);
|
|
534
|
+
const runtimeDriver = _getRuntimeSessionsDriver(runtime, source, indexConfig);
|
|
535
|
+
return _parserVersionForDriver(runtimeDriver);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Check only the persisted inventory timestamp. Inventory owns file checks.
|
|
540
|
+
*
|
|
541
|
+
* @param {string} source
|
|
542
|
+
* @returns {boolean}
|
|
543
|
+
*/
|
|
544
|
+
function _isSourceFresh(source) {
|
|
545
|
+
const row = _getDb().prepare(
|
|
546
|
+
'SELECT last_inventory_ms FROM source_state WHERE source = ?'
|
|
547
|
+
).get(source);
|
|
548
|
+
return Boolean(
|
|
549
|
+
row?.last_inventory_ms
|
|
550
|
+
&& Date.now() - Number(row.last_inventory_ms) < INDEX_INVENTORY_TTL_MS
|
|
551
|
+
&& !_hasKnownParserMigrationPending(source)
|
|
552
|
+
&& (!explicitAdapters || !_hasParserMigrationPending(source))
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function _hasKnownParserMigrationPending(source) {
|
|
557
|
+
if (explicitAdapters || runtimeProvided) return false;
|
|
558
|
+
const parserVersion = BUILTIN_SESSION_PARSER_VERSIONS[source];
|
|
559
|
+
if (!parserVersion) return false;
|
|
560
|
+
const row = _getDb().prepare(
|
|
561
|
+
'SELECT 1 AS pending FROM session_file WHERE source = ? AND parser_version <> ? LIMIT 1'
|
|
562
|
+
).get(source, parserVersion);
|
|
563
|
+
return Boolean(row);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function _hasParserMigrationPending(source) {
|
|
567
|
+
const row = _getDb().prepare(
|
|
568
|
+
'SELECT 1 AS pending FROM session_file WHERE source = ? AND parser_version <> ? LIMIT 1'
|
|
569
|
+
).get(source, _parserVersionForSource(source));
|
|
570
|
+
return Boolean(row);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function _hasIndexedData(source) {
|
|
574
|
+
const row = _getDb().prepare(
|
|
575
|
+
'SELECT 1 AS indexed FROM session_file WHERE source = ? LIMIT 1'
|
|
576
|
+
).get(source);
|
|
577
|
+
return Boolean(row);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function _hasUsableIndexedData(source) {
|
|
581
|
+
return _hasIndexedData(source)
|
|
582
|
+
&& !_hasKnownParserMigrationPending(source)
|
|
583
|
+
&& (!explicitAdapters || !_hasParserMigrationPending(source));
|
|
584
|
+
}
|
|
186
585
|
|
|
187
586
|
function _initSchema(db) {
|
|
188
587
|
db.exec('PRAGMA journal_mode = WAL');
|
|
189
588
|
db.exec(SCHEMA_SQL);
|
|
589
|
+
const columns = db.prepare('PRAGMA table_info(session_file)').all();
|
|
590
|
+
if (!columns.some((column) => column.name === 'parser_version')) {
|
|
591
|
+
db.exec('ALTER TABLE session_file ADD COLUMN parser_version INTEGER NOT NULL DEFAULT 0');
|
|
592
|
+
}
|
|
190
593
|
}
|
|
191
594
|
|
|
192
595
|
function _initFts(db) {
|
|
@@ -199,28 +602,41 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
199
602
|
|
|
200
603
|
/**
|
|
201
604
|
* @param {string} source
|
|
202
|
-
* @param {{ force?: boolean, consistency?: string }} [options]
|
|
605
|
+
* @param {{ force?: boolean, consistency?: string, allowStaleData?: boolean }} [options]
|
|
203
606
|
* @returns {Promise<void>}
|
|
204
607
|
*/
|
|
205
608
|
async function ensureSourceIndexed(source, options = {}) {
|
|
206
609
|
const consistency = options.consistency || 'stale-ok';
|
|
207
610
|
const force = options.force === true;
|
|
208
|
-
|
|
611
|
+
const allowStaleData = options.allowStaleData === true;
|
|
209
612
|
const key = `ensure:${source}`;
|
|
613
|
+
|
|
614
|
+
if (!force && _isSourceFresh(source)) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
210
618
|
if (_inflight.has(key)) {
|
|
211
619
|
if (consistency === 'complete') {
|
|
212
620
|
return _inflight.get(key);
|
|
213
621
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
622
|
+
// The project directory can safely use the previous rows while a
|
|
623
|
+
// parser migration or inventory refresh is running. Session reads
|
|
624
|
+
// retain their existing behavior and wait for migration completion.
|
|
625
|
+
if (allowStaleData ? _hasIndexedData(source) : _hasUsableIndexedData(source)) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
return _inflight.get(key);
|
|
219
629
|
}
|
|
220
630
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
631
|
+
const useWorker = shouldUseWorker && !runtimeProvided && !explicitAdapters;
|
|
632
|
+
const workerOptions = { force };
|
|
633
|
+
const workerProjectsDir = source === 'claude'
|
|
634
|
+
? (options.config?.projectsDir || explicitProjectsDir)
|
|
635
|
+
: explicitProjectsDir;
|
|
636
|
+
if (workerProjectsDir) workerOptions.projectsDir = workerProjectsDir;
|
|
637
|
+
const promise = useWorker
|
|
638
|
+
? workerRunner(source, dbPath, workerOptions)
|
|
639
|
+
: _runInventory(source, { force, config: options.config || indexConfig });
|
|
224
640
|
_inflight.set(key, promise);
|
|
225
641
|
promise.finally(() => {
|
|
226
642
|
if (_inflight.get(key) === promise) {
|
|
@@ -228,166 +644,305 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
228
644
|
}
|
|
229
645
|
}).catch(() => {});
|
|
230
646
|
|
|
647
|
+
if (consistency === 'stale-ok' && (allowStaleData ? _hasIndexedData(source) : _hasUsableIndexedData(source))) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
231
651
|
if (consistency === 'stale-ok') {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
652
|
+
await Promise.race([
|
|
653
|
+
promise,
|
|
654
|
+
new Promise(resolve => setTimeout(resolve, INDEX_COLD_WAIT_MS))
|
|
655
|
+
]);
|
|
656
|
+
return;
|
|
237
657
|
}
|
|
238
658
|
|
|
239
659
|
await promise;
|
|
240
660
|
}
|
|
661
|
+
function _isTypedFailureResult(result) {
|
|
662
|
+
return _isTypedSessionsResult(result);
|
|
663
|
+
}
|
|
664
|
+
function _typedFailureToError(result) {
|
|
665
|
+
const fallbackMessage = result.status === 'unsupported'
|
|
666
|
+
? `unsupported ${result.capability}`
|
|
667
|
+
: 'inventory failed';
|
|
668
|
+
const cause = result.cause || (result.error instanceof Error ? result.error : new Error(String(result.error || fallbackMessage)));
|
|
669
|
+
const error = new Error(`Runtime ${result.capability} ${result.operation} ${result.status} on ${result.platform}: ${cause.message}`);
|
|
670
|
+
error.status = result.status;
|
|
671
|
+
error.platform = result.platform;
|
|
672
|
+
error.capability = result.capability;
|
|
673
|
+
error.operation = result.operation;
|
|
674
|
+
error.context = result;
|
|
675
|
+
error.cause = cause;
|
|
676
|
+
error.failure = result;
|
|
677
|
+
return error;
|
|
678
|
+
}
|
|
241
679
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
680
|
+
function _recordSourceState(db, source, lastInventoryMs, lastError) {
|
|
681
|
+
db.prepare(
|
|
682
|
+
'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'
|
|
683
|
+
).run(source, lastInventoryMs, lastError);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function _runInventory(source, { force = false, config = indexConfig } = {}) {
|
|
687
|
+
// SQLite's writer lock is database-wide, so source-specific lock files do
|
|
688
|
+
// not prevent two different sources from entering BEGIN IMMEDIATE at the
|
|
689
|
+
// same time. Acquire one lock for the whole index before opening/schema
|
|
690
|
+
// initializing the writer connection.
|
|
691
|
+
const inventoryLock = await _acquireInventoryLock(dbPath, source);
|
|
692
|
+
if (!inventoryLock) {
|
|
693
|
+
// Another process is already doing the expensive work. Existing rows
|
|
694
|
+
// remain readable, and the next scheduled inventory will observe the
|
|
695
|
+
// new source_state timestamp once that worker finishes.
|
|
246
696
|
return;
|
|
247
697
|
}
|
|
698
|
+
let db = null;
|
|
699
|
+
let errorMsg = null;
|
|
700
|
+
let stateToRecord = null;
|
|
248
701
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
702
|
+
try {
|
|
703
|
+
db = _getDb();
|
|
704
|
+
if (!force && _isSourceFresh(source)) {
|
|
252
705
|
return;
|
|
253
706
|
}
|
|
254
|
-
|
|
707
|
+
let adapter = null;
|
|
255
708
|
|
|
256
|
-
|
|
257
|
-
|
|
709
|
+
if (explicitAdapters) {
|
|
710
|
+
adapter = adapters[source];
|
|
711
|
+
} else {
|
|
712
|
+
const runtimeDriver = _getRuntimeSessionsDriver(runtime, source, config);
|
|
713
|
+
if (_isTypedFailureResult(runtimeDriver)) {
|
|
714
|
+
throw _typedFailureToError(runtimeDriver);
|
|
715
|
+
}
|
|
258
716
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
717
|
+
if (runtimeDriver) {
|
|
718
|
+
adapter = _adaptRuntimeSessionsDriver(runtimeDriver);
|
|
719
|
+
} else if (BUILTIN_SESSION_SOURCES.has(source) && adapters[source]) {
|
|
720
|
+
adapter = adapters[source];
|
|
721
|
+
} else {
|
|
722
|
+
throw _typedFailureToError(_typedSessionsUnsupported(source, new Error(`unsupported sessions source: ${source}`)));
|
|
723
|
+
}
|
|
264
724
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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);
|
|
725
|
+
const parserVersion = _parserVersionForSource(source, adapter);
|
|
726
|
+
const inventoryResult = await adapter.inventory({
|
|
727
|
+
projectsDir: source === 'claude' ? config.projectsDir : explicitProjectsDir
|
|
728
|
+
});
|
|
729
|
+
if (_isTypedFailureResult(inventoryResult)) {
|
|
730
|
+
throw _typedFailureToError(inventoryResult);
|
|
280
731
|
}
|
|
281
|
-
}
|
|
282
732
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
733
|
+
const indexedFiles = new Map();
|
|
734
|
+
for (const row of db.prepare('SELECT file_path, size, mtime_ms, session_id, parser_version FROM session_file WHERE source = ?').all(source)) {
|
|
735
|
+
indexedFiles.set(row.file_path, {
|
|
736
|
+
size: row.size,
|
|
737
|
+
mtime_ms: row.mtime_ms,
|
|
738
|
+
sessionId: row.session_id,
|
|
739
|
+
parserVersion: row.parser_version
|
|
740
|
+
});
|
|
286
741
|
}
|
|
287
|
-
}
|
|
288
742
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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;
|
|
743
|
+
const winnersBySessionId = new Map();
|
|
744
|
+
for (const descriptor of inventoryResult) {
|
|
745
|
+
const current = winnersBySessionId.get(descriptor.sessionId);
|
|
746
|
+
if (!current
|
|
747
|
+
|| descriptor.mtimeMs > current.mtimeMs
|
|
748
|
+
|| (descriptor.mtimeMs === current.mtimeMs && descriptor.filePath < current.filePath)) {
|
|
749
|
+
winnersBySessionId.set(descriptor.sessionId, descriptor);
|
|
310
750
|
}
|
|
751
|
+
}
|
|
311
752
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
753
|
+
const toParse = [];
|
|
754
|
+
const activePaths = new Set();
|
|
755
|
+
for (const d of winnersBySessionId.values()) {
|
|
756
|
+
activePaths.add(d.filePath);
|
|
757
|
+
const idx = indexedFiles.get(d.filePath);
|
|
758
|
+
if (!idx
|
|
759
|
+
|| idx.size !== d.size
|
|
760
|
+
|| idx.mtime_ms !== d.mtimeMs
|
|
761
|
+
|| idx.parserVersion !== parserVersion) {
|
|
762
|
+
toParse.push(d);
|
|
317
763
|
}
|
|
318
764
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
765
|
+
const parseDescriptor = async (d) => {
|
|
766
|
+
const preFingerprint = { size: d.size, mtimeMs: d.mtimeMs };
|
|
767
|
+
const parseResult = await adapter.parse({ ...d, projectsDir: config.projectsDir });
|
|
768
|
+
if (_isTypedFailureResult(parseResult)) throw _typedFailureToError(parseResult);
|
|
769
|
+
if (!parseResult || typeof parseResult !== 'object' || !parseResult.session || typeof parseResult.session !== 'object' || typeof parseResult.session.sessionId !== 'string' || !parseResult.session.sessionId.trim() || !Array.isArray(parseResult.messages)) {
|
|
770
|
+
throw new Error('invalid parsed session result');
|
|
771
|
+
}
|
|
325
772
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
773
|
+
let postStat;
|
|
774
|
+
try {
|
|
775
|
+
postStat = await fs.promises.stat(d.filePath);
|
|
776
|
+
} catch (_) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
if (postStat.size === preFingerprint.size && postStat.mtimeMs === preFingerprint.mtimeMs) {
|
|
780
|
+
return { descriptor: d, parseResult };
|
|
781
|
+
}
|
|
782
|
+
const retryDescriptor = { ...d, size: postStat.size, mtimeMs: postStat.mtimeMs };
|
|
783
|
+
const retryResult = await adapter.parse(retryDescriptor);
|
|
784
|
+
if (_isTypedFailureResult(retryResult)) {
|
|
785
|
+
throw _typedFailureToError(retryResult);
|
|
786
|
+
}
|
|
787
|
+
if (!retryResult || typeof retryResult !== 'object' || !retryResult.session || typeof retryResult.session !== 'object' || typeof retryResult.session.sessionId !== 'string' || !retryResult.session.sessionId.trim() || !Array.isArray(retryResult.messages)) {
|
|
788
|
+
throw new Error('invalid parsed session result');
|
|
789
|
+
}
|
|
790
|
+
let retryStat;
|
|
791
|
+
try {
|
|
792
|
+
retryStat = await fs.promises.stat(d.filePath);
|
|
793
|
+
} catch (_) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
if (retryStat.size !== postStat.size || retryStat.mtimeMs !== postStat.mtimeMs) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
descriptor: { ...retryDescriptor, size: retryStat.size, mtimeMs: retryStat.mtimeMs },
|
|
801
|
+
parseResult: retryResult
|
|
802
|
+
};
|
|
803
|
+
};
|
|
330
804
|
|
|
331
|
-
db.prepare(
|
|
805
|
+
const deletePath = db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?');
|
|
806
|
+
const deleteSession = db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?');
|
|
807
|
+
const insertFile = db.prepare(`
|
|
332
808
|
INSERT INTO session_file(
|
|
333
809
|
source, file_path, size, mtime_ms, session_id,
|
|
334
810
|
project_name, project_display_name, project_full_path,
|
|
335
811
|
first_message, git_branch, provider, model,
|
|
336
|
-
started_at, updated_at, message_count, usage_json, extra_json
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
);
|
|
812
|
+
started_at, updated_at, message_count, usage_json, extra_json,
|
|
813
|
+
parser_version
|
|
814
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
815
|
+
`);
|
|
816
|
+
const insertMessage = db.prepare(`
|
|
817
|
+
INSERT INTO session_message(
|
|
818
|
+
source, session_id, ordinal, message_id, role, type, subtype,
|
|
819
|
+
content, timestamp, model, provider, user_message_number, extra_json
|
|
820
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
821
|
+
`);
|
|
357
822
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
823
|
+
db.exec('BEGIN IMMEDIATE');
|
|
824
|
+
try {
|
|
825
|
+
for (const filePath of indexedFiles.keys()) {
|
|
826
|
+
if (!activePaths.has(filePath)) deletePath.run(source, filePath);
|
|
827
|
+
}
|
|
828
|
+
db.exec('COMMIT');
|
|
829
|
+
} catch (error) {
|
|
830
|
+
try { db.exec('ROLLBACK'); } catch (_) {}
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
365
833
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
834
|
+
const recordParseError = (descriptor, error) => {
|
|
835
|
+
const detail = `${descriptor.filePath}: ${error.message}`;
|
|
836
|
+
errorMsg = errorMsg ? `${errorMsg}; ${detail}` : detail;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
for (let batchStart = 0; batchStart < toParse.length; batchStart += INVENTORY_PARSE_BATCH_SIZE) {
|
|
840
|
+
const batch = toParse.slice(batchStart, batchStart + INVENTORY_PARSE_BATCH_SIZE);
|
|
841
|
+
const parsed = new Array(batch.length);
|
|
842
|
+
let next = 0;
|
|
843
|
+
const parseWorker = async () => {
|
|
844
|
+
while (true) {
|
|
845
|
+
const index = next++;
|
|
846
|
+
if (index >= batch.length) return;
|
|
847
|
+
const descriptor = batch[index];
|
|
848
|
+
try {
|
|
849
|
+
parsed[index] = { descriptor, value: await parseDescriptor(descriptor) };
|
|
850
|
+
} catch (error) {
|
|
851
|
+
parsed[index] = { descriptor, error };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
await Promise.all(Array.from({ length: Math.min(INVENTORY_PARSE_CONCURRENCY, batch.length) }, parseWorker));
|
|
856
|
+
|
|
857
|
+
for (const item of parsed) {
|
|
858
|
+
if (item?.error) recordParseError(item.descriptor, item.error);
|
|
384
859
|
}
|
|
860
|
+
|
|
861
|
+
db.exec('BEGIN IMMEDIATE');
|
|
862
|
+
try {
|
|
863
|
+
for (const item of parsed) {
|
|
864
|
+
if (item?.value) {
|
|
865
|
+
const { descriptor, parseResult } = item.value;
|
|
866
|
+
_insertSession({ deletePath, deleteSession, insertFile, insertMessage }, source, descriptor, parseResult.session, parseResult.messages, parserVersion);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
db.exec('COMMIT');
|
|
870
|
+
} catch (error) {
|
|
871
|
+
try { db.exec('ROLLBACK'); } catch (_) {}
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
// Drop the batch references before parsing the next group. This is
|
|
875
|
+
// the key difference from the old whole-inventory array: large
|
|
876
|
+
// session payloads no longer accumulate until the final COMMIT.
|
|
385
877
|
}
|
|
386
878
|
|
|
387
|
-
|
|
879
|
+
stateToRecord = {
|
|
880
|
+
lastInventoryMs: Date.now(),
|
|
881
|
+
lastError: errorMsg
|
|
882
|
+
};
|
|
388
883
|
} catch (err) {
|
|
389
|
-
|
|
884
|
+
errorMsg = err && err.message ? err.message : String(err);
|
|
885
|
+
stateToRecord = {
|
|
886
|
+
lastInventoryMs: null,
|
|
887
|
+
lastError: errorMsg
|
|
888
|
+
};
|
|
390
889
|
throw err;
|
|
890
|
+
} finally {
|
|
891
|
+
if (stateToRecord) {
|
|
892
|
+
try {
|
|
893
|
+
if (db) {
|
|
894
|
+
_recordSourceState(db, source, stateToRecord.lastInventoryMs, stateToRecord.lastError);
|
|
895
|
+
}
|
|
896
|
+
} catch (_err) {}
|
|
897
|
+
}
|
|
898
|
+
_releaseInventoryLock(inventoryLock);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
function _insertSession(statements, source, descriptor, session, messages, parserVersion) {
|
|
904
|
+
const { deletePath, deleteSession, insertFile, insertMessage } = statements;
|
|
905
|
+
deletePath.run(source, descriptor.filePath);
|
|
906
|
+
deleteSession.run(source, session.sessionId);
|
|
907
|
+
insertFile.run(
|
|
908
|
+
source,
|
|
909
|
+
descriptor.filePath,
|
|
910
|
+
descriptor.size,
|
|
911
|
+
descriptor.mtimeMs,
|
|
912
|
+
session.sessionId,
|
|
913
|
+
session.projectName || '',
|
|
914
|
+
session.projectDisplayName || null,
|
|
915
|
+
session.projectFullPath || null,
|
|
916
|
+
session.firstMessage || null,
|
|
917
|
+
session.gitBranch || null,
|
|
918
|
+
session.provider || null,
|
|
919
|
+
session.model || null,
|
|
920
|
+
session.startedAt || null,
|
|
921
|
+
session.updatedAt || null,
|
|
922
|
+
messages.length,
|
|
923
|
+
session.usageJson || null,
|
|
924
|
+
session.extraJson || null,
|
|
925
|
+
parserVersion
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
let ordinal = 0;
|
|
929
|
+
for (const msg of messages) {
|
|
930
|
+
insertMessage.run(
|
|
931
|
+
source,
|
|
932
|
+
session.sessionId,
|
|
933
|
+
ordinal,
|
|
934
|
+
msg.messageId || null,
|
|
935
|
+
msg.role || null,
|
|
936
|
+
msg.type || null,
|
|
937
|
+
msg.subtype || null,
|
|
938
|
+
msg.content || null,
|
|
939
|
+
msg.timestamp || null,
|
|
940
|
+
msg.model || null,
|
|
941
|
+
msg.provider || null,
|
|
942
|
+
msg.userMessageNumber != null ? msg.userMessageNumber : null,
|
|
943
|
+
msg.extraJson || null
|
|
944
|
+
);
|
|
945
|
+
ordinal++;
|
|
391
946
|
}
|
|
392
947
|
}
|
|
393
948
|
|
|
@@ -401,7 +956,12 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
401
956
|
* @returns {Promise<Array<{name, displayName, fullPath, path, sessionCount, lastUsed, latestSession, source}>>}
|
|
402
957
|
*/
|
|
403
958
|
async function listProjects(source, options = {}) {
|
|
404
|
-
await ensureSourceIndexed(source, {
|
|
959
|
+
await ensureSourceIndexed(source, {
|
|
960
|
+
force: options.force === true,
|
|
961
|
+
consistency: options.consistency || 'stale-ok',
|
|
962
|
+
allowStaleData: true,
|
|
963
|
+
config: options.config
|
|
964
|
+
});
|
|
405
965
|
const db = _getDb();
|
|
406
966
|
|
|
407
967
|
const rows = db.prepare(`
|
|
@@ -436,7 +996,11 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
436
996
|
* @returns {Promise<Array>}
|
|
437
997
|
*/
|
|
438
998
|
async function listSessions(source, projectName, options = {}) {
|
|
439
|
-
await ensureSourceIndexed(source, {
|
|
999
|
+
await ensureSourceIndexed(source, {
|
|
1000
|
+
force: options.force === true,
|
|
1001
|
+
consistency: options.consistency || 'stale-ok',
|
|
1002
|
+
config: options.config
|
|
1003
|
+
});
|
|
440
1004
|
|
|
441
1005
|
const db = _getDb();
|
|
442
1006
|
const rows = db.prepare(`
|
|
@@ -445,25 +1009,30 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
445
1009
|
ORDER BY updated_at DESC
|
|
446
1010
|
`).all(source, projectName);
|
|
447
1011
|
|
|
448
|
-
return rows.map(r =>
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1012
|
+
return rows.map(r => {
|
|
1013
|
+
const extra = _safeParseObject(r.extra_json);
|
|
1014
|
+
|
|
1015
|
+
return {
|
|
1016
|
+
sessionId: r.session_id,
|
|
1017
|
+
filePath: r.file_path,
|
|
1018
|
+
size: r.size,
|
|
1019
|
+
mtime: r.mtime_ms,
|
|
1020
|
+
firstMessage: r.first_message,
|
|
1021
|
+
gitBranch: r.git_branch,
|
|
1022
|
+
provider: r.provider,
|
|
1023
|
+
model: r.model,
|
|
1024
|
+
messageCount: r.message_count,
|
|
1025
|
+
tokens: _safeParseJson(r.usage_json, null),
|
|
1026
|
+
extra,
|
|
1027
|
+
source: r.source,
|
|
1028
|
+
projectName: r.project_name,
|
|
1029
|
+
projectHint: extra.projectHint || r.project_name,
|
|
1030
|
+
projectDisplayName: r.project_display_name,
|
|
1031
|
+
projectFullPath: r.project_full_path,
|
|
1032
|
+
startedAt: r.started_at,
|
|
1033
|
+
updatedAt: r.updated_at
|
|
1034
|
+
};
|
|
1035
|
+
});
|
|
467
1036
|
}
|
|
468
1037
|
|
|
469
1038
|
/**
|
|
@@ -473,23 +1042,13 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
473
1042
|
* @returns {Promise<{sessionId, lastModified, size, filePath}|null>}
|
|
474
1043
|
*/
|
|
475
1044
|
async function getSessionStatus(source, sessionId, options = {}) {
|
|
476
|
-
await
|
|
1045
|
+
await _ensureSessionCurrent(source, sessionId);
|
|
477
1046
|
const db = _getDb();
|
|
478
1047
|
const row = db.prepare(
|
|
479
1048
|
'SELECT session_id, mtime_ms, size, file_path FROM session_file WHERE source = ? AND session_id = ?'
|
|
480
1049
|
).get(source, sessionId);
|
|
481
|
-
|
|
482
1050
|
if (!row) return null;
|
|
483
1051
|
|
|
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
1052
|
return {
|
|
494
1053
|
sessionId: row.session_id,
|
|
495
1054
|
lastModified: row.mtime_ms,
|
|
@@ -575,7 +1134,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
575
1134
|
model: r.model,
|
|
576
1135
|
provider: r.provider,
|
|
577
1136
|
userMessageNumber: r.user_message_number,
|
|
578
|
-
extra:
|
|
1137
|
+
extra: _safeParseObject(r.extra_json)
|
|
579
1138
|
})),
|
|
580
1139
|
metadata: _buildMessageMetadata(sf),
|
|
581
1140
|
pagination: {
|
|
@@ -594,7 +1153,11 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
594
1153
|
* @returns {Promise<Array>}
|
|
595
1154
|
*/
|
|
596
1155
|
async function getRecentSessions(source, limit = 5, options = {}) {
|
|
597
|
-
await ensureSourceIndexed(source, {
|
|
1156
|
+
await ensureSourceIndexed(source, {
|
|
1157
|
+
force: options.force === true,
|
|
1158
|
+
consistency: options.consistency || 'stale-ok',
|
|
1159
|
+
config: options.config
|
|
1160
|
+
});
|
|
598
1161
|
const db = _getDb();
|
|
599
1162
|
|
|
600
1163
|
const rows = db.prepare(`
|
|
@@ -629,7 +1192,12 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
629
1192
|
* @returns {Promise<Array>}
|
|
630
1193
|
*/
|
|
631
1194
|
async function searchSessions(source, keyword, options = {}) {
|
|
632
|
-
|
|
1195
|
+
if (!String(keyword || '').trim()) return [];
|
|
1196
|
+
await ensureSourceIndexed(source, {
|
|
1197
|
+
force: options.force === true,
|
|
1198
|
+
consistency: options.consistency || 'complete',
|
|
1199
|
+
config: options.config
|
|
1200
|
+
});
|
|
633
1201
|
const db = _getDb();
|
|
634
1202
|
const contextLength = options.contextLength || 35;
|
|
635
1203
|
const projectName = options.projectName || null;
|
|
@@ -641,13 +1209,15 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
641
1209
|
const needle = String(keyword);
|
|
642
1210
|
if ([...needle].length >= 3) {
|
|
643
1211
|
const ftsRows = db.prepare(`
|
|
644
|
-
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp
|
|
1212
|
+
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
|
|
1213
|
+
sf.project_name, sf.project_display_name, sf.project_full_path,
|
|
1214
|
+
sf.file_path, sf.first_message, sf.updated_at
|
|
645
1215
|
FROM session_message_fts fts
|
|
646
1216
|
JOIN session_message sm ON sm.rowid = fts.rowid
|
|
647
1217
|
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
648
1218
|
WHERE sm.source = ? AND session_message_fts MATCH ?
|
|
649
1219
|
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
650
|
-
ORDER BY sf.updated_at DESC
|
|
1220
|
+
ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
|
|
651
1221
|
LIMIT 500
|
|
652
1222
|
`);
|
|
653
1223
|
const params = [source, _ftsQuote(needle)];
|
|
@@ -680,20 +1250,26 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
680
1250
|
idx += lowerKeyword.length;
|
|
681
1251
|
}
|
|
682
1252
|
if (count === 0) continue;
|
|
683
|
-
|
|
684
|
-
if (!
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1253
|
+
let entry = matchMap.get(c.session_id);
|
|
1254
|
+
if (!entry) {
|
|
1255
|
+
entry = {
|
|
1256
|
+
session: {
|
|
1257
|
+
source: c.source,
|
|
1258
|
+
session_id: c.session_id,
|
|
1259
|
+
project_name: c.project_name,
|
|
1260
|
+
project_display_name: c.project_display_name,
|
|
1261
|
+
project_full_path: c.project_full_path,
|
|
1262
|
+
file_path: c.file_path,
|
|
1263
|
+
first_message: c.first_message,
|
|
1264
|
+
updated_at: c.updated_at
|
|
1265
|
+
},
|
|
689
1266
|
matchCount: 0,
|
|
690
1267
|
messages: []
|
|
691
|
-
}
|
|
1268
|
+
};
|
|
1269
|
+
matchMap.set(c.session_id, entry);
|
|
692
1270
|
}
|
|
693
1271
|
|
|
694
|
-
|
|
695
|
-
// Claude: count every occurrence; Codex/Gemini/OMP: at most 1 per message
|
|
696
|
-
entry.matchCount += (source === 'claude') ? count : 1;
|
|
1272
|
+
entry.matchCount += source === 'claude' ? count : 1;
|
|
697
1273
|
entry.messages.push({
|
|
698
1274
|
ordinal: c.ordinal,
|
|
699
1275
|
content,
|
|
@@ -724,6 +1300,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
724
1300
|
|
|
725
1301
|
// Sort: matchCount DESC, then updated_at DESC
|
|
726
1302
|
results.sort((a, b) => {
|
|
1303
|
+
|
|
727
1304
|
const cm = b.matchCount - a.matchCount;
|
|
728
1305
|
if (cm !== 0) return cm;
|
|
729
1306
|
const aRow = matchMap.get(a.sessionId);
|
|
@@ -737,20 +1314,50 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
737
1314
|
}
|
|
738
1315
|
|
|
739
1316
|
function _scanMessagesRelational(db, source, keyword, projectName = null) {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1317
|
+
const useSqlMatch = /^[\x00-\x7F]*$/.test(String(keyword));
|
|
1318
|
+
const select = `
|
|
1319
|
+
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
|
|
1320
|
+
sf.project_name, sf.project_display_name, sf.project_full_path,
|
|
1321
|
+
sf.file_path, sf.first_message, sf.updated_at
|
|
744
1322
|
FROM session_message sm
|
|
745
1323
|
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
746
|
-
WHERE sm.source = ?
|
|
1324
|
+
WHERE sm.source = ?
|
|
747
1325
|
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
748
|
-
ORDER BY sf.updated_at DESC
|
|
749
|
-
LIMIT 500
|
|
1326
|
+
ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
|
|
750
1327
|
`;
|
|
751
|
-
|
|
752
|
-
if (
|
|
753
|
-
|
|
1328
|
+
|
|
1329
|
+
if (useSqlMatch) {
|
|
1330
|
+
const sql = `${select.replace('WHERE sm.source = ?', 'WHERE sm.source = ? AND instr(lower(sm.content), lower(?)) > 0')}\nLIMIT 500`;
|
|
1331
|
+
const params = [source, keyword];
|
|
1332
|
+
if (projectName) params.push(projectName);
|
|
1333
|
+
return db.prepare(sql).all(...params);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// SQLite's lower() is ASCII-oriented. Page the broad joined query and
|
|
1337
|
+
// apply the same locale-aware JavaScript matcher used by searchSessions,
|
|
1338
|
+
// collecting at most the 500 matching candidates (not 500 raw rows).
|
|
1339
|
+
const candidates = [];
|
|
1340
|
+
const pageSize = 500;
|
|
1341
|
+
let offset = 0;
|
|
1342
|
+
while (candidates.length < pageSize) {
|
|
1343
|
+
const params = [source];
|
|
1344
|
+
if (projectName) params.push(projectName);
|
|
1345
|
+
params.push(pageSize, offset);
|
|
1346
|
+
const batch = db.prepare(`${select}\nLIMIT ? OFFSET ?`).all(...params);
|
|
1347
|
+
if (batch.length === 0) break;
|
|
1348
|
+
for (const row of batch) {
|
|
1349
|
+
if (_containsLocaleMatch(row.content, keyword)) candidates.push(row);
|
|
1350
|
+
if (candidates.length >= pageSize) break;
|
|
1351
|
+
}
|
|
1352
|
+
if (batch.length < pageSize) break;
|
|
1353
|
+
offset += batch.length;
|
|
1354
|
+
}
|
|
1355
|
+
return candidates;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function _containsLocaleMatch(content, keyword) {
|
|
1359
|
+
const lowerKeyword = String(keyword).toLocaleLowerCase();
|
|
1360
|
+
return String(content || '').toLocaleLowerCase().indexOf(lowerKeyword) !== -1;
|
|
754
1361
|
}
|
|
755
1362
|
|
|
756
1363
|
function _extractContext(content, position, contextLength, keywordLength) {
|
|
@@ -778,8 +1385,8 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
778
1385
|
provider: sf.provider,
|
|
779
1386
|
model: sf.model,
|
|
780
1387
|
messageCount: sf.message_count,
|
|
781
|
-
usage:
|
|
782
|
-
extra:
|
|
1388
|
+
usage: _safeParseObject(sf.usage_json),
|
|
1389
|
+
extra: _safeParseObject(sf.extra_json)
|
|
783
1390
|
};
|
|
784
1391
|
}
|
|
785
1392
|
|
|
@@ -794,17 +1401,41 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
794
1401
|
).get(source, sessionId);
|
|
795
1402
|
if (!row) return;
|
|
796
1403
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1404
|
+
const fileKey = `${source}:${row.file_path}`;
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
const cached = fileVersions.get(fileKey);
|
|
1407
|
+
if (cached && now - cached.checkedAt < INDEX_INVENTORY_TTL_MS) return;
|
|
1408
|
+
|
|
1409
|
+
const activeCheck = fileChecks.get(fileKey);
|
|
1410
|
+
if (activeCheck) {
|
|
1411
|
+
await activeCheck;
|
|
802
1412
|
return;
|
|
803
1413
|
}
|
|
804
1414
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1415
|
+
const check = (async () => {
|
|
1416
|
+
let currentStat;
|
|
1417
|
+
try {
|
|
1418
|
+
currentStat = await fs.promises.stat(row.file_path);
|
|
1419
|
+
} catch (_) {
|
|
1420
|
+
fileVersions.set(fileKey, { size: -1, mtimeMs: -1, checkedAt: Date.now(), missing: true });
|
|
1421
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?').run(source, row.file_path);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
fileVersions.set(fileKey, {
|
|
1426
|
+
size: currentStat.size,
|
|
1427
|
+
mtimeMs: currentStat.mtimeMs,
|
|
1428
|
+
checkedAt: Date.now()
|
|
1429
|
+
});
|
|
1430
|
+
if (currentStat.size !== row.size || currentStat.mtimeMs !== row.mtime_ms) {
|
|
1431
|
+
await ensureSourceIndexed(source, { force: true, consistency: 'complete' });
|
|
1432
|
+
}
|
|
1433
|
+
})();
|
|
1434
|
+
fileChecks.set(fileKey, check);
|
|
1435
|
+
try {
|
|
1436
|
+
await check;
|
|
1437
|
+
} finally {
|
|
1438
|
+
if (fileChecks.get(fileKey) === check) fileChecks.delete(fileKey);
|
|
808
1439
|
}
|
|
809
1440
|
}
|
|
810
1441
|
|
|
@@ -819,16 +1450,38 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
819
1450
|
if (options.deleted && options.sessionId) {
|
|
820
1451
|
db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, options.sessionId);
|
|
821
1452
|
}
|
|
1453
|
+
if (options.projectName) {
|
|
1454
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND project_name = ?').run(source, options.projectName);
|
|
1455
|
+
}
|
|
822
1456
|
|
|
823
1457
|
// Mark state as stale
|
|
824
1458
|
db.prepare(
|
|
825
1459
|
'UPDATE source_state SET last_inventory_ms = NULL WHERE source = ?'
|
|
826
1460
|
).run(source);
|
|
1461
|
+
for (const key of fileVersions.keys()) {
|
|
1462
|
+
if (key.startsWith(`${source}:`)) fileVersions.delete(key);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function getSourceIndexMeta(source) {
|
|
1466
|
+
const row = _getDb().prepare(
|
|
1467
|
+
'SELECT last_inventory_ms, last_error FROM source_state WHERE source = ?'
|
|
1468
|
+
).get(source);
|
|
1469
|
+
const generatedAt = row?.last_inventory_ms ? Number(row.last_inventory_ms) : null;
|
|
1470
|
+
const stale = !generatedAt || Date.now() - generatedAt >= INDEX_INVENTORY_TTL_MS;
|
|
1471
|
+
const refreshing = _inflight.has(`ensure:${source}`);
|
|
1472
|
+
return {
|
|
1473
|
+
generatedAt,
|
|
1474
|
+
stale,
|
|
1475
|
+
refreshing,
|
|
1476
|
+
fallback: Boolean(row?.last_error && !_hasUsableIndexedData(source)),
|
|
1477
|
+
error: row?.last_error || null
|
|
1478
|
+
};
|
|
827
1479
|
}
|
|
828
|
-
|
|
829
1480
|
function closeSessionHistoryIndex() {
|
|
830
1481
|
closeDatabase(dbPath);
|
|
831
1482
|
_db = null;
|
|
1483
|
+
fileVersions.clear();
|
|
1484
|
+
fileChecks.clear();
|
|
832
1485
|
}
|
|
833
1486
|
|
|
834
1487
|
// Build the API object
|
|
@@ -842,6 +1495,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
842
1495
|
getRecentSessions,
|
|
843
1496
|
searchSessions,
|
|
844
1497
|
invalidateSource,
|
|
1498
|
+
getSourceIndexMeta,
|
|
845
1499
|
closeSessionHistoryIndex,
|
|
846
1500
|
};
|
|
847
1501
|
|
|
@@ -858,9 +1512,11 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
858
1512
|
|
|
859
1513
|
let _defaultIndex = null;
|
|
860
1514
|
|
|
861
|
-
function _getDefaultIndex() {
|
|
1515
|
+
function _getDefaultIndex(options = {}) {
|
|
862
1516
|
if (!_defaultIndex) {
|
|
863
|
-
_defaultIndex =
|
|
1517
|
+
_defaultIndex = Object.keys(options).length > 0
|
|
1518
|
+
? createSessionHistoryIndex({ config: options })
|
|
1519
|
+
: createSessionHistoryIndex();
|
|
864
1520
|
}
|
|
865
1521
|
return _defaultIndex;
|
|
866
1522
|
}
|
|
@@ -870,7 +1526,7 @@ async function ensureSourceIndexed(source, options) {
|
|
|
870
1526
|
}
|
|
871
1527
|
|
|
872
1528
|
async function listProjects(source, options) {
|
|
873
|
-
return _getDefaultIndex().listProjects(source, options);
|
|
1529
|
+
return _getDefaultIndex(options).listProjects(source, options);
|
|
874
1530
|
}
|
|
875
1531
|
|
|
876
1532
|
async function listSessions(source, projectName, options) {
|
|
@@ -897,6 +1553,10 @@ async function searchSessions(source, keyword, options) {
|
|
|
897
1553
|
return _getDefaultIndex().searchSessions(source, keyword, options);
|
|
898
1554
|
}
|
|
899
1555
|
|
|
1556
|
+
function getSourceIndexMeta(source) {
|
|
1557
|
+
return _getDefaultIndex().getSourceIndexMeta(source);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
900
1560
|
function invalidateSource(source, options) {
|
|
901
1561
|
_getDefaultIndex().invalidateSource(source, options);
|
|
902
1562
|
}
|
|
@@ -926,5 +1586,7 @@ module.exports = {
|
|
|
926
1586
|
getRecentSessions,
|
|
927
1587
|
searchSessions,
|
|
928
1588
|
invalidateSource,
|
|
929
|
-
|
|
1589
|
+
getSourceIndexMeta,
|
|
1590
|
+
closeSessionHistoryIndex,
|
|
1591
|
+
cleanupStaleInventoryLocks,
|
|
930
1592
|
};
|