@code-yeongyu/senpi 2026.9.23-5 → 2026.9.24-2
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 +51 -0
- package/dist/bundle/chunks/{anthropic-messages-77YBORJ5.js → anthropic-messages-UPLQFNTT.js} +2 -2
- package/dist/bundle/chunks/{app-server-command-BGT65PGC.js → app-server-command-UHPJ2C4D.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-PSESFNHN.js → azure-openai-responses-XAR2E66I.js} +1 -1
- package/dist/bundle/chunks/{chunk-WXIRFRXY.js → chunk-2PLPSKHD.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PANH3NB.js → chunk-3WBSBDBX.js} +1 -1
- package/dist/bundle/chunks/{chunk-BBOQ5H4W.js → chunk-6WBYI25N.js} +1 -1
- package/dist/bundle/chunks/{chunk-RG3UEFSF.js → chunk-7DY7UYLL.js} +1 -1
- package/dist/bundle/chunks/{chunk-VKBY4F76.js → chunk-BSOJHK4W.js} +1 -1
- package/dist/bundle/chunks/{chunk-3XHWW7SQ.js → chunk-CLZVNQW3.js} +1 -1
- package/dist/bundle/chunks/{chunk-ORCF262V.js → chunk-EJKCBRYK.js} +3 -3
- package/dist/bundle/chunks/chunk-J7IUURQ6.js +8 -0
- package/dist/bundle/chunks/{chunk-PSMTQDII.js → chunk-JRQSHZPP.js} +15 -9
- package/dist/bundle/chunks/{chunk-DUV3UFKX.js → chunk-MKAM7IRH.js} +1 -1
- package/dist/bundle/chunks/chunk-MO2U7YRX.js +11 -0
- package/dist/bundle/chunks/{chunk-ZEFQYQOU.js → chunk-NGHOUIPR.js} +1 -1
- package/dist/bundle/chunks/{chunk-XDSQYM4F.js → chunk-NKVUDCGZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-J3QXPP4T.js → chunk-ONR57UMO.js} +1 -1
- package/dist/bundle/chunks/{chunk-TQDWU3ZQ.js → chunk-SYQVC7UM.js} +1 -1
- package/dist/bundle/chunks/chunk-TTHMOL5G.js +2 -0
- package/dist/bundle/chunks/{chunk-NNHFMZD5.js → chunk-UBPMXBOJ.js} +1 -1
- package/dist/bundle/chunks/chunk-WASCMT6Y.js +2 -0
- package/dist/bundle/chunks/{chunk-EN6KDFY2.js → chunk-WISJJQ4K.js} +1 -1
- package/dist/bundle/chunks/{chunk-IVYZMSFY.js → chunk-WL5EQYNL.js} +1 -1
- package/dist/bundle/chunks/{chunk-LC6SZTVE.js → chunk-WTSAB6AE.js} +1 -1
- package/dist/bundle/chunks/{chunk-HKE42SWX.js → chunk-Y54V65MS.js} +1 -1
- package/dist/bundle/chunks/{chunk-3RN6BFG2.js → chunk-YBC274GY.js} +4 -4
- package/dist/bundle/chunks/{chunk-RRSND33J.js → chunk-YKU45B65.js} +54 -54
- package/dist/bundle/chunks/{chunk-WUYBEF7C.js → chunk-YWO3OBYK.js} +1 -1
- package/dist/bundle/chunks/cli-main-O5Y3TBID.js +3 -0
- package/dist/bundle/chunks/{google-generative-ai-YJHSZAY5.js → google-generative-ai-B5QMFONL.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-JTFWEDNJ.js → google-vertex-O2TGMVWF.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-QXVCHTP3.js → help-fast-path-Y33OIZBU.js} +1 -1
- package/dist/bundle/chunks/{host-command-3PUZYQOK.js → host-command-WJOL27Q7.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-OHPSU5VU.js → host-lifecycle-YBFXYFZP.js} +1 -1
- package/dist/bundle/chunks/{interactive-host-runtime-57LEAJPU.js → interactive-host-runtime-XVTF3NUD.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-OO3NGM4G.js +2 -0
- package/dist/bundle/chunks/{list-tips-Z62FXPOJ.js → list-tips-EBRNBPBM.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-VJPMPQ5T.js → mistral-conversations-AS3SAWPA.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-GFSDQQUJ.js → multi-session-host-6MKBBOKF.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-IQ35QACK.js → openai-codex-responses-CK7T4WNM.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-74OLE55U.js → openai-completions-OPQKN7FH.js} +3 -3
- package/dist/bundle/chunks/openai-responses-QHWM3KPO.js +2 -0
- package/dist/bundle/chunks/{openrouter-images-T4GY3JYM.js → openrouter-images-MW5TXV7L.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-J7244EJE.js → package-manager-cli-BMEWLAI2.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-CZT4TRPD.js → rotation-stream-ZFA6ELXJ.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-75CRTY24.js → rpc-mode-5653CV65.js} +1 -1
- package/dist/bundle/chunks/{session-picker-ZIBANFBQ.js → session-picker-PPYK56TA.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +265 -259
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +13 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +28 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +75 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/dynamic-prompt/build.d.ts +6 -1
- package/dist/core/dynamic-prompt/build.d.ts.map +1 -1
- package/dist/core/dynamic-prompt/build.js +0 -7
- package/dist/core/dynamic-prompt/build.js.map +1 -1
- package/dist/core/environment-context.d.ts +32 -0
- package/dist/core/environment-context.d.ts.map +1 -0
- package/dist/core/environment-context.js +63 -0
- package/dist/core/environment-context.js.map +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.d.ts.map +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +1 -15
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +3 -0
- package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts +19 -2
- package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/cache-warm.js +28 -2
- package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +4 -2
- package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
- package/dist/core/extensions/builtin/goal/parked-wait.js +2 -0
- package/dist/core/extensions/builtin/goal/parked-wait.js.map +1 -1
- package/dist/core/extensions/builtin/goal/prompt.js +1 -1
- package/dist/core/extensions/builtin/goal/prompt.js.map +1 -1
- package/dist/core/extensions/builtin/herdr/herdr-state.d.ts +5 -0
- package/dist/core/extensions/builtin/herdr/herdr-state.d.ts.map +1 -1
- package/dist/core/extensions/builtin/herdr/herdr-state.js +28 -3
- package/dist/core/extensions/builtin/herdr/herdr-state.js.map +1 -1
- package/dist/core/extensions/builtin/herdr/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/herdr/index.js +7 -1
- package/dist/core/extensions/builtin/herdr/index.js.map +1 -1
- package/dist/core/extensions/builtin/hooks/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/hooks/index.js +3 -15
- package/dist/core/extensions/builtin/hooks/index.js.map +1 -1
- package/dist/core/extensions/builtin/hooks/stop-lifecycle.d.ts +26 -0
- package/dist/core/extensions/builtin/hooks/stop-lifecycle.d.ts.map +1 -0
- package/dist/core/extensions/builtin/hooks/stop-lifecycle.js +88 -0
- package/dist/core/extensions/builtin/hooks/stop-lifecycle.js.map +1 -0
- package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.js +28 -10
- package/dist/core/extensions/builtin/recommended-models/index.js.map +1 -1
- package/dist/core/extensions/builtin/rules/commands.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/commands.js +5 -1
- package/dist/core/extensions/builtin/rules/commands.js.map +1 -1
- package/dist/core/extensions/builtin/rules/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/index.js +25 -3
- package/dist/core/extensions/builtin/rules/index.js.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/constants.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/constants.js +22 -14
- package/dist/core/extensions/builtin/rules/rules/constants.js.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/engine.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/engine.js +39 -9
- package/dist/core/extensions/builtin/rules/rules/engine.js.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/finder.js +3 -0
- package/dist/core/extensions/builtin/rules/rules/finder.js.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/project-root.d.ts +17 -0
- package/dist/core/extensions/builtin/rules/rules/project-root.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/project-root.js +67 -5
- package/dist/core/extensions/builtin/rules/rules/project-root.js.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/types.d.ts +2 -2
- package/dist/core/extensions/builtin/rules/rules/types.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/rules/types.js.map +1 -1
- package/dist/core/extensions/builtin/rules/ui/rules-banner.d.ts +3 -1
- package/dist/core/extensions/builtin/rules/ui/rules-banner.d.ts.map +1 -1
- package/dist/core/extensions/builtin/rules/ui/rules-banner.js +1 -1
- package/dist/core/extensions/builtin/rules/ui/rules-banner.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/todo-storage.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/todo-storage.js +10 -2
- package/dist/core/extensions/builtin/todotools/todo-storage.js.map +1 -1
- package/dist/core/extensions/builtin/todotools/tools/todo.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/tools/todo.js +2 -1
- package/dist/core/extensions/builtin/todotools/tools/todo.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/config.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/config.js +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/config.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.js +2 -0
- package/dist/core/extensions/builtin/websearch/websearch/provider-endpoints.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/providers/kagi.d.ts +3 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/kagi.d.ts.map +1 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/kagi.js +34 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/kagi.js.map +1 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/serpdive.d.ts +3 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/serpdive.d.ts.map +1 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/serpdive.js +22 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/serpdive.js.map +1 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/providers.js +4 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers.js.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/types.d.ts +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/types.d.ts.map +1 -1
- package/dist/core/extensions/builtin/websearch/websearch/types.js.map +1 -1
- package/dist/core/session-discovery.d.ts +9 -3
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +19 -8
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +8 -4
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-summary-cache.d.ts +16 -3
- package/dist/core/session-summary-cache.d.ts.map +1 -1
- package/dist/core/session-summary-cache.js +19 -3
- package/dist/core/session-summary-cache.js.map +1 -1
- package/dist/core/session-summary-index-file.d.ts +38 -0
- package/dist/core/session-summary-index-file.d.ts.map +1 -0
- package/dist/core/session-summary-index-file.js +119 -0
- package/dist/core/session-summary-index-file.js.map +1 -0
- package/dist/core/session-summary-index.d.ts +52 -0
- package/dist/core/session-summary-index.d.ts.map +1 -0
- package/dist/core/session-summary-index.js +157 -0
- package/dist/core/session-summary-index.js.map +1 -0
- package/dist/core/session-summary.d.ts +21 -2
- package/dist/core/session-summary.d.ts.map +1 -1
- package/dist/core/session-summary.js +53 -14
- package/dist/core/session-summary.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/renderers/read.d.ts.map +1 -1
- package/dist/core/tools/renderers/read.js +7 -2
- package/dist/core/tools/renderers/read.js.map +1 -1
- package/dist/core/tools/renderers/skill-read-path.d.ts +9 -0
- package/dist/core/tools/renderers/skill-read-path.d.ts.map +1 -0
- package/dist/core/tools/renderers/skill-read-path.js +49 -0
- package/dist/core/tools/renderers/skill-read-path.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/exploration-call.d.ts +1 -1
- package/dist/modes/interactive/components/exploration-call.d.ts.map +1 -1
- package/dist/modes/interactive/components/exploration-call.js +3 -1
- package/dist/modes/interactive/components/exploration-call.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +32 -16
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-tree.d.ts +32 -0
- package/dist/modes/interactive/components/session-selector-tree.d.ts.map +1 -0
- package/dist/modes/interactive/components/session-selector-tree.js +82 -0
- package/dist/modes/interactive/components/session-selector-tree.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +6 -71
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +32 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +8 -4
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/AGENTS.md +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/code-preview.ts +31 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-code.ts +34 -131
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-js-ast.ts +65 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-js-layout.ts +90 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-js-mask.ts +178 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-js.ts +100 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-python-script.ts +127 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/display-python.ts +150 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-request.ts +18 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +9 -25
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +16 -9
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +22 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +3 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/tool-name-alias.d.ts +6 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/tool-name-alias.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/tool-name-alias.js +6 -19
- package/node_modules/@earendil-works/pi-agent-core/dist/tool-name-alias.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +8 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-references.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-references.js +8 -35
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-references.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +10 -6
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +4 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +53 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.js +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/openai-responses-compat.d.ts +17 -0
- package/node_modules/@earendil-works/pi-ai/dist/openai-responses-compat.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/openai-responses-compat.js +4 -1
- package/node_modules/@earendil-works/pi-ai/dist/openai-responses-compat.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/chatgpt-subscription.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/google.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/venice.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +6 -0
- package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js +71 -14
- package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/tool-name-match.d.ts +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/tool-name-match.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/tool-name-match.js +71 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/tool-name-match.js.map +1 -0
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts +5 -0
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js +7 -2
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.js +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/chunk-KLDLZHHS.js +0 -11
- package/dist/bundle/chunks/chunk-LTIMMSDM.js +0 -8
- package/dist/bundle/chunks/chunk-XYM54ZWK.js +0 -2
- package/dist/bundle/chunks/cli-main-W55CWPEI.js +0 -3
- package/dist/bundle/chunks/interactive-mode-EDJZ4CSY.js +0 -2
- package/dist/bundle/chunks/openai-responses-7IGZSW33.js +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SessionSummary } from "./session-summary.ts";
|
|
2
|
+
import { type FileStamp } from "./session-summary-lru.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Upper bound on cached session files, evicted least-recently-used rather than
|
|
4
5
|
* grown with the sessions directory.
|
|
@@ -20,6 +21,15 @@ export type CachedSessionSummary = {
|
|
|
20
21
|
readonly summary: SessionSummary;
|
|
21
22
|
readonly mtime: Date;
|
|
22
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* A second cache level consulted on an in-memory miss, such as the persistent
|
|
26
|
+
* per-directory summary index. `record` receives every summary served from
|
|
27
|
+
* memory or streamed, so the store can persist what it lacks.
|
|
28
|
+
*/
|
|
29
|
+
export type SessionSummaryStore = {
|
|
30
|
+
lookup(filePath: string, stamp: FileStamp): Promise<SessionSummary | undefined>;
|
|
31
|
+
record(filePath: string, stamp: FileStamp, summary: SessionSummary): void;
|
|
32
|
+
};
|
|
23
33
|
/**
|
|
24
34
|
* Exact summary of one session file, reusing the cached summary when the file's
|
|
25
35
|
* size and mtime are unchanged since it was read.
|
|
@@ -28,10 +38,13 @@ export type CachedSessionSummary = {
|
|
|
28
38
|
* sessions directory costs one `stat` per file. A file that cannot be stat'ed or
|
|
29
39
|
* read drops its cache entry, so a truncated or replaced file is never served
|
|
30
40
|
* from stale bytes. A summary too large for the cache's byte budget is returned
|
|
31
|
-
* to the caller without being retained.
|
|
41
|
+
* to the caller without being retained. On a miss, `store` is consulted before
|
|
42
|
+
* the file is streamed.
|
|
32
43
|
*/
|
|
33
|
-
export declare function readCachedSessionSummary(filePath: string): Promise<CachedSessionSummary | null>;
|
|
34
|
-
/**
|
|
44
|
+
export declare function readCachedSessionSummary(filePath: string, store?: SessionSummaryStore): Promise<CachedSessionSummary | null>;
|
|
45
|
+
/** Session files streamed since process start. Test-only seam for index-hit assertions. */
|
|
46
|
+
export declare function sessionSummaryStreamCount(): number;
|
|
47
|
+
/** Drop every cached summary and index snapshot. Test-only seam for cold-read assertions. */
|
|
35
48
|
export declare function clearSessionSummaryCache(): void;
|
|
36
49
|
/** Number of cached summaries. Test-only seam for eviction assertions. */
|
|
37
50
|
export declare function sessionSummaryCacheSize(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-summary-cache.d.ts","sourceRoot":"","sources":["../../src/core/session-summary-cache.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-summary-cache.d.ts","sourceRoot":"","sources":["../../src/core/session-summary-cache.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,0BAA0B,CAAC;AAE7E;;;GAGG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oCAAoC,QAAmB,CAAC;AAErE,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IACjC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChF,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CAC1E,CAAC;AAcF;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC7C,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAqCtC;AAED,2FAA2F;AAC3F,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,6FAA6F;AAC7F,wBAAgB,wBAAwB,IAAI,IAAI,CAG/C;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED,wFAAwF;AACxF,wBAAgB,4BAA4B,IAAI,MAAM,CAErD"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stat } from "fs/promises";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { readSessionSummary } from "./session-summary.js";
|
|
4
|
+
import { forgetSessionSummaryIndexSnapshots } from "./session-summary-index.js";
|
|
4
5
|
import { SessionSummaryLru } from "./session-summary-lru.js";
|
|
5
6
|
/**
|
|
6
7
|
* Upper bound on cached session files, evicted least-recently-used rather than
|
|
@@ -24,6 +25,7 @@ const cache = new SessionSummaryLru({
|
|
|
24
25
|
maxEntries: SESSION_SUMMARY_CACHE_LIMIT,
|
|
25
26
|
maxTextBytes: SESSION_SUMMARY_CACHE_MAX_TEXT_BYTES,
|
|
26
27
|
});
|
|
28
|
+
let streamedSummaries = 0;
|
|
27
29
|
function stampsMatch(left, right) {
|
|
28
30
|
return left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
29
31
|
}
|
|
@@ -35,9 +37,10 @@ function stampsMatch(left, right) {
|
|
|
35
37
|
* sessions directory costs one `stat` per file. A file that cannot be stat'ed or
|
|
36
38
|
* read drops its cache entry, so a truncated or replaced file is never served
|
|
37
39
|
* from stale bytes. A summary too large for the cache's byte budget is returned
|
|
38
|
-
* to the caller without being retained.
|
|
40
|
+
* to the caller without being retained. On a miss, `store` is consulted before
|
|
41
|
+
* the file is streamed.
|
|
39
42
|
*/
|
|
40
|
-
export async function readCachedSessionSummary(filePath) {
|
|
43
|
+
export async function readCachedSessionSummary(filePath, store) {
|
|
41
44
|
const key = resolve(filePath);
|
|
42
45
|
let stamp;
|
|
43
46
|
let mtime;
|
|
@@ -54,19 +57,32 @@ export async function readCachedSessionSummary(filePath) {
|
|
|
54
57
|
}
|
|
55
58
|
const cached = cache.get(key);
|
|
56
59
|
if (cached && stampsMatch(cached.stamp, stamp)) {
|
|
60
|
+
store?.record(filePath, stamp, cached.summary);
|
|
57
61
|
return { summary: cached.summary, mtime: cached.mtime };
|
|
58
62
|
}
|
|
63
|
+
const stored = await store?.lookup(filePath, stamp);
|
|
64
|
+
if (stored) {
|
|
65
|
+
cache.retain(key, { stamp, summary: stored, mtime });
|
|
66
|
+
return { summary: stored, mtime };
|
|
67
|
+
}
|
|
68
|
+
streamedSummaries++;
|
|
59
69
|
const summary = await readSessionSummary(filePath);
|
|
60
70
|
if (!summary) {
|
|
61
71
|
cache.drop(key);
|
|
62
72
|
return null;
|
|
63
73
|
}
|
|
64
74
|
cache.retain(key, { stamp, summary, mtime });
|
|
75
|
+
store?.record(filePath, stamp, summary);
|
|
65
76
|
return { summary, mtime };
|
|
66
77
|
}
|
|
67
|
-
/**
|
|
78
|
+
/** Session files streamed since process start. Test-only seam for index-hit assertions. */
|
|
79
|
+
export function sessionSummaryStreamCount() {
|
|
80
|
+
return streamedSummaries;
|
|
81
|
+
}
|
|
82
|
+
/** Drop every cached summary and index snapshot. Test-only seam for cold-read assertions. */
|
|
68
83
|
export function clearSessionSummaryCache() {
|
|
69
84
|
cache.clear();
|
|
85
|
+
forgetSessionSummaryIndexSnapshots();
|
|
70
86
|
}
|
|
71
87
|
/** Number of cached summaries. Test-only seam for eviction assertions. */
|
|
72
88
|
export function sessionSummaryCacheSize() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-summary-cache.js","sourceRoot":"","sources":["../../src/core/session-summary-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAuB,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAkB,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7E;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oCAAoC,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"session-summary-cache.js","sourceRoot":"","sources":["../../src/core/session-summary-cache.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat } from \"fs/promises\";\nimport { resolve } from \"path\";\nimport { readSessionSummary, type SessionSummary } from \"./session-summary.ts\";\nimport { forgetSessionSummaryIndexSnapshots } from \"./session-summary-index.ts\";\nimport { type FileStamp, SessionSummaryLru } from \"./session-summary-lru.ts\";\n\n/**\n * Upper bound on cached session files, evicted least-recently-used rather than\n * grown with the sessions directory.\n */\nexport const SESSION_SUMMARY_CACHE_LIMIT = 4096;\n\n/**\n * Upper bound on retained transcript text across the whole cache.\n *\n * A cached summary's footprint is its `allMessagesText`, which grows with the\n * session, so the entry ceiling alone bounds nothing: 4096 large sessions would\n * pin gigabytes for the life of the process. 64 MiB is deliberately conservative\n * — it holds a normal user's entire sessions directory (a heavy 2 MB session\n * file yields well under 2 MB of visible text, so hundreds of them fit) while\n * capping the worst case at a fraction of a Node heap. Exceeding it evicts the\n * least recently listed sessions, which then cost one streaming re-read.\n */\nexport const SESSION_SUMMARY_CACHE_MAX_TEXT_BYTES = 64 * 1024 * 1024;\n\nexport type CachedSessionSummary = {\n\treadonly summary: SessionSummary;\n\treadonly mtime: Date;\n};\n\n/**\n * A second cache level consulted on an in-memory miss, such as the persistent\n * per-directory summary index. `record` receives every summary served from\n * memory or streamed, so the store can persist what it lacks.\n */\nexport type SessionSummaryStore = {\n\tlookup(filePath: string, stamp: FileStamp): Promise<SessionSummary | undefined>;\n\trecord(filePath: string, stamp: FileStamp, summary: SessionSummary): void;\n};\n\n/** Process-local summary cache, bounded by entry count and retained text bytes. */\nconst cache = new SessionSummaryLru({\n\tmaxEntries: SESSION_SUMMARY_CACHE_LIMIT,\n\tmaxTextBytes: SESSION_SUMMARY_CACHE_MAX_TEXT_BYTES,\n});\n\nlet streamedSummaries = 0;\n\nfunction stampsMatch(left: FileStamp, right: FileStamp): boolean {\n\treturn left.size === right.size && left.mtimeMs === right.mtimeMs;\n}\n\n/**\n * Exact summary of one session file, reusing the cached summary when the file's\n * size and mtime are unchanged since it was read.\n *\n * A cache hit deserializes nothing: the whole point is that listing an unchanged\n * sessions directory costs one `stat` per file. A file that cannot be stat'ed or\n * read drops its cache entry, so a truncated or replaced file is never served\n * from stale bytes. A summary too large for the cache's byte budget is returned\n * to the caller without being retained. On a miss, `store` is consulted before\n * the file is streamed.\n */\nexport async function readCachedSessionSummary(\n\tfilePath: string,\n\tstore?: SessionSummaryStore,\n): Promise<CachedSessionSummary | null> {\n\tconst key = resolve(filePath);\n\n\tlet stamp: FileStamp;\n\tlet mtime: Date;\n\ttry {\n\t\tconst stats = await stat(filePath);\n\t\tstamp = { size: stats.size, mtimeMs: stats.mtimeMs };\n\t\tmtime = stats.mtime;\n\t} catch (error) {\n\t\tif (!(error instanceof Error)) throw error;\n\t\tcache.drop(key);\n\t\treturn null;\n\t}\n\n\tconst cached = cache.get(key);\n\tif (cached && stampsMatch(cached.stamp, stamp)) {\n\t\tstore?.record(filePath, stamp, cached.summary);\n\t\treturn { summary: cached.summary, mtime: cached.mtime };\n\t}\n\n\tconst stored = await store?.lookup(filePath, stamp);\n\tif (stored) {\n\t\tcache.retain(key, { stamp, summary: stored, mtime });\n\t\treturn { summary: stored, mtime };\n\t}\n\n\tstreamedSummaries++;\n\tconst summary = await readSessionSummary(filePath);\n\tif (!summary) {\n\t\tcache.drop(key);\n\t\treturn null;\n\t}\n\n\tcache.retain(key, { stamp, summary, mtime });\n\tstore?.record(filePath, stamp, summary);\n\treturn { summary, mtime };\n}\n\n/** Session files streamed since process start. Test-only seam for index-hit assertions. */\nexport function sessionSummaryStreamCount(): number {\n\treturn streamedSummaries;\n}\n\n/** Drop every cached summary and index snapshot. Test-only seam for cold-read assertions. */\nexport function clearSessionSummaryCache(): void {\n\tcache.clear();\n\tforgetSessionSummaryIndexSnapshots();\n}\n\n/** Number of cached summaries. Test-only seam for eviction assertions. */\nexport function sessionSummaryCacheSize(): number {\n\treturn cache.size;\n}\n\n/** Retained transcript bytes across the cache. Test-only seam for budget assertions. */\nexport function sessionSummaryCacheTextBytes(): number {\n\treturn cache.textBytes;\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type SessionSummary } from "./session-summary.ts";
|
|
2
|
+
import type { FileStamp } from "./session-summary-lru.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Name of the summary index inside the sessions directory it describes. The
|
|
5
|
+
* extension is deliberately not `.jsonl`: every `*.jsonl` there is a session.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SESSION_SUMMARY_INDEX_FILE = ".session-summaries.index";
|
|
8
|
+
/** One indexed session file: its basename, the stamp it was read at, and its summary. */
|
|
9
|
+
export type IndexEntry = {
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly stamp: FileStamp;
|
|
12
|
+
readonly summary: SessionSummary;
|
|
13
|
+
/** Serialized line, newline included. */
|
|
14
|
+
readonly line: string;
|
|
15
|
+
readonly lineBytes: number;
|
|
16
|
+
};
|
|
17
|
+
/** An index as loaded from disk; an absent or unusable file loads as `usable: false`. */
|
|
18
|
+
export type LoadedIndex = {
|
|
19
|
+
readonly usable: boolean;
|
|
20
|
+
/** Deduplicated entries, last line wins. */
|
|
21
|
+
readonly entries: ReadonlyMap<string, IndexEntry>;
|
|
22
|
+
/** Bytes after the header line, including torn and superseded lines. */
|
|
23
|
+
readonly entryBytes: number;
|
|
24
|
+
/** False when the file ends in a torn line, so the next append must start a new line. */
|
|
25
|
+
readonly endsWithNewline: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** Serialize one entry, or undefined when the summary would not survive a JSON round trip. */
|
|
28
|
+
export declare function toIndexEntry(file: string, stamp: FileStamp, summary: SessionSummary): IndexEntry | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Load an index, tolerating every failure: a missing file, a missing or foreign
|
|
31
|
+
* version header, or a read error loads as unusable; a line that does not parse
|
|
32
|
+
* (a torn append, truncation, garbage) is skipped.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadIndexFile(path: string): Promise<LoadedIndex>;
|
|
35
|
+
/** Append entries one whole line per write, first closing a torn tail when there is one. */
|
|
36
|
+
export declare function appendIndexEntries(path: string, entries: readonly IndexEntry[], endsWithNewline: boolean): Promise<void>;
|
|
37
|
+
export declare function rewriteIndexFile(path: string, entries: readonly IndexEntry[]): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=session-summary-index-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-summary-index-file.d.ts","sourceRoot":"","sources":["../../src/core/session-summary-index-file.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AAKrE,yFAAyF;AACzF,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClD,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC,CAAC;AA6BF,8FAA8F;AAC9F,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,UAAU,GAAG,SAAS,CAQ5G;AA6BD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0BtE;AAED,4FAA4F;AAC5F,wBAAsB,kBAAkB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,eAAe,EAAE,OAAO,GACtB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CASlG"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { appendFile, rename, rm, writeFile } from "fs/promises";
|
|
2
|
+
import { readFileLines } from "./session-summary.js";
|
|
3
|
+
/**
|
|
4
|
+
* Name of the summary index inside the sessions directory it describes. The
|
|
5
|
+
* extension is deliberately not `.jsonl`: every `*.jsonl` there is a session.
|
|
6
|
+
*/
|
|
7
|
+
export const SESSION_SUMMARY_INDEX_FILE = ".session-summaries.index";
|
|
8
|
+
const INDEX_VERSION = 1;
|
|
9
|
+
const HEADER_LINE = `${JSON.stringify({ version: INDEX_VERSION })}\n`;
|
|
10
|
+
const UNUSABLE = { usable: false, entries: new Map(), entryBytes: 0, endsWithNewline: true };
|
|
11
|
+
function isObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null;
|
|
13
|
+
}
|
|
14
|
+
function isOptional(value, type) {
|
|
15
|
+
return value === undefined || typeof value === type;
|
|
16
|
+
}
|
|
17
|
+
function isSessionSummary(value) {
|
|
18
|
+
if (!isObject(value) || !isObject(value.header))
|
|
19
|
+
return false;
|
|
20
|
+
return (value.header.type === "session" &&
|
|
21
|
+
typeof value.header.id === "string" &&
|
|
22
|
+
typeof value.firstUserMessage === "string" &&
|
|
23
|
+
typeof value.messageCount === "number" &&
|
|
24
|
+
typeof value.allMessagesText === "string" &&
|
|
25
|
+
isOptional(value.name, "string") &&
|
|
26
|
+
isOptional(value.lastActivityTime, "number"));
|
|
27
|
+
}
|
|
28
|
+
function isFiniteNumber(value) {
|
|
29
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
30
|
+
}
|
|
31
|
+
/** Serialize one entry, or undefined when the summary would not survive a JSON round trip. */
|
|
32
|
+
export function toIndexEntry(file, stamp, summary) {
|
|
33
|
+
if (summary.lastActivityTime !== undefined && !Number.isFinite(summary.lastActivityTime))
|
|
34
|
+
return undefined;
|
|
35
|
+
return withLine(file, stamp, summary, `${JSON.stringify({ file, size: stamp.size, mtimeMs: stamp.mtimeMs, summary })}\n`);
|
|
36
|
+
}
|
|
37
|
+
function withLine(file, stamp, summary, line) {
|
|
38
|
+
return { file, stamp, summary, line, lineBytes: Buffer.byteLength(line, "utf8") };
|
|
39
|
+
}
|
|
40
|
+
function parseEntryLine(line) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (!isObject(parsed) || typeof parsed.file !== "string" || parsed.file.includes("/"))
|
|
49
|
+
return undefined;
|
|
50
|
+
if (!isFiniteNumber(parsed.size) || !isFiniteNumber(parsed.mtimeMs) || !isSessionSummary(parsed.summary)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return withLine(parsed.file, { size: parsed.size, mtimeMs: parsed.mtimeMs }, parsed.summary, `${line}\n`);
|
|
54
|
+
}
|
|
55
|
+
function isCurrentHeader(line) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(line);
|
|
58
|
+
return isObject(parsed) && parsed.version === INDEX_VERSION;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Load an index, tolerating every failure: a missing file, a missing or foreign
|
|
66
|
+
* version header, or a read error loads as unusable; a line that does not parse
|
|
67
|
+
* (a torn append, truncation, garbage) is skipped.
|
|
68
|
+
*/
|
|
69
|
+
export async function loadIndexFile(path) {
|
|
70
|
+
const entries = new Map();
|
|
71
|
+
let headerSeen = false;
|
|
72
|
+
let headerBytes = 0;
|
|
73
|
+
try {
|
|
74
|
+
const read = await readFileLines(path, (line) => {
|
|
75
|
+
if (!headerSeen) {
|
|
76
|
+
headerSeen = true;
|
|
77
|
+
headerBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
78
|
+
return isCurrentHeader(line);
|
|
79
|
+
}
|
|
80
|
+
const entry = parseEntryLine(line);
|
|
81
|
+
if (entry)
|
|
82
|
+
entries.set(entry.file, entry);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
if (!read.completed || !headerSeen)
|
|
86
|
+
return UNUSABLE;
|
|
87
|
+
return {
|
|
88
|
+
usable: true,
|
|
89
|
+
entries,
|
|
90
|
+
entryBytes: Math.max(0, read.bytes - headerBytes),
|
|
91
|
+
endsWithNewline: read.endsWithNewline,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error)
|
|
96
|
+
return UNUSABLE;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Append entries one whole line per write, first closing a torn tail when there is one. */
|
|
101
|
+
export async function appendIndexEntries(path, entries, endsWithNewline) {
|
|
102
|
+
let prefix = endsWithNewline ? "" : "\n";
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
await appendFile(path, prefix + entry.line);
|
|
105
|
+
prefix = "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function rewriteIndexFile(path, entries) {
|
|
109
|
+
const temp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
110
|
+
try {
|
|
111
|
+
await writeFile(temp, HEADER_LINE + entries.map((entry) => entry.line).join(""));
|
|
112
|
+
await rename(temp, path);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=session-summary-index-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-summary-index-file.js","sourceRoot":"","sources":["../../src/core/session-summary-index-file.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, rename, rm, writeFile } from \"fs/promises\";\nimport { readFileLines, type SessionSummary } from \"./session-summary.ts\";\nimport type { FileStamp } from \"./session-summary-lru.ts\";\n\n/**\n * Name of the summary index inside the sessions directory it describes. The\n * extension is deliberately not `.jsonl`: every `*.jsonl` there is a session.\n */\nexport const SESSION_SUMMARY_INDEX_FILE = \".session-summaries.index\";\n\nconst INDEX_VERSION = 1;\nconst HEADER_LINE = `${JSON.stringify({ version: INDEX_VERSION })}\\n`;\n\n/** One indexed session file: its basename, the stamp it was read at, and its summary. */\nexport type IndexEntry = {\n\treadonly file: string;\n\treadonly stamp: FileStamp;\n\treadonly summary: SessionSummary;\n\t/** Serialized line, newline included. */\n\treadonly line: string;\n\treadonly lineBytes: number;\n};\n\n/** An index as loaded from disk; an absent or unusable file loads as `usable: false`. */\nexport type LoadedIndex = {\n\treadonly usable: boolean;\n\t/** Deduplicated entries, last line wins. */\n\treadonly entries: ReadonlyMap<string, IndexEntry>;\n\t/** Bytes after the header line, including torn and superseded lines. */\n\treadonly entryBytes: number;\n\t/** False when the file ends in a torn line, so the next append must start a new line. */\n\treadonly endsWithNewline: boolean;\n};\n\nconst UNUSABLE: LoadedIndex = { usable: false, entries: new Map(), entryBytes: 0, endsWithNewline: true };\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction isOptional(value: unknown, type: \"string\" | \"number\"): boolean {\n\treturn value === undefined || typeof value === type;\n}\n\nfunction isSessionSummary(value: unknown): value is SessionSummary {\n\tif (!isObject(value) || !isObject(value.header)) return false;\n\treturn (\n\t\tvalue.header.type === \"session\" &&\n\t\ttypeof value.header.id === \"string\" &&\n\t\ttypeof value.firstUserMessage === \"string\" &&\n\t\ttypeof value.messageCount === \"number\" &&\n\t\ttypeof value.allMessagesText === \"string\" &&\n\t\tisOptional(value.name, \"string\") &&\n\t\tisOptional(value.lastActivityTime, \"number\")\n\t);\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value);\n}\n\n/** Serialize one entry, or undefined when the summary would not survive a JSON round trip. */\nexport function toIndexEntry(file: string, stamp: FileStamp, summary: SessionSummary): IndexEntry | undefined {\n\tif (summary.lastActivityTime !== undefined && !Number.isFinite(summary.lastActivityTime)) return undefined;\n\treturn withLine(\n\t\tfile,\n\t\tstamp,\n\t\tsummary,\n\t\t`${JSON.stringify({ file, size: stamp.size, mtimeMs: stamp.mtimeMs, summary })}\\n`,\n\t);\n}\n\nfunction withLine(file: string, stamp: FileStamp, summary: SessionSummary, line: string): IndexEntry {\n\treturn { file, stamp, summary, line, lineBytes: Buffer.byteLength(line, \"utf8\") };\n}\n\nfunction parseEntryLine(line: string): IndexEntry | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!isObject(parsed) || typeof parsed.file !== \"string\" || parsed.file.includes(\"/\")) return undefined;\n\tif (!isFiniteNumber(parsed.size) || !isFiniteNumber(parsed.mtimeMs) || !isSessionSummary(parsed.summary)) {\n\t\treturn undefined;\n\t}\n\treturn withLine(parsed.file, { size: parsed.size, mtimeMs: parsed.mtimeMs }, parsed.summary, `${line}\\n`);\n}\n\nfunction isCurrentHeader(line: string): boolean {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(line);\n\t\treturn isObject(parsed) && parsed.version === INDEX_VERSION;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Load an index, tolerating every failure: a missing file, a missing or foreign\n * version header, or a read error loads as unusable; a line that does not parse\n * (a torn append, truncation, garbage) is skipped.\n */\nexport async function loadIndexFile(path: string): Promise<LoadedIndex> {\n\tconst entries = new Map<string, IndexEntry>();\n\tlet headerSeen = false;\n\tlet headerBytes = 0;\n\ttry {\n\t\tconst read = await readFileLines(path, (line) => {\n\t\t\tif (!headerSeen) {\n\t\t\t\theaderSeen = true;\n\t\t\t\theaderBytes = Buffer.byteLength(line, \"utf8\") + 1;\n\t\t\t\treturn isCurrentHeader(line);\n\t\t\t}\n\t\t\tconst entry = parseEntryLine(line);\n\t\t\tif (entry) entries.set(entry.file, entry);\n\t\t\treturn true;\n\t\t});\n\t\tif (!read.completed || !headerSeen) return UNUSABLE;\n\t\treturn {\n\t\t\tusable: true,\n\t\t\tentries,\n\t\t\tentryBytes: Math.max(0, read.bytes - headerBytes),\n\t\t\tendsWithNewline: read.endsWithNewline,\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof Error) return UNUSABLE;\n\t\tthrow error;\n\t}\n}\n\n/** Append entries one whole line per write, first closing a torn tail when there is one. */\nexport async function appendIndexEntries(\n\tpath: string,\n\tentries: readonly IndexEntry[],\n\tendsWithNewline: boolean,\n): Promise<void> {\n\tlet prefix = endsWithNewline ? \"\" : \"\\n\";\n\tfor (const entry of entries) {\n\t\tawait appendFile(path, prefix + entry.line);\n\t\tprefix = \"\";\n\t}\n}\n\nexport async function rewriteIndexFile(path: string, entries: readonly IndexEntry[]): Promise<void> {\n\tconst temp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n\ttry {\n\t\tawait writeFile(temp, HEADER_LINE + entries.map((entry) => entry.line).join(\"\"));\n\t\tawait rename(temp, path);\n\t} catch (error) {\n\t\tawait rm(temp, { force: true }).catch(() => undefined);\n\t\tthrow error;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SessionSummary } from "./session-summary.ts";
|
|
2
|
+
import type { SessionSummaryStore } from "./session-summary-cache.ts";
|
|
3
|
+
import type { FileStamp } from "./session-summary-lru.ts";
|
|
4
|
+
export { SESSION_SUMMARY_INDEX_FILE } from "./session-summary-index-file.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Upper bound on live index bytes per sessions directory. It sits well above the
|
|
7
|
+
* in-memory cache's 64 MiB because the index is read once per cold listing, not
|
|
8
|
+
* retained; past it the least recently active sessions are left out and stream
|
|
9
|
+
* like any cache miss.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SESSION_SUMMARY_INDEX_MAX_BYTES: number;
|
|
12
|
+
export type SessionSummaryIndexOptions = {
|
|
13
|
+
readonly maxBytes?: number;
|
|
14
|
+
};
|
|
15
|
+
/** Forget every index snapshot, as a fresh process would start. */
|
|
16
|
+
export declare function forgetSessionSummaryIndexSnapshots(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Persistent summaries for one sessions directory, stored as
|
|
19
|
+
* `<dir>/.session-summaries.index` (senpi#2087).
|
|
20
|
+
*
|
|
21
|
+
* It is the second cache level behind the in-memory LRU: a cold process lists an
|
|
22
|
+
* unchanged directory by reading this one file instead of re-streaming every
|
|
23
|
+
* session. The file is loaded lazily on the first in-memory miss it can serve,
|
|
24
|
+
* so a warm process does not read it again for a session that just changed.
|
|
25
|
+
* Nothing here throws or logs: an unreadable, corrupt,
|
|
26
|
+
* or unwritable index only means the listing streams, and its rows are
|
|
27
|
+
* identical either way.
|
|
28
|
+
*/
|
|
29
|
+
export declare class SessionSummaryIndex implements SessionSummaryStore {
|
|
30
|
+
private readonly path;
|
|
31
|
+
private readonly maxBytes;
|
|
32
|
+
private loading;
|
|
33
|
+
private snapshot;
|
|
34
|
+
private readonly recorded;
|
|
35
|
+
constructor(dir: string, options?: SessionSummaryIndexOptions);
|
|
36
|
+
lookup(filePath: string, stamp: FileStamp): Promise<SessionSummary | undefined>;
|
|
37
|
+
record(filePath: string, stamp: FileStamp, summary: SessionSummary): void;
|
|
38
|
+
/**
|
|
39
|
+
* Bring the index up to date after a listing of `presentFiles`.
|
|
40
|
+
*
|
|
41
|
+
* Summaries the index lacks are appended one line each. The file is rewritten
|
|
42
|
+
* instead (temp file, then rename) when it is unusable, or when its entry
|
|
43
|
+
* bytes exceed twice the live entries' bytes: superseded lines, torn lines,
|
|
44
|
+
* removed sessions, and entries over the byte budget all count as dead.
|
|
45
|
+
*/
|
|
46
|
+
persist(presentFiles: readonly string[]): Promise<void>;
|
|
47
|
+
private currentSnapshot;
|
|
48
|
+
/** Stat before reading, so a concurrent writer can only make the snapshot look stale. */
|
|
49
|
+
private load;
|
|
50
|
+
private remember;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=session-summary-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-summary-index.d.ts","sourceRoot":"","sources":["../../src/core/session-summary-index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAUtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAE7E;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,QAAoB,CAAC;AAEjE,MAAM,MAAM,0BAA0B,GAAG;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAqBF,mEAAmE;AACnE,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD;AAiCD;;;;;;;;;;;GAWG;AACH,qBAAa,mBAAoB,YAAW,mBAAmB;IAC9D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,QAAQ,CAAiD;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAE3C,YAAY,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B,EAGhE;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAWpF;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAExE;IAED;;;;;;;OAOG;IACG,OAAO,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC5D;YAEa,eAAe;IAO7B,yFAAyF;YAC3E,IAAI;IAOlB,OAAO,CAAC,QAAQ;CAShB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { stat } from "fs/promises";
|
|
2
|
+
import { basename, join } from "path";
|
|
3
|
+
import { appendIndexEntries, loadIndexFile, rewriteIndexFile, SESSION_SUMMARY_INDEX_FILE, toIndexEntry, } from "./session-summary-index-file.js";
|
|
4
|
+
export { SESSION_SUMMARY_INDEX_FILE } from "./session-summary-index-file.js";
|
|
5
|
+
/**
|
|
6
|
+
* Upper bound on live index bytes per sessions directory. It sits well above the
|
|
7
|
+
* in-memory cache's 64 MiB because the index is read once per cold listing, not
|
|
8
|
+
* retained; past it the least recently active sessions are left out and stream
|
|
9
|
+
* like any cache miss.
|
|
10
|
+
*/
|
|
11
|
+
export const SESSION_SUMMARY_INDEX_MAX_BYTES = 256 * 1024 * 1024;
|
|
12
|
+
const snapshots = new Map();
|
|
13
|
+
/** Forget every index snapshot, as a fresh process would start. */
|
|
14
|
+
export function forgetSessionSummaryIndexSnapshots() {
|
|
15
|
+
snapshots.clear();
|
|
16
|
+
}
|
|
17
|
+
function stampsMatch(left, right) {
|
|
18
|
+
return left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
19
|
+
}
|
|
20
|
+
async function statStamp(path) {
|
|
21
|
+
try {
|
|
22
|
+
const stats = await stat(path);
|
|
23
|
+
return { size: stats.size, mtimeMs: stats.mtimeMs };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof Error)
|
|
27
|
+
return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function recency(entry) {
|
|
32
|
+
const headerTime = Date.parse(entry.summary.header.timestamp);
|
|
33
|
+
return entry.summary.lastActivityTime ?? (Number.isNaN(headerTime) ? 0 : headerTime);
|
|
34
|
+
}
|
|
35
|
+
function selectWithinBudget(entries, maxBytes) {
|
|
36
|
+
const kept = [];
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
for (const entry of [...entries].sort((left, right) => recency(right) - recency(left))) {
|
|
39
|
+
if (entry.lineBytes > maxBytes)
|
|
40
|
+
continue;
|
|
41
|
+
if (bytes + entry.lineBytes > maxBytes)
|
|
42
|
+
break;
|
|
43
|
+
kept.push(entry);
|
|
44
|
+
bytes += entry.lineBytes;
|
|
45
|
+
}
|
|
46
|
+
return kept;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Persistent summaries for one sessions directory, stored as
|
|
50
|
+
* `<dir>/.session-summaries.index` (senpi#2087).
|
|
51
|
+
*
|
|
52
|
+
* It is the second cache level behind the in-memory LRU: a cold process lists an
|
|
53
|
+
* unchanged directory by reading this one file instead of re-streaming every
|
|
54
|
+
* session. The file is loaded lazily on the first in-memory miss it can serve,
|
|
55
|
+
* so a warm process does not read it again for a session that just changed.
|
|
56
|
+
* Nothing here throws or logs: an unreadable, corrupt,
|
|
57
|
+
* or unwritable index only means the listing streams, and its rows are
|
|
58
|
+
* identical either way.
|
|
59
|
+
*/
|
|
60
|
+
export class SessionSummaryIndex {
|
|
61
|
+
constructor(dir, options = {}) {
|
|
62
|
+
this.recorded = [];
|
|
63
|
+
this.path = join(dir, SESSION_SUMMARY_INDEX_FILE);
|
|
64
|
+
this.maxBytes = options.maxBytes ?? SESSION_SUMMARY_INDEX_MAX_BYTES;
|
|
65
|
+
}
|
|
66
|
+
async lookup(filePath, stamp) {
|
|
67
|
+
const file = basename(filePath);
|
|
68
|
+
if (!this.loading) {
|
|
69
|
+
this.snapshot ??= this.currentSnapshot();
|
|
70
|
+
const known = await this.snapshot;
|
|
71
|
+
const indexed = known?.entryStamps.get(file);
|
|
72
|
+
if (known && !(indexed && stampsMatch(indexed, stamp)))
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
this.loading ??= this.load();
|
|
76
|
+
const entry = (await this.loading).entries.get(file);
|
|
77
|
+
return entry && stampsMatch(entry.stamp, stamp) ? entry.summary : undefined;
|
|
78
|
+
}
|
|
79
|
+
record(filePath, stamp, summary) {
|
|
80
|
+
this.recorded.push({ file: basename(filePath), stamp, summary });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Bring the index up to date after a listing of `presentFiles`.
|
|
84
|
+
*
|
|
85
|
+
* Summaries the index lacks are appended one line each. The file is rewritten
|
|
86
|
+
* instead (temp file, then rename) when it is unusable, or when its entry
|
|
87
|
+
* bytes exceed twice the live entries' bytes: superseded lines, torn lines,
|
|
88
|
+
* removed sessions, and entries over the byte budget all count as dead.
|
|
89
|
+
*/
|
|
90
|
+
async persist(presentFiles) {
|
|
91
|
+
if (!this.loading)
|
|
92
|
+
return;
|
|
93
|
+
const loaded = await this.loading;
|
|
94
|
+
const entries = new Map(loaded.entries);
|
|
95
|
+
const added = new Set();
|
|
96
|
+
for (const { file, stamp, summary } of this.recorded) {
|
|
97
|
+
const existing = entries.get(file);
|
|
98
|
+
if (existing && stampsMatch(existing.stamp, stamp))
|
|
99
|
+
continue;
|
|
100
|
+
const entry = toIndexEntry(file, stamp, summary);
|
|
101
|
+
if (!entry)
|
|
102
|
+
continue;
|
|
103
|
+
entries.set(file, entry);
|
|
104
|
+
added.add(entry);
|
|
105
|
+
}
|
|
106
|
+
const present = new Set(presentFiles.map((path) => basename(path)));
|
|
107
|
+
const live = [...entries.values()].filter((entry) => present.has(entry.file));
|
|
108
|
+
const kept = selectWithinBudget(live, this.maxBytes);
|
|
109
|
+
const keptBytes = kept.reduce((total, entry) => total + entry.lineBytes, 0);
|
|
110
|
+
const appended = kept.filter((entry) => added.has(entry));
|
|
111
|
+
const appendedBytes = appended.reduce((total, entry) => total + entry.lineBytes, 0);
|
|
112
|
+
try {
|
|
113
|
+
if ((!loaded.usable && kept.length > 0) ||
|
|
114
|
+
(loaded.usable && loaded.entryBytes + appendedBytes > 2 * keptBytes)) {
|
|
115
|
+
await rewriteIndexFile(this.path, kept);
|
|
116
|
+
this.remember(await statStamp(this.path), kept);
|
|
117
|
+
}
|
|
118
|
+
else if (loaded.usable && appended.length > 0) {
|
|
119
|
+
await appendIndexEntries(this.path, appended, loaded.endsWithNewline);
|
|
120
|
+
const inFile = new Map(loaded.entries);
|
|
121
|
+
for (const entry of appended)
|
|
122
|
+
inFile.set(entry.file, entry);
|
|
123
|
+
this.remember(await statStamp(this.path), inFile.values());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof Error))
|
|
128
|
+
throw error;
|
|
129
|
+
snapshots.delete(this.path);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async currentSnapshot() {
|
|
133
|
+
const recorded = snapshots.get(this.path);
|
|
134
|
+
if (!recorded)
|
|
135
|
+
return undefined;
|
|
136
|
+
const current = await statStamp(this.path);
|
|
137
|
+
return current && stampsMatch(current, recorded.indexStamp) ? recorded : undefined;
|
|
138
|
+
}
|
|
139
|
+
/** Stat before reading, so a concurrent writer can only make the snapshot look stale. */
|
|
140
|
+
async load() {
|
|
141
|
+
const before = await statStamp(this.path);
|
|
142
|
+
const loaded = await loadIndexFile(this.path);
|
|
143
|
+
this.remember(before, loaded.usable ? loaded.entries.values() : undefined);
|
|
144
|
+
return loaded;
|
|
145
|
+
}
|
|
146
|
+
remember(indexStamp, entries) {
|
|
147
|
+
if (!indexStamp || !entries) {
|
|
148
|
+
snapshots.delete(this.path);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const entryStamps = new Map();
|
|
152
|
+
for (const entry of entries)
|
|
153
|
+
entryStamps.set(entry.file, entry.stamp);
|
|
154
|
+
snapshots.set(this.path, { indexStamp, entryStamps });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=session-summary-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-summary-index.js","sourceRoot":"","sources":["../../src/core/session-summary-index.ts"],"names":[],"mappings":"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","sourcesContent":["import { stat } from \"fs/promises\";\nimport { basename, join } from \"path\";\nimport type { SessionSummary } from \"./session-summary.ts\";\nimport type { SessionSummaryStore } from \"./session-summary-cache.ts\";\nimport {\n\tappendIndexEntries,\n\ttype IndexEntry,\n\ttype LoadedIndex,\n\tloadIndexFile,\n\trewriteIndexFile,\n\tSESSION_SUMMARY_INDEX_FILE,\n\ttoIndexEntry,\n} from \"./session-summary-index-file.ts\";\nimport type { FileStamp } from \"./session-summary-lru.ts\";\n\nexport { SESSION_SUMMARY_INDEX_FILE } from \"./session-summary-index-file.ts\";\n\n/**\n * Upper bound on live index bytes per sessions directory. It sits well above the\n * in-memory cache's 64 MiB because the index is read once per cold listing, not\n * retained; past it the least recently active sessions are left out and stream\n * like any cache miss.\n */\nexport const SESSION_SUMMARY_INDEX_MAX_BYTES = 256 * 1024 * 1024;\n\nexport type SessionSummaryIndexOptions = {\n\treadonly maxBytes?: number;\n};\n\ntype Recorded = {\n\treadonly file: string;\n\treadonly stamp: FileStamp;\n\treadonly summary: SessionSummary;\n};\n\n/**\n * What this process last saw in one index file: every entry's stamp, trusted\n * only while the index file keeps the size and mtime recorded with it. A warm\n * process uses it to skip loading the index for a file the index cannot serve\n * (changed since it was indexed, or never indexed, like a non-session file).\n */\ntype IndexSnapshot = {\n\treadonly indexStamp: FileStamp;\n\treadonly entryStamps: ReadonlyMap<string, FileStamp>;\n};\n\nconst snapshots = new Map<string, IndexSnapshot>();\n\n/** Forget every index snapshot, as a fresh process would start. */\nexport function forgetSessionSummaryIndexSnapshots(): void {\n\tsnapshots.clear();\n}\n\nfunction stampsMatch(left: FileStamp, right: FileStamp): boolean {\n\treturn left.size === right.size && left.mtimeMs === right.mtimeMs;\n}\n\nasync function statStamp(path: string): Promise<FileStamp | undefined> {\n\ttry {\n\t\tconst stats = await stat(path);\n\t\treturn { size: stats.size, mtimeMs: stats.mtimeMs };\n\t} catch (error) {\n\t\tif (error instanceof Error) return undefined;\n\t\tthrow error;\n\t}\n}\n\nfunction recency(entry: IndexEntry): number {\n\tconst headerTime = Date.parse(entry.summary.header.timestamp);\n\treturn entry.summary.lastActivityTime ?? (Number.isNaN(headerTime) ? 0 : headerTime);\n}\n\nfunction selectWithinBudget(entries: readonly IndexEntry[], maxBytes: number): IndexEntry[] {\n\tconst kept: IndexEntry[] = [];\n\tlet bytes = 0;\n\tfor (const entry of [...entries].sort((left, right) => recency(right) - recency(left))) {\n\t\tif (entry.lineBytes > maxBytes) continue;\n\t\tif (bytes + entry.lineBytes > maxBytes) break;\n\t\tkept.push(entry);\n\t\tbytes += entry.lineBytes;\n\t}\n\treturn kept;\n}\n\n/**\n * Persistent summaries for one sessions directory, stored as\n * `<dir>/.session-summaries.index` (senpi#2087).\n *\n * It is the second cache level behind the in-memory LRU: a cold process lists an\n * unchanged directory by reading this one file instead of re-streaming every\n * session. The file is loaded lazily on the first in-memory miss it can serve,\n * so a warm process does not read it again for a session that just changed.\n * Nothing here throws or logs: an unreadable, corrupt,\n * or unwritable index only means the listing streams, and its rows are\n * identical either way.\n */\nexport class SessionSummaryIndex implements SessionSummaryStore {\n\tprivate readonly path: string;\n\tprivate readonly maxBytes: number;\n\tprivate loading: Promise<LoadedIndex> | undefined;\n\tprivate snapshot: Promise<IndexSnapshot | undefined> | undefined;\n\tprivate readonly recorded: Recorded[] = [];\n\n\tconstructor(dir: string, options: SessionSummaryIndexOptions = {}) {\n\t\tthis.path = join(dir, SESSION_SUMMARY_INDEX_FILE);\n\t\tthis.maxBytes = options.maxBytes ?? SESSION_SUMMARY_INDEX_MAX_BYTES;\n\t}\n\n\tasync lookup(filePath: string, stamp: FileStamp): Promise<SessionSummary | undefined> {\n\t\tconst file = basename(filePath);\n\t\tif (!this.loading) {\n\t\t\tthis.snapshot ??= this.currentSnapshot();\n\t\t\tconst known = await this.snapshot;\n\t\t\tconst indexed = known?.entryStamps.get(file);\n\t\t\tif (known && !(indexed && stampsMatch(indexed, stamp))) return undefined;\n\t\t}\n\t\tthis.loading ??= this.load();\n\t\tconst entry = (await this.loading).entries.get(file);\n\t\treturn entry && stampsMatch(entry.stamp, stamp) ? entry.summary : undefined;\n\t}\n\n\trecord(filePath: string, stamp: FileStamp, summary: SessionSummary): void {\n\t\tthis.recorded.push({ file: basename(filePath), stamp, summary });\n\t}\n\n\t/**\n\t * Bring the index up to date after a listing of `presentFiles`.\n\t *\n\t * Summaries the index lacks are appended one line each. The file is rewritten\n\t * instead (temp file, then rename) when it is unusable, or when its entry\n\t * bytes exceed twice the live entries' bytes: superseded lines, torn lines,\n\t * removed sessions, and entries over the byte budget all count as dead.\n\t */\n\tasync persist(presentFiles: readonly string[]): Promise<void> {\n\t\tif (!this.loading) return;\n\t\tconst loaded = await this.loading;\n\t\tconst entries = new Map(loaded.entries);\n\t\tconst added = new Set<IndexEntry>();\n\t\tfor (const { file, stamp, summary } of this.recorded) {\n\t\t\tconst existing = entries.get(file);\n\t\t\tif (existing && stampsMatch(existing.stamp, stamp)) continue;\n\t\t\tconst entry = toIndexEntry(file, stamp, summary);\n\t\t\tif (!entry) continue;\n\t\t\tentries.set(file, entry);\n\t\t\tadded.add(entry);\n\t\t}\n\n\t\tconst present = new Set(presentFiles.map((path) => basename(path)));\n\t\tconst live = [...entries.values()].filter((entry) => present.has(entry.file));\n\t\tconst kept = selectWithinBudget(live, this.maxBytes);\n\t\tconst keptBytes = kept.reduce((total, entry) => total + entry.lineBytes, 0);\n\t\tconst appended = kept.filter((entry) => added.has(entry));\n\t\tconst appendedBytes = appended.reduce((total, entry) => total + entry.lineBytes, 0);\n\n\t\ttry {\n\t\t\tif (\n\t\t\t\t(!loaded.usable && kept.length > 0) ||\n\t\t\t\t(loaded.usable && loaded.entryBytes + appendedBytes > 2 * keptBytes)\n\t\t\t) {\n\t\t\t\tawait rewriteIndexFile(this.path, kept);\n\t\t\t\tthis.remember(await statStamp(this.path), kept);\n\t\t\t} else if (loaded.usable && appended.length > 0) {\n\t\t\t\tawait appendIndexEntries(this.path, appended, loaded.endsWithNewline);\n\t\t\t\tconst inFile = new Map(loaded.entries);\n\t\t\t\tfor (const entry of appended) inFile.set(entry.file, entry);\n\t\t\t\tthis.remember(await statStamp(this.path), inFile.values());\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error)) throw error;\n\t\t\tsnapshots.delete(this.path);\n\t\t}\n\t}\n\n\tprivate async currentSnapshot(): Promise<IndexSnapshot | undefined> {\n\t\tconst recorded = snapshots.get(this.path);\n\t\tif (!recorded) return undefined;\n\t\tconst current = await statStamp(this.path);\n\t\treturn current && stampsMatch(current, recorded.indexStamp) ? recorded : undefined;\n\t}\n\n\t/** Stat before reading, so a concurrent writer can only make the snapshot look stale. */\n\tprivate async load(): Promise<LoadedIndex> {\n\t\tconst before = await statStamp(this.path);\n\t\tconst loaded = await loadIndexFile(this.path);\n\t\tthis.remember(before, loaded.usable ? loaded.entries.values() : undefined);\n\t\treturn loaded;\n\t}\n\n\tprivate remember(indexStamp: FileStamp | undefined, entries: Iterable<IndexEntry> | undefined): void {\n\t\tif (!indexStamp || !entries) {\n\t\t\tsnapshots.delete(this.path);\n\t\t\treturn;\n\t\t}\n\t\tconst entryStamps = new Map<string, FileStamp>();\n\t\tfor (const entry of entries) entryStamps.set(entry.file, entry.stamp);\n\t\tsnapshots.set(this.path, { indexStamp, entryStamps });\n\t}\n}\n"]}
|
|
@@ -18,10 +18,29 @@ export type SessionSummary = {
|
|
|
18
18
|
/** Full user/assistant transcript text in file order. */
|
|
19
19
|
readonly allMessagesText: string;
|
|
20
20
|
};
|
|
21
|
+
/** What a completed {@link readFileLines} pass saw. */
|
|
22
|
+
export type FileLinesRead = {
|
|
23
|
+
/** False when `onLine` stopped the pass early. */
|
|
24
|
+
readonly completed: boolean;
|
|
25
|
+
/** Bytes read from the file. */
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
/** Whether the last byte read was a newline (a torn tail has none). */
|
|
28
|
+
readonly endsWithNewline: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read a file line by line through one reused 1 MiB buffer, handing each line
|
|
32
|
+
* (LF-split, a trailing CR dropped) to `onLine` until it returns false.
|
|
33
|
+
*
|
|
34
|
+
* This replaces `readline`, whose per-line async iteration dominated summary
|
|
35
|
+
* cost (senpi#2087). Only the unfinished tail of a chunk is carried between
|
|
36
|
+
* reads, and the next newline search resumes where the carried tail ends, so a
|
|
37
|
+
* record larger than the buffer is not rescanned per chunk. Read errors throw.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readFileLines(filePath: string, onLine: (line: string) => boolean): Promise<FileLinesRead>;
|
|
21
40
|
/**
|
|
22
|
-
*
|
|
41
|
+
* Read one session file line by line and fold it into an exact summary.
|
|
23
42
|
*
|
|
24
|
-
* Memory stays bounded by
|
|
43
|
+
* Memory stays bounded by one read buffer plus the transcript text the summary
|
|
25
44
|
* contract requires; no full-file string is ever materialized. A file whose
|
|
26
45
|
* first record is not a session header, or that cannot be read, yields null.
|
|
27
46
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-summary.d.ts","sourceRoot":"","sources":["../../src/core/session-summary.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4DAA4D;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,yDAAyD;IACzD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-summary.d.ts","sourceRoot":"","sources":["../../src/core/session-summary.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4DAA4D;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,yDAAyD;IACzD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC,CAAC;AA8DF,uDAAuD;AACvD,MAAM,MAAM,aAAa,GAAG;IAC3B,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,gCAAgC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAmC/G;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAsBzF"}
|