@remnic/core 9.57.6 → 9.58.1
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/dist/access-admin-ops-surface.d.ts +12 -11
- package/dist/access-admin-ops-surface.js +25 -24
- package/dist/access-authorization-probe.d.ts +12 -11
- package/dist/access-authorization-probe.js +26 -25
- package/dist/access-boundary.d.ts +12 -11
- package/dist/access-boundary.js +25 -24
- package/dist/access-cli.js +77 -76
- package/dist/access-cli.js.map +1 -1
- package/dist/access-coding-context-resolution.d.ts +1 -1
- package/dist/access-extraction-force-flush.d.ts +12 -11
- package/dist/access-extraction-force-flush.js +25 -24
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +12 -11
- package/dist/access-http-lifecycle-flush.d.ts +12 -11
- package/dist/access-http-offline-stream.d.ts +12 -11
- package/dist/access-http-query.js +25 -24
- package/dist/access-http.d.ts +13 -12
- package/dist/access-http.js +36 -35
- package/dist/access-identity-continuity-surface.d.ts +10 -9
- package/dist/access-identity-continuity-surface.js +25 -24
- package/dist/access-lcm-surface.d.ts +12 -11
- package/dist/access-lcm-surface.js +25 -24
- package/dist/access-mcp.d.ts +12 -11
- package/dist/access-mcp.js +32 -31
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-Bpt-SYP0.d.ts → access-namespace-preflight-TBXz-8s_.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +25 -24
- package/dist/access-observe-write-surface.d.ts +12 -11
- package/dist/access-observe-write-surface.js +25 -24
- package/dist/access-offline-manifest.d.ts +10 -9
- package/dist/access-operations-batch.js +27 -26
- package/dist/access-operations.d.ts +15 -14
- package/dist/access-operations.js +31 -30
- package/dist/access-recall-concurrency.d.ts +12 -11
- package/dist/access-recall-concurrency.js +25 -24
- package/dist/access-recall-response.d.ts +12 -11
- package/dist/access-recall-response.js +25 -24
- package/dist/access-recall-surface.d.ts +12 -11
- package/dist/access-recall-surface.js +25 -24
- package/dist/access-schema.d.ts +88 -88
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-4DO8XMq6.d.ts → access-service-BogiG4Dy.d.ts} +71 -71
- package/dist/access-service-helpers.d.ts +12 -11
- package/dist/access-service.d.ts +12 -11
- package/dist/access-service.js +25 -24
- package/dist/access-surface-catalog.d.ts +12 -11
- package/dist/access-surface-catalog.js +27 -26
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +3 -3
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/ambient-provenance.d.ts +1 -1
- package/dist/artifact-search.d.ts +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +10 -9
- package/dist/briefing-window.d.ts +2 -2
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +10 -10
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-surprise.js +4 -3
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/{catalog-BasnO3Bw.d.ts → catalog-z1UerMbh.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +2 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +14 -13
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-P6WBAR27.js → chunk-2GBFKHBZ.js} +9 -9
- package/dist/chunk-2GBFKHBZ.js.map +1 -0
- package/dist/{chunk-YTZA7U6Q.js → chunk-2VHCLHZ5.js} +3 -3
- package/dist/{chunk-NEJQ65NV.js → chunk-4CLE2LXB.js} +2 -2
- package/dist/{chunk-3VFVQDZO.js → chunk-4IIKJOYM.js} +1 -1
- package/dist/{chunk-3VFVQDZO.js.map → chunk-4IIKJOYM.js.map} +1 -1
- package/dist/{chunk-JLNBQWZ2.js → chunk-5JMAGU26.js} +7 -6
- package/dist/{chunk-JLNBQWZ2.js.map → chunk-5JMAGU26.js.map} +1 -1
- package/dist/{chunk-VDX2J7OX.js → chunk-5W7CTZ2E.js} +4 -4
- package/dist/{chunk-CZUHNJHX.js → chunk-6C7Q6NG3.js} +13 -13
- package/dist/{chunk-LMXUWAXW.js → chunk-6TBIWIAG.js} +9 -8
- package/dist/chunk-6TBIWIAG.js.map +1 -0
- package/dist/{chunk-4O6R3GPK.js → chunk-6W2ISYUV.js} +4 -4
- package/dist/{chunk-3AJOJOFP.js → chunk-77DAFTUU.js} +3 -3
- package/dist/{chunk-IKT6ZVO7.js → chunk-7DHFB5SG.js} +3 -3
- package/dist/{chunk-SFR7J2G6.js → chunk-7G34CBKS.js} +2 -2
- package/dist/{chunk-IT57AOMC.js → chunk-AH2NX6S7.js} +3 -3
- package/dist/{chunk-HYQJQGD3.js → chunk-AM4T6CHX.js} +49 -35
- package/dist/chunk-AM4T6CHX.js.map +1 -0
- package/dist/{chunk-Z54EM5YZ.js → chunk-B2QHVLJO.js} +60 -30
- package/dist/chunk-B2QHVLJO.js.map +1 -0
- package/dist/{chunk-FI3PIOOI.js → chunk-B4CSPFYN.js} +8 -8
- package/dist/{chunk-7PJARJS4.js → chunk-BPM6UHUL.js} +2 -2
- package/dist/{chunk-PPUTUOC3.js → chunk-BTHDQOF4.js} +5 -5
- package/dist/{chunk-DUHMYBVE.js → chunk-CZGOLAEW.js} +41 -41
- package/dist/{chunk-5PGYCWA7.js → chunk-DE6YCZEE.js} +2 -2
- package/dist/{chunk-JOA4Q765.js → chunk-EUYWQ7TS.js} +2 -2
- package/dist/{chunk-O5AFC5KM.js → chunk-EWL5DICB.js} +2 -2
- package/dist/{chunk-4YZCIK5E.js → chunk-EZXGJJXM.js} +2 -2
- package/dist/{chunk-GXIY65UH.js → chunk-FI5EAP4N.js} +2 -2
- package/dist/{chunk-YOKSRIFI.js → chunk-FUWS2MNJ.js} +4 -4
- package/dist/{chunk-6RK2X7BR.js → chunk-G3JFFUKR.js} +2 -2
- package/dist/{chunk-VANZKHTN.js → chunk-G5OB4XZX.js} +4 -4
- package/dist/{chunk-TEHPIMXD.js → chunk-HK6KYIF6.js} +2 -2
- package/dist/{chunk-XBGLEOID.js → chunk-I4BLRM7X.js} +9 -9
- package/dist/{chunk-5ZIWRMZM.js → chunk-IVKIODHO.js} +7 -6
- package/dist/chunk-IVKIODHO.js.map +1 -0
- package/dist/{chunk-6SX4MY5Q.js → chunk-J65AKOD6.js} +2 -2
- package/dist/{chunk-BPWZDHFM.js → chunk-J6QIAYNR.js} +2 -2
- package/dist/{chunk-XL3TATMJ.js → chunk-JHMAYABJ.js} +4 -4
- package/dist/{chunk-F7M36OG2.js → chunk-JNWISMYG.js} +10 -10
- package/dist/{chunk-VYIXCMUP.js → chunk-KBIIE57I.js} +3 -3
- package/dist/{chunk-QKPDN325.js → chunk-KF75ANST.js} +6 -3
- package/dist/chunk-KF75ANST.js.map +1 -0
- package/dist/{chunk-24PNX2GF.js → chunk-KXYMU4IN.js} +3 -3
- package/dist/{chunk-FKTD7IVS.js → chunk-L3JVAM5Q.js} +2 -2
- package/dist/{chunk-5QHDXMXP.js → chunk-LLZKOSGM.js} +1 -1
- package/dist/{chunk-5QHDXMXP.js.map → chunk-LLZKOSGM.js.map} +1 -1
- package/dist/{chunk-LIF6DO6U.js → chunk-MG7GI4TT.js} +2 -2
- package/dist/{chunk-RUZM6Z52.js → chunk-MMZK2DWN.js} +1 -1
- package/dist/{chunk-NTJ6TY44.js → chunk-ODAE36PG.js} +3 -3
- package/dist/{chunk-QFN4U7AG.js → chunk-ODZKV2F7.js} +3 -3
- package/dist/{chunk-QFN4U7AG.js.map → chunk-ODZKV2F7.js.map} +1 -1
- package/dist/{chunk-YLHM5BQQ.js → chunk-OLX2HJFW.js} +4 -4
- package/dist/{chunk-DNC4A7RS.js → chunk-OU7RWWW6.js} +2 -2
- package/dist/{chunk-L3LRWDJW.js → chunk-OV36CROW.js} +3 -3
- package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-CSV34HIW.js → chunk-P5OYNV7B.js} +2 -2
- package/dist/{chunk-SNW3YRO6.js → chunk-P6GOYLHN.js} +2 -2
- package/dist/{chunk-REVMY7RA.js → chunk-PFPNUFKZ.js} +109 -59
- package/dist/chunk-PFPNUFKZ.js.map +1 -0
- package/dist/{chunk-USGKHF36.js → chunk-QUJC22CT.js} +2 -2
- package/dist/{chunk-BVRREV3N.js → chunk-R7IV3EUJ.js} +2 -2
- package/dist/{chunk-KSXJOC4F.js → chunk-RDFF2NAU.js} +18 -8
- package/dist/chunk-RDFF2NAU.js.map +1 -0
- package/dist/{chunk-D4LVHPOA.js → chunk-RXN7Z2V5.js} +3 -3
- package/dist/{chunk-7VSHDLJQ.js → chunk-T2VJ2EO5.js} +6 -6
- package/dist/chunk-TOTT6S2V.js +20 -0
- package/dist/chunk-TOTT6S2V.js.map +1 -0
- package/dist/{chunk-U7BMN33M.js → chunk-UBSM6TZP.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-U7XLOSG2.js → chunk-VIM6QH4D.js} +2 -2
- package/dist/{chunk-LJJWFV2Q.js → chunk-VJXSAJFW.js} +2 -2
- package/dist/{chunk-HZC3OECF.js → chunk-VOKK4WCG.js} +2 -2
- package/dist/{chunk-LTAVSOZW.js → chunk-WVP2I6C5.js} +5 -4
- package/dist/chunk-WVP2I6C5.js.map +1 -0
- package/dist/{chunk-LRDP2AC2.js → chunk-YBCZTBN2.js} +2 -2
- package/dist/{chunk-USAUBGBC.js → chunk-YKZFX2HQ.js} +2 -2
- package/dist/{chunk-I2GIYTRU.js → chunk-ZOGOOSB3.js} +2 -2
- package/dist/{chunk-PBGWYM3H.js → chunk-ZPUIRTIZ.js} +27 -15
- package/dist/chunk-ZPUIRTIZ.js.map +1 -0
- package/dist/chunking.js +2 -1
- package/dist/{cli-CQdYilcx.d.ts → cli-CTgFjmDs.d.ts} +5 -5
- package/dist/cli.d.ts +14 -13
- package/dist/cli.js +57 -56
- package/dist/coding/pre-action-gate.d.ts +1 -1
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +10 -10
- package/dist/compounding/preference-consolidator.d.ts +2 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +12 -11
- package/dist/connectors/codex-materialize.d.ts +7 -12
- package/dist/connectors/codex-materialize.js +2 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +12 -11
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/converge-config.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +2 -2
- package/dist/convergence-refresh.js +12 -12
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/corpus-watermark.d.ts +1 -1
- package/dist/corpus-watermark.js +12 -12
- package/dist/day-summary.d.ts +1 -1
- package/dist/day-summary.js +3 -2
- package/dist/delinearize.d.ts +1 -1
- package/dist/dependency-propagation-config.d.ts +1 -1
- package/dist/{dependency-propagation-delivery-C_K_AHcv.d.ts → dependency-propagation-delivery-DdwSc1qh.d.ts} +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +10 -10
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +10 -9
- package/dist/external-wiki-access.d.ts +12 -11
- package/dist/external-wiki-access.js +26 -25
- package/dist/external-wiki-collection-registration.d.ts +1 -1
- package/dist/external-wiki-collection.d.ts +1 -1
- package/dist/external-wiki-mcp-tools.d.ts +12 -11
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-normalization.d.ts +1 -1
- package/dist/extraction-prompt.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +16 -15
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-path-reconstruction.d.ts +1 -1
- package/dist/graph-path-scoring.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +698 -697
- package/dist/index.js +81 -80
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/archive.d.ts +3 -2
- package/dist/lcm/archive.js +2 -1
- package/dist/lcm/engine.d.ts +2 -1
- package/dist/lcm/engine.js +5 -4
- package/dist/lcm/index.d.ts +2 -1
- package/dist/lcm/index.js +7 -6
- package/dist/lcm/recall.d.ts +1 -0
- package/dist/lcm/summarizer.d.ts +1 -0
- package/dist/lcm/summarizer.js +3 -2
- package/dist/lcm/tools.d.ts +2 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +2 -3
- package/dist/local-llm.js +2 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/{maintenance-DdP931i9.d.ts → maintenance-DvkiF7fm.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +12 -11
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-snapshot.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +10 -10
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +15 -15
- package/dist/{orchestrator-DxUWX7gs.d.ts → orchestrator-CXPIzs3D.d.ts} +9 -9
- package/dist/orchestrator.d.ts +10 -9
- package/dist/orchestrator.js +77 -76
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-BKUMU1vU.d.ts → pipeline-BuYWU9rt.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{public-http-Dw0bJhcC.d.ts → public-http-CEwDOGO7.d.ts} +1 -1
- package/dist/{qmd-DJP-GLFe.d.ts → qmd-DYnrPw-m.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +4 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +5 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +4 -3
- package/dist/recall-xray.d.ts +2 -7
- package/dist/recall-xray.js +3 -2
- package/dist/reconcile/cursor.d.ts +1 -1
- package/dist/reconcile/manifest.d.ts +1 -1
- package/dist/reconcile/plan.d.ts +1 -1
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -3
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +138 -138
- package/dist/{scope-profiles-CaKcnB-6.d.ts → scope-profiles-C1QG4Cmv.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +11 -11
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/semantic-chunking.js +3 -2
- package/dist/{semantic-consolidation-P0eMWqh-.d.ts → semantic-consolidation-DMeX8XS0.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +14 -13
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/{service-D5GQLxq_.d.ts → service-CT3ENiTO.d.ts} +2 -2
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/session-observer-state.js +1 -1
- package/dist/shared-context/manager.d.ts +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +12 -11
- package/dist/{storage-DIyil2ll.d.ts → storage-B0L76y7N.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +9 -9
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -2
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/support-passport/index.d.ts +15 -14
- package/dist/support-passport/index.js +28 -27
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/token-estimate.d.ts +10 -0
- package/dist/token-estimate.js +8 -0
- package/dist/token-estimate.js.map +1 -0
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -6
- package/dist/transcript.js +2 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CbXieHKr.d.ts → types-D7pB0QwO.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +12 -2
- package/src/chunking.ts +6 -12
- package/src/config.ts +2 -0
- package/src/connectors/codex-materialize.ts +21 -26
- package/src/lcm/archive.ts +4 -5
- package/src/lcm/engine.ts +55 -34
- package/src/lcm/summarizer.ts +27 -16
- package/src/lcm-engine.test.ts +82 -2
- package/src/local-llm.ts +4 -6
- package/src/memory-extension-host/render-extensions-block.ts +3 -9
- package/src/orchestration/recall-section-coordinator.ts +93 -27
- package/src/recall-token-budget.test.ts +51 -0
- package/src/recall-xray.ts +4 -11
- package/src/semantic-chunking.ts +5 -12
- package/src/session-observer-state.ts +3 -3
- package/src/testing/subjects/recall-budget.test.ts +21 -5
- package/src/token-estimate.test.ts +38 -0
- package/src/token-estimate.ts +22 -0
- package/src/transcript.test.ts +38 -3
- package/src/transcript.ts +58 -59
- package/src/types.ts +1 -1
- package/dist/chunk-5ZIWRMZM.js.map +0 -1
- package/dist/chunk-HYQJQGD3.js.map +0 -1
- package/dist/chunk-KSXJOC4F.js.map +0 -1
- package/dist/chunk-LMXUWAXW.js.map +0 -1
- package/dist/chunk-LTAVSOZW.js.map +0 -1
- package/dist/chunk-P6WBAR27.js.map +0 -1
- package/dist/chunk-PBGWYM3H.js.map +0 -1
- package/dist/chunk-QKPDN325.js.map +0 -1
- package/dist/chunk-REVMY7RA.js.map +0 -1
- package/dist/chunk-Z54EM5YZ.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-YTZA7U6Q.js.map → chunk-2VHCLHZ5.js.map} +0 -0
- /package/dist/{chunk-NEJQ65NV.js.map → chunk-4CLE2LXB.js.map} +0 -0
- /package/dist/{chunk-VDX2J7OX.js.map → chunk-5W7CTZ2E.js.map} +0 -0
- /package/dist/{chunk-CZUHNJHX.js.map → chunk-6C7Q6NG3.js.map} +0 -0
- /package/dist/{chunk-4O6R3GPK.js.map → chunk-6W2ISYUV.js.map} +0 -0
- /package/dist/{chunk-3AJOJOFP.js.map → chunk-77DAFTUU.js.map} +0 -0
- /package/dist/{chunk-IKT6ZVO7.js.map → chunk-7DHFB5SG.js.map} +0 -0
- /package/dist/{chunk-SFR7J2G6.js.map → chunk-7G34CBKS.js.map} +0 -0
- /package/dist/{chunk-IT57AOMC.js.map → chunk-AH2NX6S7.js.map} +0 -0
- /package/dist/{chunk-FI3PIOOI.js.map → chunk-B4CSPFYN.js.map} +0 -0
- /package/dist/{chunk-7PJARJS4.js.map → chunk-BPM6UHUL.js.map} +0 -0
- /package/dist/{chunk-PPUTUOC3.js.map → chunk-BTHDQOF4.js.map} +0 -0
- /package/dist/{chunk-DUHMYBVE.js.map → chunk-CZGOLAEW.js.map} +0 -0
- /package/dist/{chunk-5PGYCWA7.js.map → chunk-DE6YCZEE.js.map} +0 -0
- /package/dist/{chunk-JOA4Q765.js.map → chunk-EUYWQ7TS.js.map} +0 -0
- /package/dist/{chunk-O5AFC5KM.js.map → chunk-EWL5DICB.js.map} +0 -0
- /package/dist/{chunk-4YZCIK5E.js.map → chunk-EZXGJJXM.js.map} +0 -0
- /package/dist/{chunk-GXIY65UH.js.map → chunk-FI5EAP4N.js.map} +0 -0
- /package/dist/{chunk-YOKSRIFI.js.map → chunk-FUWS2MNJ.js.map} +0 -0
- /package/dist/{chunk-6RK2X7BR.js.map → chunk-G3JFFUKR.js.map} +0 -0
- /package/dist/{chunk-VANZKHTN.js.map → chunk-G5OB4XZX.js.map} +0 -0
- /package/dist/{chunk-TEHPIMXD.js.map → chunk-HK6KYIF6.js.map} +0 -0
- /package/dist/{chunk-XBGLEOID.js.map → chunk-I4BLRM7X.js.map} +0 -0
- /package/dist/{chunk-6SX4MY5Q.js.map → chunk-J65AKOD6.js.map} +0 -0
- /package/dist/{chunk-BPWZDHFM.js.map → chunk-J6QIAYNR.js.map} +0 -0
- /package/dist/{chunk-XL3TATMJ.js.map → chunk-JHMAYABJ.js.map} +0 -0
- /package/dist/{chunk-F7M36OG2.js.map → chunk-JNWISMYG.js.map} +0 -0
- /package/dist/{chunk-VYIXCMUP.js.map → chunk-KBIIE57I.js.map} +0 -0
- /package/dist/{chunk-24PNX2GF.js.map → chunk-KXYMU4IN.js.map} +0 -0
- /package/dist/{chunk-FKTD7IVS.js.map → chunk-L3JVAM5Q.js.map} +0 -0
- /package/dist/{chunk-LIF6DO6U.js.map → chunk-MG7GI4TT.js.map} +0 -0
- /package/dist/{chunk-RUZM6Z52.js.map → chunk-MMZK2DWN.js.map} +0 -0
- /package/dist/{chunk-NTJ6TY44.js.map → chunk-ODAE36PG.js.map} +0 -0
- /package/dist/{chunk-YLHM5BQQ.js.map → chunk-OLX2HJFW.js.map} +0 -0
- /package/dist/{chunk-DNC4A7RS.js.map → chunk-OU7RWWW6.js.map} +0 -0
- /package/dist/{chunk-L3LRWDJW.js.map → chunk-OV36CROW.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-CSV34HIW.js.map → chunk-P5OYNV7B.js.map} +0 -0
- /package/dist/{chunk-SNW3YRO6.js.map → chunk-P6GOYLHN.js.map} +0 -0
- /package/dist/{chunk-USGKHF36.js.map → chunk-QUJC22CT.js.map} +0 -0
- /package/dist/{chunk-BVRREV3N.js.map → chunk-R7IV3EUJ.js.map} +0 -0
- /package/dist/{chunk-D4LVHPOA.js.map → chunk-RXN7Z2V5.js.map} +0 -0
- /package/dist/{chunk-7VSHDLJQ.js.map → chunk-T2VJ2EO5.js.map} +0 -0
- /package/dist/{chunk-U7BMN33M.js.map → chunk-UBSM6TZP.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-U7XLOSG2.js.map → chunk-VIM6QH4D.js.map} +0 -0
- /package/dist/{chunk-LJJWFV2Q.js.map → chunk-VJXSAJFW.js.map} +0 -0
- /package/dist/{chunk-HZC3OECF.js.map → chunk-VOKK4WCG.js.map} +0 -0
- /package/dist/{chunk-LRDP2AC2.js.map → chunk-YBCZTBN2.js.map} +0 -0
- /package/dist/{chunk-USAUBGBC.js.map → chunk-YKZFX2HQ.js.map} +0 -0
- /package/dist/{chunk-I2GIYTRU.js.map → chunk-ZOGOOSB3.js.map} +0 -0
package/src/transcript.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { TranscriptEntry, Checkpoint, PluginConfig } from "./types.js";
|
|
|
5
5
|
import { analyzeSessionIntegrity, type SessionIntegrityReport } from "./session-integrity.js";
|
|
6
6
|
import { resolveSafeStoragePath } from "./storage-paths.js";
|
|
7
7
|
import { sessionStoragePaths } from "./session-identity.js";
|
|
8
|
+
import { estimateTokenCount } from "./token-estimate.js";
|
|
8
9
|
|
|
9
10
|
type DirectorySessionStatus = "missing" | "empty" | "matches" | "occupied";
|
|
10
11
|
type DirectoryOwnershipCacheEntry = {
|
|
@@ -76,14 +77,18 @@ export class TranscriptManager {
|
|
|
76
77
|
private config: PluginConfig;
|
|
77
78
|
private sessionFootprintCache = new Map<
|
|
78
79
|
string,
|
|
79
|
-
{
|
|
80
|
+
{
|
|
81
|
+
totalBytes: number;
|
|
82
|
+
totalTokens: number;
|
|
83
|
+
fileBytes: Map<string, number>;
|
|
84
|
+
fileSizes: Map<string, number>;
|
|
85
|
+
fileTokens: Map<string, number>;
|
|
86
|
+
}
|
|
80
87
|
>();
|
|
81
88
|
private directoryOwnershipCache = new Map<string, DirectoryOwnershipCacheEntry>();
|
|
82
89
|
|
|
83
90
|
/** Default checkpoint TTL in hours */
|
|
84
91
|
private static readonly DEFAULT_CHECKPOINT_TTL_HOURS = 24;
|
|
85
|
-
/** Approximate characters per token for rough estimation */
|
|
86
|
-
private static readonly CHARS_PER_TOKEN = 4;
|
|
87
92
|
|
|
88
93
|
constructor(config: PluginConfig) {
|
|
89
94
|
this.config = config;
|
|
@@ -456,14 +461,7 @@ export class TranscriptManager {
|
|
|
456
461
|
async estimateSessionFootprint(sessionKey: string): Promise<{ bytes: number; tokens: number }> {
|
|
457
462
|
const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);
|
|
458
463
|
let bytes = 0;
|
|
459
|
-
|
|
460
|
-
// NOTE: this is a best-effort byte ESTIMATE for compaction sizing, not an
|
|
461
|
-
// exact row count, and is maintained via an incremental per-file cache. A
|
|
462
|
-
// copied-but-not-trimmed migration window (issue #1496) can transiently
|
|
463
|
-
// over-estimate by counting a duplicated row in both the primary and a
|
|
464
|
-
// read-back dir; that self-heals once the migration trims the source. The
|
|
465
|
-
// exact-once guarantee that matters for callers lives in readRecent /
|
|
466
|
-
// readToolUse / the summarizer fetch, which dedup by raw line.
|
|
464
|
+
let tokens = 0;
|
|
467
465
|
try {
|
|
468
466
|
const files = await this.getSessionStorageFiles(
|
|
469
467
|
this.transcriptsDir,
|
|
@@ -476,106 +474,108 @@ export class TranscriptManager {
|
|
|
476
474
|
if (!cached) {
|
|
477
475
|
const fileBytes = new Map<string, number>();
|
|
478
476
|
const fileSizes = new Map<string, number>();
|
|
477
|
+
const fileTokens = new Map<string, number>();
|
|
479
478
|
for (const file of files) {
|
|
480
479
|
try {
|
|
481
480
|
const fileInfo = await stat(file.path);
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
);
|
|
486
|
-
fileBytes.set(file.cacheKey, sessionBytes);
|
|
481
|
+
const estimate = await this.estimateSessionBytesInFile(file.path, sessionKey);
|
|
482
|
+
fileBytes.set(file.cacheKey, estimate.bytes);
|
|
483
|
+
fileTokens.set(file.cacheKey, estimate.tokens);
|
|
487
484
|
fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));
|
|
488
|
-
bytes +=
|
|
485
|
+
bytes += estimate.bytes;
|
|
486
|
+
tokens += estimate.tokens;
|
|
489
487
|
} catch {
|
|
490
|
-
// fail-open
|
|
491
488
|
}
|
|
492
489
|
}
|
|
493
|
-
this.sessionFootprintCache.set(sessionKey, { totalBytes: bytes, fileBytes, fileSizes });
|
|
490
|
+
this.sessionFootprintCache.set(sessionKey, { totalBytes: bytes, totalTokens: tokens, fileBytes, fileSizes, fileTokens });
|
|
494
491
|
} else {
|
|
495
492
|
bytes = cached.totalBytes;
|
|
493
|
+
tokens = cached.totalTokens;
|
|
496
494
|
const seen = new Set(files.map((file) => file.cacheKey));
|
|
497
495
|
|
|
498
|
-
// Drop removed files from the cached total.
|
|
499
496
|
for (const [cachedFile, cachedSessionBytes] of cached.fileBytes.entries()) {
|
|
500
497
|
if (!seen.has(cachedFile)) {
|
|
501
498
|
bytes -= cachedSessionBytes;
|
|
499
|
+
tokens -= cached.fileTokens.get(cachedFile) ?? 0;
|
|
502
500
|
cached.fileBytes.delete(cachedFile);
|
|
501
|
+
cached.fileTokens.delete(cachedFile);
|
|
503
502
|
cached.fileSizes.delete(cachedFile);
|
|
504
503
|
}
|
|
505
504
|
}
|
|
506
505
|
|
|
507
|
-
// Read only newly discovered files.
|
|
508
506
|
for (const file of files) {
|
|
509
507
|
if (cached.fileBytes.has(file.cacheKey)) continue;
|
|
510
508
|
try {
|
|
511
509
|
const fileInfo = await stat(file.path);
|
|
512
|
-
const
|
|
513
|
-
cached.fileBytes.set(file.cacheKey,
|
|
510
|
+
const estimate = await this.estimateSessionBytesInFile(file.path, sessionKey);
|
|
511
|
+
cached.fileBytes.set(file.cacheKey, estimate.bytes);
|
|
512
|
+
cached.fileTokens.set(file.cacheKey, estimate.tokens);
|
|
514
513
|
cached.fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));
|
|
515
|
-
bytes +=
|
|
514
|
+
bytes += estimate.bytes;
|
|
515
|
+
tokens += estimate.tokens;
|
|
516
516
|
} catch {
|
|
517
|
-
// fail-open
|
|
518
517
|
}
|
|
519
518
|
}
|
|
520
519
|
|
|
521
|
-
// Recompute any shard whose file size changed. A session can have both
|
|
522
|
-
// encoded and legacy directories during migration, so path ordering does
|
|
523
|
-
// not reliably identify the file that can grow.
|
|
524
520
|
for (const file of files) {
|
|
525
521
|
try {
|
|
526
522
|
const fileInfo = await stat(file.path);
|
|
527
523
|
const size = Math.max(0, fileInfo.size);
|
|
528
|
-
const
|
|
524
|
+
const previousEstimate = {
|
|
525
|
+
bytes: cached.fileBytes.get(file.cacheKey) ?? 0,
|
|
526
|
+
tokens: cached.fileTokens.get(file.cacheKey) ?? 0,
|
|
527
|
+
};
|
|
529
528
|
const previousSize = cached.fileSizes.get(file.cacheKey) ?? -1;
|
|
530
529
|
if (size !== previousSize) {
|
|
531
|
-
const
|
|
532
|
-
cached.fileBytes.set(file.cacheKey,
|
|
530
|
+
const estimate = await this.estimateSessionBytesInFile(file.path, sessionKey);
|
|
531
|
+
cached.fileBytes.set(file.cacheKey, estimate.bytes);
|
|
532
|
+
cached.fileTokens.set(file.cacheKey, estimate.tokens);
|
|
533
533
|
cached.fileSizes.set(file.cacheKey, size);
|
|
534
|
-
bytes +=
|
|
534
|
+
bytes += estimate.bytes - previousEstimate.bytes;
|
|
535
|
+
tokens += estimate.tokens - previousEstimate.tokens;
|
|
535
536
|
}
|
|
536
537
|
} catch {
|
|
537
|
-
// fail-open
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
|
|
541
|
+
bytes = Math.max(0, bytes);
|
|
542
|
+
tokens = Math.max(0, tokens);
|
|
542
543
|
cached.totalBytes = bytes;
|
|
544
|
+
cached.totalTokens = tokens;
|
|
543
545
|
}
|
|
544
546
|
} catch {
|
|
545
|
-
// fail-open
|
|
546
547
|
this.sessionFootprintCache.delete(sessionKey);
|
|
547
548
|
}
|
|
548
549
|
|
|
549
|
-
return {
|
|
550
|
-
bytes,
|
|
551
|
-
tokens: Math.floor(bytes / TranscriptManager.CHARS_PER_TOKEN),
|
|
552
|
-
};
|
|
550
|
+
return { bytes, tokens };
|
|
553
551
|
}
|
|
554
552
|
|
|
555
|
-
private async estimateSessionBytesInFile(
|
|
553
|
+
private async estimateSessionBytesInFile(
|
|
554
|
+
filePath: string,
|
|
555
|
+
sessionKey: string,
|
|
556
|
+
): Promise<{ bytes: number; tokens: number }> {
|
|
556
557
|
try {
|
|
557
558
|
const raw = await readFile(filePath, "utf-8");
|
|
558
|
-
let
|
|
559
|
+
let bytes = 0;
|
|
560
|
+
let tokens = 0;
|
|
559
561
|
for (const line of raw.split("\n")) {
|
|
560
562
|
if (!line.trim()) continue;
|
|
561
563
|
try {
|
|
562
|
-
const parsed = JSON.parse(line) as { sessionKey?: string };
|
|
564
|
+
const parsed = JSON.parse(line) as { sessionKey?: string; content?: unknown };
|
|
563
565
|
if (parsed.sessionKey === sessionKey) {
|
|
564
|
-
|
|
566
|
+
bytes += Buffer.byteLength(`${line}\n`, "utf-8");
|
|
567
|
+
if (typeof parsed.content === "string") tokens += estimateTokenCount(parsed.content);
|
|
565
568
|
}
|
|
566
569
|
} catch {
|
|
567
570
|
// fail-open for malformed lines
|
|
568
571
|
}
|
|
569
572
|
}
|
|
570
|
-
return
|
|
573
|
+
return { bytes, tokens };
|
|
571
574
|
} catch {
|
|
572
|
-
return 0;
|
|
575
|
+
return { bytes: 0, tokens: 0 };
|
|
573
576
|
}
|
|
574
577
|
}
|
|
575
578
|
|
|
576
|
-
/**
|
|
577
|
-
* Check if a file is a legacy flat transcript file (YYYY-MM-DD.jsonl format).
|
|
578
|
-
*/
|
|
579
579
|
private isLegacyTranscriptFile(filename: string): boolean {
|
|
580
580
|
return /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(filename);
|
|
581
581
|
}
|
|
@@ -1079,9 +1079,9 @@ export class TranscriptManager {
|
|
|
1079
1079
|
return "";
|
|
1080
1080
|
}
|
|
1081
1081
|
|
|
1082
|
-
const
|
|
1082
|
+
const maxTokenBudget = Math.max(0, Math.floor(maxTokens));
|
|
1083
|
+
if (maxTokenBudget === 0) return "";
|
|
1083
1084
|
const lines: string[] = [];
|
|
1084
|
-
|
|
1085
1085
|
// Calculate time range for header
|
|
1086
1086
|
const firstEntry = new Date(entries[0].timestamp);
|
|
1087
1087
|
const lastEntry = new Date(entries[entries.length - 1].timestamp);
|
|
@@ -1094,6 +1094,9 @@ export class TranscriptManager {
|
|
|
1094
1094
|
lines.push(`## Recent Conversation (last ${hoursDiff} hour${hoursDiff === 1 ? "" : "s"})`);
|
|
1095
1095
|
}
|
|
1096
1096
|
lines.push("");
|
|
1097
|
+
if (estimateTokenCount(lines.join("\n")) > maxTokenBudget) {
|
|
1098
|
+
return "";
|
|
1099
|
+
}
|
|
1097
1100
|
|
|
1098
1101
|
// Format each entry
|
|
1099
1102
|
const formattedEntries: string[] = [];
|
|
@@ -1108,23 +1111,19 @@ export class TranscriptManager {
|
|
|
1108
1111
|
formattedEntries.push(`[${timeStr}] ${roleLabel}: ${entry.content}`);
|
|
1109
1112
|
}
|
|
1110
1113
|
|
|
1111
|
-
// Build output, trimming from the beginning if too long
|
|
1112
|
-
// (we want to keep the most recent context)
|
|
1113
|
-
let totalChars = lines.join("\n").length;
|
|
1114
1114
|
const selectedEntries: string[] = [];
|
|
1115
1115
|
|
|
1116
1116
|
for (let i = formattedEntries.length - 1; i >= 0; i--) {
|
|
1117
1117
|
const entry = formattedEntries[i];
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
break;
|
|
1118
|
+
const candidate = [...lines, entry, ...selectedEntries, ""].join("\n");
|
|
1119
|
+
if (estimateTokenCount(candidate) > maxTokenBudget) {
|
|
1120
|
+
if (selectedEntries.length > 0) break;
|
|
1121
|
+
return "";
|
|
1123
1122
|
}
|
|
1124
1123
|
|
|
1125
1124
|
selectedEntries.unshift(entry);
|
|
1126
|
-
totalChars += entryChars;
|
|
1127
1125
|
}
|
|
1126
|
+
if (selectedEntries.length === 0) return "";
|
|
1128
1127
|
|
|
1129
1128
|
lines.push(...selectedEntries);
|
|
1130
1129
|
lines.push(""); // Trailing newline
|
package/src/types.ts
CHANGED
|
@@ -2269,7 +2269,7 @@ export interface PluginConfig extends BoundedJsonlStateConfig, SecurityConfig {
|
|
|
2269
2269
|
codexMaterializeMemories: boolean;
|
|
2270
2270
|
/** Namespace to materialize; "auto" derives from the connector context. Default "auto". */
|
|
2271
2271
|
codexMaterializeNamespace: string;
|
|
2272
|
-
/** Max
|
|
2272
|
+
/** Max estimated-token size of memory_summary.md. Default 4500. */
|
|
2273
2273
|
codexMaterializeMaxSummaryTokens: number;
|
|
2274
2274
|
/** Max age in days for rollout_summaries/*.md before pruning. Default 30. */
|
|
2275
2275
|
codexMaterializeRolloutRetentionDays: number;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/local-llm.ts"],"sourcesContent":["import { log } from \"./logger.js\";\nimport type { PluginConfig } from \"./types.js\";\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport type { ModelRegistry } from \"./model-registry.js\";\nimport { launchProcessSync } from \"./runtime/child-process.js\";\nimport { mergeEnv, readEnvVar } from \"./runtime/env.js\";\nimport { resolveLocalLlmCapabilities } from \"./capabilities.js\";\nimport { resolvePipelineProcessingCapabilities } from \"./capabilities.js\";\nimport { ChatTransport } from \"./local-llm-transport.js\";\nimport {\n extractNonRecoverableBackendReason,\n extractNonRecoverableBackendReasonFromErrorText,\n isAbortError,\n probeFetch,\n resolveUnavailableVerdict,\n type ProbeFetchResult,\n normalizeBackendTripReason,\n waitForRetryBackoff,\n SingleFlightProbe,\n} from \"./local-llm-helpers.js\";\n\n/** Trim trailing slash characters without backtracking regex. */\nfunction trimTrailingSlashes(s: string): string {\n let end = s.length;\n while (end > 0 && s[end - 1] === \"/\") end--;\n return s.substring(0, end);\n}\n\nfunction stripTrailingV1Path(s: string): string {\n return s.endsWith(\"/v1\") ? s.slice(0, -3) : s;\n}\n\nfunction explicitPortFromUrl(s: string): number | null {\n try {\n const parsed = new URL(s);\n if (!parsed.port) return null;\n const port = Number(parsed.port);\n return Number.isInteger(port) ? port : null;\n } catch {\n return null;\n }\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isLlamaCppPropsResponse(value: unknown): boolean {\n return (\n isObjectRecord(value) &&\n isObjectRecord(value.default_generation_settings) &&\n typeof value.total_slots === \"number\" &&\n (\n typeof value.model_path === \"string\" ||\n typeof value.chat_template === \"string\" ||\n typeof value.build_info === \"string\"\n )\n );\n}\n\nfunction isLlamaCppModelsResponse(value: unknown): boolean {\n if (!isObjectRecord(value) || !Array.isArray(value.data)) {\n return false;\n }\n return value.data.some((entry) => {\n if (!isObjectRecord(entry)) return false;\n if (entry.owned_by === \"llamacpp\") return true;\n if (typeof entry.id === \"string\" && entry.id.endsWith(\".gguf\")) return true;\n const meta = entry.meta;\n return (\n isObjectRecord(meta) &&\n (\"n_ctx_train\" in meta || \"n_params\" in meta || \"vocab_type\" in meta)\n );\n });\n}\n\nfunction isLmStudioApiV1ModelsResponse(value: unknown): boolean {\n if (!isObjectRecord(value) || !Array.isArray(value.models)) {\n return false;\n }\n return value.models.some((entry) => {\n if (!isObjectRecord(entry)) return false;\n return (\n typeof entry.key === \"string\" &&\n typeof entry.display_name === \"string\" &&\n (\n typeof entry.format === \"string\" ||\n typeof entry.max_context_length === \"number\" ||\n Array.isArray(entry.loaded_instances)\n )\n );\n });\n}\n\nfunction isLmStudioApiV0ModelsResponse(value: unknown): boolean {\n if (!isObjectRecord(value) || !Array.isArray(value.data)) {\n return false;\n }\n return value.data.some((entry) => {\n if (!isObjectRecord(entry)) return false;\n return (\n typeof entry.id === \"string\" &&\n typeof entry.publisher === \"string\" &&\n (\n typeof entry.compatibility_type === \"string\" ||\n typeof entry.max_context_length === \"number\" ||\n typeof entry.state === \"string\"\n )\n );\n });\n}\n\nfunction isLmStudioNativeModelsResponse(value: unknown): boolean {\n return isLmStudioApiV1ModelsResponse(value) || isLmStudioApiV0ModelsResponse(value);\n}\n\n/**\n * Local LLM client for OpenAI-compatible endpoints (LM Studio, Ollama, MLX, etc.)\n *\n * Based on openclaw-tactician's provider detection patterns for consistency.\n * Provides privacy-preserving, cost-effective LLM operations with\n * graceful fallback to cloud providers when local LLM is unavailable.\n */\nexport type LocalLlmType = \"lmstudio\" | \"ollama\" | \"mlx\" | \"vllm\" | \"llamacpp\" | \"generic\";\n\n/**\n * Backends known to honor `chat_template_kwargs: { enable_thinking: false }`\n * on OpenAI-compatible `/v1/chat/completions`. LM Studio, vLLM, and\n * llama.cpp forward this field to the jinja chat template, where thinking-capable\n * models (Qwen 3.5, Gemma 4, DeepSeek) suppress reasoning tokens.\n *\n * Strict OpenAI-compatible backends (standard OpenAI, Azure OpenAI, some\n * proxies) reject unknown request fields with 400 — which trips the\n * `localLlm400*` cooldown path. `LocalLlmClient` therefore only injects\n * the kwarg when the detected backend is in this set; unknown / `generic`\n * / `ollama` / `mlx` fail open (no injection, no 400 risk). Issue #548.\n */\nconst THINKING_COMPATIBLE_BACKENDS: ReadonlySet<LocalLlmType> = new Set([\n \"lmstudio\",\n \"vllm\",\n \"llamacpp\",\n]);\n\nconst THINKING_SUPPRESSED_OPERATIONS: ReadonlySet<string> = new Set([\n \"extraction\",\n \"extraction-judge\",\n \"extraction-faithfulness\",\n \"contradiction-judge\",\n \"contradiction_verification\",\n \"link_suggestion\",\n \"memory_summarization\",\n \"proactive_extraction\",\n \"lcm-summarize\",\n \"day_summary\",\n \"hourly_summary\",\n \"hourly_summary_extended\",\n]);\n\ninterface LocalServerConfig {\n type: LocalLlmType;\n defaultPort: number;\n healthEndpoint: string;\n modelsEndpoint: string;\n detectFn: (response: unknown) => boolean;\n}\n\nconst LOCAL_SERVERS: LocalServerConfig[] = [\n {\n type: \"ollama\",\n defaultPort: 11434,\n healthEndpoint: \"/\",\n modelsEndpoint: \"/api/tags\",\n detectFn: (resp) => typeof resp === \"string\" && resp.includes(\"Ollama\"),\n },\n {\n type: \"llamacpp\",\n defaultPort: 8080,\n healthEndpoint: \"/health\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => isObjectRecord(resp) && resp.status === \"ok\",\n },\n {\n type: \"mlx\",\n defaultPort: 8080,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => isObjectRecord(resp) && Array.isArray(resp.data),\n },\n {\n type: \"lmstudio\",\n defaultPort: 1234,\n healthEndpoint: \"/v1/models\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => isObjectRecord(resp) && Array.isArray(resp.data),\n },\n {\n type: \"vllm\",\n defaultPort: 8000,\n healthEndpoint: \"/health\",\n modelsEndpoint: \"/v1/models\",\n detectFn: (resp) => resp === \"\" || (isObjectRecord(resp) && !(\"status\" in resp)),\n },\n];\n\nfunction orderedLocalServers(configuredBaseUrl: string): LocalServerConfig[] {\n const configuredPort = explicitPortFromUrl(configuredBaseUrl);\n if (configuredPort === null) return LOCAL_SERVERS;\n const matching = LOCAL_SERVERS.filter(\n (serverConfig) => serverConfig.defaultPort === configuredPort,\n );\n if (matching.length === 0) return LOCAL_SERVERS;\n const matchingTypes = new Set(matching.map((serverConfig) => serverConfig.type));\n return [\n ...matching,\n ...LOCAL_SERVERS.filter((serverConfig) => !matchingTypes.has(serverConfig.type)),\n ];\n}\n\nexport interface LocalModelInfo {\n id: string;\n contextWindow?: number;\n maxTokens?: number;\n}\n\nexport type LocalLlmRequestPriority = \"recall-critical\" | \"background\";\n\ninterface LocalLlmChatCompletionOptions {\n temperature?: number;\n maxTokens?: number;\n responseFormat?: { type: string };\n timeoutMs?: number;\n operation?: string;\n forceDisableThinking?: boolean;\n disableThinking?: boolean;\n priority?: LocalLlmRequestPriority;\n signal?: AbortSignal;\n redactProviderErrors?: boolean;\n}\ninterface LocalLlmQueuedRequest {\n messages: Array<{ role: string; content: string }>;\n options: LocalLlmChatCompletionOptions;\n priority: LocalLlmRequestPriority;\n enqueuedAtMs: number;\n resolve: (value: LocalLlmChatCompletionResult | null) => void;\n}\n\ninterface LocalLlmChatCompletionResult {\n content: string;\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n}\n\nconst LOCAL_LLM_GLOBAL_BACKEND_STATE = \"__openclawEngramLocalLlmBackendState\";\n\ntype LocalLlmBackendState = {\n untilMs: number;\n reason: string;\n};\nexport class LocalLlmClient {\n private config: PluginConfig;\n private isAvailable: boolean | null = null;\n private lastHealthCheck: number = 0;\n private readonly availabilityProbe = new SingleFlightProbe();\n private detectedType: LocalLlmType | null = null;\n private cachedModelInfo: LocalModelInfo | null = null;\n private cachedLmsContext: number | null = null;\n private lastLmsCheck: number = 0;\n private consecutive400s: number = 0;\n private cooldownUntilMs: number = 0;\n private modelRegistry?: ModelRegistry;\n private _disableThinking: boolean = false;\n private readonly requestQueues: Record<LocalLlmRequestPriority, LocalLlmQueuedRequest[]> = {\n \"recall-critical\": [],\n background: [],\n };\n private readonly queueProcessing = new Set<LocalLlmRequestPriority>();\n private queueDrainScheduled: boolean = false;\n private static readonly HEALTH_CHECK_INTERVAL_MS = 60000; // 1 minute\n private static readonly LMS_CACHE_INTERVAL_MS = 30000; // 30 seconds\n private readonly chatTransport = new ChatTransport();\n\n constructor(config: PluginConfig, modelRegistry?: ModelRegistry) {\n this.config = config;\n this.modelRegistry = modelRegistry;\n }\n\n /**\n * Request thinking/reasoning suppression on the next chat completion.\n *\n * When `true`, the client will inject\n * `chat_template_kwargs: { enable_thinking: false }` into the request\n * body — **but only when the detected backend is known to support it**\n * (LM Studio, vLLM; see `THINKING_COMPATIBLE_BACKENDS`). Strict\n * OpenAI-compat backends reject unknown fields with 400; on those the\n * client fails open (thinking runs normally). This is the safe\n * default for Remnic extraction-style operations: measurable latency\n * win on thinking-capable backends, zero risk on others. Consolidation\n * operations keep thinking enabled unless explicitly added to\n * `THINKING_SUPPRESSED_OPERATIONS`. Issues #548 and #979.\n */\n set disableThinking(value: boolean) {\n this._disableThinking = value;\n }\n\n private resolveHomeDir(): string {\n return this.config.localLlmHomeDir || readEnvVar(\"HOME\") || os.homedir();\n }\n\n private buildRequestHeaders(base: Record<string, string> = {}): Record<string, string> {\n const headers: Record<string, string> = {\n ...base,\n ...(this.config.localLlmHeaders ?? {}),\n };\n if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {\n headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;\n }\n return headers;\n }\n\n\n /**\n * Set the ModelRegistry for caching detected capabilities\n */\n setModelRegistry(registry: ModelRegistry): void {\n this.modelRegistry = registry;\n }\n\n /**\n * Get the detected server type (null if not detected)\n */\n getDetectedType(): LocalLlmType | null {\n return this.detectedType;\n }\n\n private getBackendKey(): string {\n return trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n ).replace(/\\/v1$/, \"\");\n }\n\n private getGlobalBackendState(): Map<string, LocalLlmBackendState> {\n const globalAny = globalThis as typeof globalThis & {\n [LOCAL_LLM_GLOBAL_BACKEND_STATE]?: Map<string, LocalLlmBackendState>;\n };\n if (!globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE]) {\n globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE] = new Map();\n }\n return globalAny[LOCAL_LLM_GLOBAL_BACKEND_STATE];\n }\n\n private getTrippedBackendState(now: number): LocalLlmBackendState | null {\n const state = this.getGlobalBackendState().get(this.getBackendKey()) ?? null;\n if (!state) return null;\n if (state.untilMs <= now) {\n this.getGlobalBackendState().delete(this.getBackendKey());\n this.lastHealthCheck = 0;\n return null;\n }\n return state;\n }\n\n private markBackendUnavailable(reason: string, durationMs: number): void {\n const normalizedReason = normalizeBackendTripReason(reason);\n if (durationMs > 0) {\n const untilMs = Date.now() + durationMs;\n this.getGlobalBackendState().set(this.getBackendKey(), { untilMs, reason: normalizedReason });\n } else {\n this.getGlobalBackendState().delete(this.getBackendKey());\n }\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.warn(\n `local LLM backend unavailable for ${durationMs}ms: model=${this.config.localLlmModel} reason=${normalizedReason}`,\n );\n }\n\n\n /**\n * Fetch with timeout for health checks. Body lives in the sibling helper so\n * this file stays under its size ceiling (issue #1995).\n */\n private async fetchWithTimeout(\n url: string,\n timeoutMs: number = 2000,\n headers?: Record<string, string>,\n signal?: AbortSignal,\n ): Promise<ProbeFetchResult> {\n return await probeFetch(url, {\n timeoutMs,\n headers: this.buildRequestHeaders({ Accept: \"application/json\", ...(headers ?? {}) }),\n signal,\n });\n }\n\n private async probeLmStudioNativeModels(\n probeBaseUrl: string,\n signal?: AbortSignal,\n ): Promise<{ matched: boolean; unauthorized: boolean }> {\n let unauthorized = false;\n for (const endpoint of [\"/api/v1/models\", \"/api/v0/models\"]) {\n if (signal?.aborted) return { matched: false, unauthorized };\n const probe = await this.fetchWithTimeout(`${probeBaseUrl}${endpoint}`, 2000, undefined, signal);\n if (signal?.aborted) return { matched: false, unauthorized };\n if (probe.ok && isLmStudioNativeModelsResponse(probe.data)) {\n return { matched: true, unauthorized };\n }\n if (probe.status === 401 || probe.status === 403) {\n unauthorized = true;\n }\n }\n return { matched: false, unauthorized };\n }\n\n /**\n * Check if local LLM is available\n * Uses 127.0.0.1 instead of localhost to avoid DNS issues (consistent with tactician)\n */\n async checkAvailability(signal?: AbortSignal): Promise<boolean> {\n if (signal?.aborted) return false;\n // Cache health check results for 1 minute\n const now = Date.now();\n const trippedState = this.getTrippedBackendState(now);\n if (trippedState) {\n this.isAvailable = false;\n this.lastHealthCheck = 0;\n log.info(\n `local LLM availability: backend circuit open for ${Math.max(0, trippedState.untilMs - now)}ms (${trippedState.reason})`,\n );\n return false;\n }\n if (this.isAvailable !== null && now - this.lastHealthCheck < LocalLlmClient.HEALTH_CHECK_INTERVAL_MS) {\n return this.isAvailable;\n }\n return await this.availabilityProbe.run((probeSignal) => this.probeAvailability(probeSignal), signal);\n }\n\n private async probeAvailability(signal?: AbortSignal): Promise<boolean> {\n const now = Date.now();\n // Probe server-native endpoints from the server root even when users configure\n // the OpenAI-compatible `/v1` base URL for chat completions.\n const configuredBaseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n const probeBaseUrl = stripTrailingV1Path(configuredBaseUrl);\n let sawUnauthorizedProbe = false;\n // A probe that TIMED OUT says the event loop was busy, not that the backend\n // is down (issue #2210). Tracked separately so a loaded daemon cannot cache\n // itself into a blackout.\n let sawAbortedProbe = false;\n\n // Try to detect which server type is running\n if (signal?.aborted) return false;\n for (const serverConfig of orderedLocalServers(configuredBaseUrl)) {\n const healthUrl = `${probeBaseUrl}${serverConfig.healthEndpoint}`;\n log.debug(`checking ${serverConfig.type} at ${healthUrl}`);\n\n const result = await this.fetchWithTimeout(healthUrl, 2000, undefined, signal);\n if (result.aborted) sawAbortedProbe = true;\n if (signal?.aborted) return false;\n if (result.ok && serverConfig.detectFn(result.data)) {\n if (serverConfig.type === \"mlx\") {\n const lmStudioProbe = await this.probeLmStudioNativeModels(probeBaseUrl, signal);\n if (signal?.aborted) return false;\n if (lmStudioProbe.unauthorized) {\n sawUnauthorizedProbe = true;\n }\n if (lmStudioProbe.matched) {\n this.isAvailable = true;\n this.detectedType = \"lmstudio\";\n this.lastHealthCheck = now;\n log.info(`detected lmstudio at ${configuredBaseUrl}`);\n return true;\n }\n }\n if (serverConfig.type === \"llamacpp\") {\n let sawLlamaCppSignal = false;\n const propsProbe = await this.fetchWithTimeout(`${probeBaseUrl}/props`, 2000, undefined, signal);\n if (propsProbe.aborted) sawAbortedProbe = true;\n if (signal?.aborted) return false;\n if (propsProbe.ok && isLlamaCppPropsResponse(propsProbe.data)) {\n sawLlamaCppSignal = true;\n }\n if (propsProbe.status === 401 || propsProbe.status === 403) {\n sawUnauthorizedProbe = true;\n }\n\n const modelsUrl = `${probeBaseUrl}${serverConfig.modelsEndpoint}`;\n const modelsProbe = await this.fetchWithTimeout(modelsUrl, 2000, undefined, signal);\n if (modelsProbe.aborted) sawAbortedProbe = true;\n if (signal?.aborted) return false;\n if (modelsProbe.ok && isLlamaCppModelsResponse(modelsProbe.data)) {\n sawLlamaCppSignal = true;\n }\n if (modelsProbe.status === 401 || modelsProbe.status === 403) {\n sawUnauthorizedProbe = true;\n continue;\n }\n\n const authConfigured =\n Boolean(this.config.localLlmApiKey) &&\n this.config.localLlmAuthHeader !== false;\n if (!sawLlamaCppSignal || (authConfigured && !modelsProbe.ok)) {\n continue;\n }\n }\n this.isAvailable = true;\n this.detectedType = serverConfig.type;\n this.lastHealthCheck = now;\n log.info(`detected ${serverConfig.type} at ${configuredBaseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n }\n if (signal?.aborted) return false;\n\n // Generic check if specific detection failed\n try {\n const modelsUrl = `${probeBaseUrl}/v1/models`;\n const result = await this.fetchWithTimeout(modelsUrl, 2000, undefined, signal);\n if (result.aborted) sawAbortedProbe = true;\n if (signal?.aborted) return false;\n if (result.ok) {\n this.isAvailable = true;\n this.detectedType = \"generic\";\n this.lastHealthCheck = now;\n log.info(`detected generic OpenAI-compatible server at ${configuredBaseUrl}`);\n return true;\n }\n if (result.status === 401 || result.status === 403) {\n sawUnauthorizedProbe = true;\n }\n } catch {\n // Fall through to unavailable\n }\n\n const wasAvailable = this.isAvailable;\n this.detectedType = null;\n if (sawUnauthorizedProbe) {\n log.warn(\n `local LLM availability probe was unauthorized at ${configuredBaseUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n const verdict = resolveUnavailableVerdict({\n baseUrl: configuredBaseUrl,\n sawAbortedProbe,\n wasAvailable,\n });\n this.isAvailable = verdict.cacheVerdict ? false : null;\n this.lastHealthCheck = verdict.cacheVerdict ? now : 0;\n if (verdict.warning) log.warn(verdict.warning);\n log.debug(\"local LLM not available at\", configuredBaseUrl);\n return false;\n }\n\n /**\n * Try to get context window from LM Studio settings.json as fallback.\n * This reads the defaultContextLength setting which is what LM Studio uses\n * when loading models without explicit context configuration.\n */\n private getContextFromLmStudioSettings(): number | null {\n try {\n const homeDir = this.resolveHomeDir();\n const settingsPath = `${homeDir}/.cache/lm-studio/settings.json`;\n\n if (!fs.existsSync(settingsPath)) {\n log.debug(`LM Studio settings: file not found at ${settingsPath}`);\n return null;\n }\n\n const content = fs.readFileSync(settingsPath, \"utf-8\");\n const settings = JSON.parse(content) as {\n defaultContextLength?: {\n type?: string;\n value?: number;\n };\n };\n\n if (settings.defaultContextLength?.value) {\n const contextWindow = settings.defaultContextLength.value;\n log.debug(`LM Studio settings: found default context length: ${contextWindow}`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LM Studio settings: failed to read - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Try to get context window from LMS CLI (LM Studio specific).\n * Uses --json flag for reliable parsing.\n * Returns null if LMS CLI is not available or model not found.\n */\n private getContextFromLmsCli(modelId: string): number | null {\n try {\n // Check if lms CLI exists in common locations.\n // HOME may be absent in launchd environments, so prefer the resolved helper.\n const homeDir = this.resolveHomeDir();\n const lmsPaths = [\n this.config.localLmsCliPath || \"\",\n `${homeDir}/.cache/lm-studio/bin/lms`,\n \"/usr/local/bin/lms\",\n \"/opt/homebrew/bin/lms\",\n ];\n\n const lmsPath = lmsPaths.find((p) => p.length > 0 && fs.existsSync(p));\n if (!lmsPath) {\n log.debug(`LMS CLI: not found in standard locations (checked: ${lmsPaths.join(\", \")})`);\n return null;\n }\n\n // Run lms ps --json to get loaded models with context\n // Use spawnSync with shell and explicit PATH to ensure lms can find its dependencies\n log.debug(`LMS CLI: running: ${lmsPath} ps --json`);\n const existingPath = readEnvVar(\"PATH\") || \"\";\n const result = launchProcessSync(lmsPath, [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false, // Don't use shell for JSON output - more reliable\n env: mergeEnv({\n PATH: `${this.config.localLmsBinDir || `${homeDir}/.cache/lm-studio/bin`}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${existingPath}`,\n HOME: homeDir,\n }),\n });\n\n if (result.error) {\n log.debug(`LMS CLI: spawn error - ${result.error.message}`);\n return null;\n }\n\n if (result.stderr && result.stderr.trim()) {\n log.debug(`LMS CLI: stderr - ${result.stderr.slice(0, 200)}`);\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n log.debug(\"LMS CLI: empty output - LM Studio may not be running or no models loaded\");\n return null;\n }\n\n // Parse JSON output\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch (parseErr) {\n log.debug(`LMS CLI: JSON parse error - ${parseErr}`);\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n log.debug(\"LMS CLI: no models loaded\");\n return null;\n }\n\n // Find the model matching our configured model ID\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model) {\n log.debug(`LMS CLI: model \"${modelId}\" not found in loaded models: ${models.map(m => m.identifier).join(\", \")}`);\n return null;\n }\n\n // Use contextLength (actual configured) or fall back to maxContextLength (model max)\n const contextWindow = model.contextLength || model.maxContextLength;\n\n if (contextWindow) {\n log.info(`LMS CLI detected context window: ${contextWindow} for ${modelId} (max: ${model.maxContextLength})`);\n return contextWindow;\n }\n\n return null;\n } catch (err) {\n // LMS CLI not available or failed\n const errorMsg = err instanceof Error ? err.message : String(err);\n log.debug(`LMS CLI: failed - ${errorMsg}`);\n return null;\n }\n }\n\n /**\n * Get full model info from LMS CLI including context length and max context length.\n * Returns null if LMS CLI is unavailable or model not found.\n */\n private getLmsModelInfo(modelId: string): { contextLength: number; maxContextLength: number; identifier: string } | null {\n try {\n const result = launchProcessSync(\"lms\", [\"ps\", \"--json\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n shell: false,\n });\n\n if (result.error) {\n return null;\n }\n\n const output = result.stdout || \"\";\n if (!output.trim()) {\n return null;\n }\n\n let models: Array<{\n identifier?: string;\n modelKey?: string;\n contextLength?: number;\n maxContextLength?: number;\n }>;\n\n try {\n models = JSON.parse(output) as typeof models;\n } catch {\n return null;\n }\n\n if (!Array.isArray(models) || models.length === 0) {\n return null;\n }\n\n const model = models.find((m) =>\n m.identifier === modelId ||\n m.modelKey === modelId ||\n (m.identifier?.includes(modelId.replace(/@\\d+bit$/, \"\")))\n );\n\n if (!model || !model.contextLength) {\n return null;\n }\n\n return {\n contextLength: model.contextLength,\n maxContextLength: model.maxContextLength || model.contextLength,\n identifier: model.identifier || modelId,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get context window for the configured model, using cache if available.\n * This method caches the result to avoid repeated LMS CLI calls.\n * Order: ModelRegistry (persistent) -> memory cache -> LMS CLI -> settings.json\n */\n getCachedContextWindow(modelId: string): number | null {\n const now = Date.now();\n\n // 1. Check ModelRegistry for persisted context window\n if (this.modelRegistry) {\n const caps = this.modelRegistry.getCapabilities(modelId);\n if (caps.source === \"lmstudio\" && caps.contextWindow) {\n log.debug(`ModelRegistry: using persisted LM Studio context: ${caps.contextWindow}`);\n // Also update memory cache\n this.cachedLmsContext = caps.contextWindow;\n this.lastLmsCheck = now;\n return caps.contextWindow;\n }\n }\n\n // 2. Return in-memory cached value if still valid\n if (this.cachedLmsContext && now - this.lastLmsCheck < LocalLlmClient.LMS_CACHE_INTERVAL_MS) {\n log.debug(`LMS CLI: returning in-memory cached context: ${this.cachedLmsContext}`);\n return this.cachedLmsContext;\n }\n\n // 3. Try LMS CLI (authoritative source)\n const lmsInfo = this.getLmsModelInfo(modelId);\n if (lmsInfo?.contextLength) {\n this.cachedLmsContext = lmsInfo.contextLength;\n this.lastLmsCheck = now;\n // Calculate appropriate output tokens based on context size\n // Use 12.5% of context window, capped at 16K (generous but safe)\n const calculatedOutputTokens = Math.min(Math.floor(lmsInfo.contextLength / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096); // Minimum 4K\n // Persist to ModelRegistry with detected capabilities\n if (this.modelRegistry) {\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: lmsInfo.maxContextLength || lmsInfo.contextLength,\n contextWindow: lmsInfo.contextLength,\n supportsExtendedContext: (lmsInfo.maxContextLength || lmsInfo.contextLength) > 65536,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n log.info(`LMS CLI: Stored capabilities for ${modelId}: ${lmsInfo.contextLength} context, ${outputTokens} output tokens`);\n }\n return lmsInfo.contextLength;\n }\n\n // Legacy: Try LMS CLI context only (fallback)\n const legacyContext = this.getContextFromLmsCli(modelId);\n if (legacyContext) {\n this.cachedLmsContext = legacyContext;\n this.lastLmsCheck = now;\n // Persist to ModelRegistry with calculated output tokens\n if (this.modelRegistry) {\n const calculatedOutputTokens = Math.min(Math.floor(legacyContext / 8), 16384);\n const outputTokens = Math.max(calculatedOutputTokens, 4096);\n this.modelRegistry.setCapabilities(modelId, {\n maxPositionEmbeddings: legacyContext,\n contextWindow: legacyContext,\n supportsExtendedContext: false,\n typicalOutputTokens: outputTokens,\n source: \"lmstudio\",\n });\n }\n return legacyContext;\n }\n\n // 4. Fall back to LM Studio settings.json\n const settingsContext = this.getContextFromLmStudioSettings();\n if (settingsContext) {\n log.info(`LM Studio settings: using default context: ${settingsContext}`);\n this.cachedLmsContext = settingsContext;\n this.lastLmsCheck = now;\n return settingsContext;\n }\n\n return null;\n }\n\n /**\n * Clear the LMS context cache. Call this when the model changes.\n */\n clearContextCache(): void {\n this.cachedLmsContext = null;\n this.lastLmsCheck = 0;\n log.debug(\"LMS CLI: context cache cleared\");\n }\n\n private remainingCooldownMs(now: number = Date.now()): number {\n return Math.max(0, this.cooldownUntilMs - now);\n }\n\n private scheduleQueueDrain(): void {\n if (this.queueDrainScheduled) return;\n this.queueDrainScheduled = true;\n\n queueMicrotask(() => {\n this.queueDrainScheduled = false;\n this.startAvailableQueuedRequests();\n });\n }\n\n /** True when a background-priority request occupies or awaits this client's local lane (issue #2011). */\n isBackgroundLaneContended(): boolean {\n return this.queueProcessing.has(\"background\") || this.requestQueues.background.length > 0;\n }\n\n private dequeueQueuedRequest(priority: LocalLlmRequestPriority): LocalLlmQueuedRequest | null {\n const next = this.requestQueues[priority].shift();\n return next ?? null;\n }\n\n private failOpenQueuedRequestsForCooldown(): number {\n let dropped = 0;\n for (const priority of [\"recall-critical\", \"background\"] as const) {\n while (this.requestQueues[priority].length > 0) {\n const queued = this.requestQueues[priority].shift();\n queued?.resolve(null);\n dropped += 1;\n }\n }\n return dropped;\n }\n\n private startAvailableQueuedRequests(): void {\n if (!this.queueProcessing.has(\"recall-critical\")) {\n const nextCritical = this.dequeueQueuedRequest(\"recall-critical\");\n if (nextCritical) {\n this.queueProcessing.add(\"recall-critical\");\n void this.runQueuedRequest(nextCritical);\n }\n }\n\n if (!this.queueProcessing.has(\"background\")) {\n const nextBackground = this.dequeueQueuedRequest(\"background\");\n if (nextBackground) {\n this.queueProcessing.add(\"background\");\n void this.runQueuedRequest(nextBackground);\n }\n }\n }\n\n private async runQueuedRequest(next: LocalLlmQueuedRequest): Promise<void> {\n try {\n if (next.options.signal?.aborted) {\n next.resolve(null);\n return;\n }\n const remainingCooldownMs = this.remainingCooldownMs();\n if (remainingCooldownMs > 0) {\n const additionalDropped = this.failOpenQueuedRequestsForCooldown();\n log.warn(\n `local LLM: cooldown active (${remainingCooldownMs}ms remaining), dropping ${additionalDropped + 1} queued request(s) fail-open`,\n );\n next.resolve(null);\n return;\n }\n\n let result: LocalLlmChatCompletionResult | null = null;\n try {\n result = await this.runChatCompletionRequest(next.messages, next.options, {\n priority: next.priority,\n enqueuedAtMs: next.enqueuedAtMs,\n });\n } catch (err) {\n log.warn(`local LLM queue drain failed open: ${next.options.redactProviderErrors ? \"provider error details redacted\" : err instanceof Error ? err.message : String(err)}`);\n }\n next.resolve(result);\n } finally {\n this.queueProcessing.delete(next.priority);\n if (this.requestQueues[\"recall-critical\"].length > 0 || this.requestQueues.background.length > 0) {\n this.scheduleQueueDrain();\n }\n }\n }\n\n private async runChatCompletionRequest(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions,\n queueMeta?: { priority: LocalLlmRequestPriority; enqueuedAtMs: number },\n ): Promise<LocalLlmChatCompletionResult | null> {\n log.debug(\n `local LLM chatCompletion: localLlmEnabled=${resolveLocalLlmCapabilities(this.config).localLlm}, model=${this.config.localLlmModel}`,\n );\n\n const operation = options.operation ?? \"unspecified\";\n const startedAtMs = Date.now();\n if (queueMeta) {\n log.debug(\n `local LLM queue start: priority=${queueMeta.priority} waitMs=${startedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n\n try {\n if (options.signal?.aborted) return null;\n const isAvailable = await this.checkAvailability(options.signal);\n if (!isAvailable) {\n log.debug(\n `local LLM: checkAvailability returned false for ${this.config.localLlmUrl}`,\n );\n return null;\n }\n\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const requestBody: Record<string, unknown> = {\n model: this.config.localLlmModel,\n messages,\n temperature: options.temperature ?? 0.7,\n // Use max_tokens consistent with cloud models\n max_tokens: options.maxTokens ?? 4096,\n stream: false,\n };\n\n // Skip response_format for local LLMs - they don't support json_object type\n // The prompts already instruct the model to output JSON\n // Only send if it's json_schema type which some local LLMs support\n if (options.responseFormat?.type === \"json_schema\") {\n requestBody.response_format = options.responseFormat;\n }\n\n // Suppress thinking/reasoning for operations that benefit from terse,\n // structured output. Thinking-capable models (Qwen 3.5, Gemma 4,\n // DeepSeek) default to thinking-on via their chat template; sending\n // `chat_template_kwargs: { enable_thinking: false }` tells the template\n // to skip reasoning tokens. Consolidation operations on the main client\n // intentionally keep thinking enabled so entity resolution and\n // deduplication can use the model's reasoning path. Fast-tier callers can\n // still force suppression per request to preserve their low-latency\n // contract.\n //\n // Gate the injection on detected backend support (issue #548,\n // Codex P1 on PR #550): `chat_template_kwargs` is an LM Studio /\n // vLLM / llama.cpp extension, not part of standard OpenAI chat\n // completions. Strict OpenAI-compatible backends reject\n // unknown fields with 400, which trips the 400-cooldown path and\n // can effectively disable local extraction. Fail open when the\n // backend hasn't been positively identified as thinking-capable.\n const shouldSuppressThinking =\n options.forceDisableThinking === true ||\n ((options.disableThinking ?? this._disableThinking) &&\n THINKING_SUPPRESSED_OPERATIONS.has(operation));\n if (\n shouldSuppressThinking &&\n this.detectedType !== null &&\n THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)\n ) {\n requestBody.chat_template_kwargs = { enable_thinking: false };\n } else if (shouldSuppressThinking && this.detectedType === \"ollama\") {\n // Ollama's /v1 endpoint silently drops `chat_template_kwargs` AND\n // `enable_thinking` — the ONLY field it maps to think-off is\n // `reasoning_effort` (\"none\" → ThinkValue{false} in openai.go;\n // verified against Ollama 0.32.0 source and live probes,\n // issue #1996). Without this, thinking-capable models (Gemma 4,\n // Qwen 3.5+) burn 1,700-2,000 reasoning tokens per extraction on\n // Ollama, exceed the client timeout, and retry forever. The effort\n // is configurable via `localLlmReasoningEffort` (default \"none\");\n // an empty value disables injection (pre-#1996 behavior).\n const effort = this.config.localLlmReasoningEffort;\n if (effort) {\n requestBody.reasoning_effort = effort;\n }\n }\n\n // Normalize URL (use 127.0.0.1 instead of localhost)\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n const chatUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/chat/completions`\n : `${baseUrl}/v1/chat/completions`;\n\n const requestBodyJson = JSON.stringify(requestBody);\n log.debug(\n `local LLM: sending request to ${chatUrl} with model ${this.config.localLlmModel}`,\n );\n // Avoid logging request bodies by default (can contain sensitive user content).\n log.debug(`local LLM: request body length=${requestBodyJson.length}`);\n\n const effectiveTimeoutMs =\n typeof options.timeoutMs === \"number\"\n ? Math.min(this.config.localLlmTimeoutMs, options.timeoutMs)\n : this.config.localLlmTimeoutMs;\n const maxAttempts = 1 + Math.max(0, this.config.localLlmRetry5xxCount);\n let response: Response | null = null, responseBody = \"\";\n let lastAbortError: Error | null = null;\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n response = null;\n responseBody = \"\";\n const attemptAbort = new AbortController();\n const onCallerAbort = (): void => {\n attemptAbort.abort(options.signal?.reason);\n };\n if (options.signal) {\n if (options.signal.aborted) {\n onCallerAbort();\n } else {\n options.signal.addEventListener(\"abort\", onCallerAbort, { once: true });\n }\n }\n const attemptTimeout = setTimeout(() => attemptAbort.abort(), effectiveTimeoutMs);\n try {\n response = await this.chatTransport.post({\n url: chatUrl,\n headers: this.buildRequestHeaders({\n \"Content-Type\": \"application/json\",\n }),\n body: requestBodyJson,\n signal: attemptAbort.signal,\n budgetMs: this.config.localLlmTimeoutMs,\n });\n responseBody = await response.text();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n if (options.signal?.aborted) {\n response = null;\n lastAbortError = error;\n break;\n }\n if (response && !response.ok) {\n log.debug(`local LLM failed to read ${response.status} response body: ${options.redactProviderErrors ? \"details redacted\" : error.message}`);\n } else {\n response = null;\n if (!isAbortError(err)) throw err;\n lastAbortError = error;\n if (attempt >= maxAttempts) break;\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request aborted: op=${operation} attempt=${attempt}/${maxAttempts} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel}; retrying after ${backoffMs}ms`,\n );\n if (!(await waitForRetryBackoff(backoffMs, options.signal))) return null;\n continue;\n }\n } finally {\n clearTimeout(attemptTimeout);\n options.signal?.removeEventListener(\"abort\", onCallerAbort);\n }\n\n if (response.ok) break;\n if (response.status >= 500 && attempt < maxAttempts) {\n const nonRecoverableReason =\n extractNonRecoverableBackendReasonFromErrorText(responseBody);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n }\n if (response.status < 500 || attempt >= maxAttempts) break;\n\n const backoffMs = this.config.localLlmRetryBackoffMs * attempt;\n log.warn(\n `local LLM request got ${response.status}; retrying (attempt ${attempt + 1}/${maxAttempts}) after ${backoffMs}ms`,\n );\n if (!(await waitForRetryBackoff(backoffMs, options.signal))) return null;\n }\n log.debug(\n `local LLM: received response, status=${response?.status}, ok=${response?.ok}`,\n );\n if (!response) {\n if (lastAbortError) {\n log.warn(\n `local LLM request aborted after ${maxAttempts} attempt(s): op=${operation} timeoutMs=${effectiveTimeoutMs} model=${this.config.localLlmModel} promptChars=${promptChars} durationMs=${Date.now() - startedAtMs}`,\n );\n } else {\n log.warn(\n `local LLM request failed: no response object (op=${operation} model=${this.config.localLlmModel} durationMs=${Date.now() - startedAtMs})`,\n );\n }\n return null;\n }\n\n if (!response.ok) {\n let reason = \"\";\n try {\n const parsed = JSON.parse(responseBody) as { error?: { message?: string } };\n reason = parsed?.error?.message ? ` — ${parsed.error.message}` : \"\";\n } catch {\n if (!options.redactProviderErrors) log.debug(`local LLM error body: ${responseBody.slice(0, 500)}`);\n }\n log.warn(\n `local LLM request failed: ${response.status}${options.redactProviderErrors ? \"\" : ` ${response.statusText}${reason}`} ` +\n `(op=${operation}, model=${this.config.localLlmModel}, url=${chatUrl}, promptChars=${promptChars}, maxTokens=${requestBody.max_tokens as number})`,\n );\n const nonRecoverableReason =\n extractNonRecoverableBackendReason(reason) ??\n extractNonRecoverableBackendReasonFromErrorText(responseBody);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n this.consecutive400s = 0;\n return null;\n }\n if (response.status === 400) {\n this.consecutive400s += 1;\n if (this.consecutive400s >= this.config.localLlm400TripThreshold) {\n this.cooldownUntilMs = Date.now() + this.config.localLlm400CooldownMs;\n log.warn(\n `local LLM: entering cooldown for ${this.config.localLlm400CooldownMs}ms ` +\n `after ${this.consecutive400s} consecutive 400 responses`,\n );\n this.consecutive400s = 0;\n }\n } else {\n this.consecutive400s = 0;\n }\n return null;\n }\n this.consecutive400s = 0;\n\n const data = JSON.parse(responseBody) as {\n choices?: Array<{\n message?: { content?: string; reasoning_content?: string };\n }>;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n };\n };\n\n log.debug(\n `local LLM response: choices=${data.choices?.length}, usage=${JSON.stringify(data.usage)}`,\n );\n\n // Thinking models (e.g. Qwen 3.5) may put their response in\n // `reasoning_content` and leave `content` empty. Fall back to\n // reasoning_content so engram still gets a usable result.\n const msg = data.choices?.[0]?.message;\n const content = msg?.content || msg?.reasoning_content || \"\";\n if (!content) {\n log.warn(`local LLM returned empty content.${options.redactProviderErrors ? \"\" : ` choices=${JSON.stringify(data.choices)?.slice(0, 200)}`}`);\n return null;\n }\n\n // Estimate tokens if not provided by local LLM\n const usage = data.usage\n ? {\n promptTokens: data.usage.prompt_tokens ?? 0,\n completionTokens: data.usage.completion_tokens ?? 0,\n totalTokens: data.usage.total_tokens ?? 0,\n }\n : this.estimateTokens(messages, content);\n\n const durationMs = Date.now() - startedAtMs;\n if (resolvePipelineProcessingCapabilities(this.config).slowLog && durationMs >= this.config.slowLogThresholdMs) {\n const promptChars = messages.reduce((sum, m) => sum + (m.content?.length ?? 0), 0);\n const op = options.operation ? ` op=${options.operation}` : \"\";\n log.warn(\n `SLOW local LLM:${op} durationMs=${durationMs} model=${this.config.localLlmModel} url=${chatUrl} promptChars=${promptChars} outputTokens=${usage.completionTokens} totalTokens=${usage.totalTokens}`,\n );\n }\n\n log.debug(\"local LLM: request succeeded, tokens:\", usage.totalTokens);\n return { content, usage };\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n const durationMs = Date.now() - startedAtMs;\n if (isAbortError(err)) {\n log.warn(\n `local LLM request aborted: op=${operation} timeoutMs=${options.timeoutMs ?? this.config.localLlmTimeoutMs} model=${this.config.localLlmModel} durationMs=${durationMs} error=${options.redactProviderErrors ? \"provider error details redacted\" : errMsg}`,\n );\n return null;\n }\n log.warn(`local LLM request error: op=${operation} error=${options.redactProviderErrors ? \"provider error details redacted\" : errMsg}`);\n this.isAvailable = false; // Mark as unavailable on non-abort errors\n const nonRecoverableReason = extractNonRecoverableBackendReason(errMsg);\n if (nonRecoverableReason) {\n this.markBackendUnavailable(\n nonRecoverableReason,\n this.config.localLlm400CooldownMs,\n );\n }\n return null;\n } finally {\n if (queueMeta) {\n const finishedAtMs = Date.now();\n const waitMs = startedAtMs - queueMeta.enqueuedAtMs;\n log.debug(\n `local LLM queue finish: priority=${queueMeta.priority} waitMs=${waitMs} runMs=${finishedAtMs - startedAtMs} totalMs=${finishedAtMs - queueMeta.enqueuedAtMs} op=${operation}`,\n );\n }\n }\n }\n\n /**\n * Query the local LLM server for loaded model information.\n * Returns null if unavailable or if the model is not found.\n */\n async getLoadedModelInfo(): Promise<LocalModelInfo | null> {\n const baseUrl = trimTrailingSlashes(\n this.config.localLlmUrl.replace(\"localhost\", \"127.0.0.1\"),\n );\n\n // Handle URL construction - localLlmUrl may already include /v1\n const modelsUrl = baseUrl.endsWith(\"/v1\")\n ? `${baseUrl}/models`\n : `${baseUrl}/v1/models`;\n log.debug(`Fetching model info from ${modelsUrl}`);\n\n try {\n const result = await this.fetchWithTimeout(modelsUrl, 3000);\n if (!result.ok) {\n if (result.status === 401 || result.status === 403) {\n log.warn(\n `Local LLM: unauthorized while fetching models from ${modelsUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,\n );\n }\n log.warn(`Local LLM: Failed to fetch models from ${modelsUrl} - server returned error`);\n return null;\n }\n if (!result.data) {\n log.warn(`Local LLM: No data returned from ${modelsUrl}`);\n return null;\n }\n\n const data = result.data as {\n data?: Array<{\n id?: string;\n object?: string;\n owned_by?: string;\n // LM Studio specific fields\n max_context_length?: number;\n max_tokens?: number;\n // Ollama specific\n name?: string;\n details?: {\n parameter_size?: string;\n family?: string;\n };\n }>;\n };\n\n if (!Array.isArray(data.data) || data.data.length === 0) {\n log.warn(\"Local LLM returned no models\");\n return null;\n }\n\n // Verbose model listings are noisy on every gateway restart. Keep it debug-only.\n const modelIds = data.data.map((m) => m.id).filter(Boolean);\n log.debug(\n `Local LLM: Found ${modelIds.length} model(s). First 10: ${modelIds.slice(0, 10).join(\", \")}`,\n );\n\n // Find the model matching our configured model ID\n const configuredModel = this.config.localLlmModel;\n let model = data.data.find((m) => m.id === configuredModel);\n\n // If not found by exact match, try partial match (handle suffixes like @4bit)\n if (!model) {\n model = data.data.find((m) =>\n configuredModel.includes(m.id || \"\") ||\n (m.id || \"\").includes(configuredModel.replace(/@\\d+bit$/, \"\"))\n );\n }\n\n // If still not found, use the first loaded model and warn\n if (!model) {\n model = data.data[0];\n const availablePreview = data.data\n .map((m) => m.id)\n .filter(Boolean)\n .slice(0, 10)\n .join(\", \");\n log.warn(\n `Configured model \"${configuredModel}\" not found in local LLM. ` +\n `Using \"${model.id}\" instead. Available (first 10): ${availablePreview}`\n );\n }\n\n // Extract context window - try multiple field names\n let contextWindow = model.max_context_length || model.max_tokens;\n\n // If API doesn't report context window, try LMS CLI (LM Studio specific)\n if (!contextWindow) {\n log.info(\"Local LLM: API did not report context window, trying LMS CLI...\");\n const lmsContext = this.getCachedContextWindow(model.id || \"\");\n if (lmsContext) {\n contextWindow = lmsContext;\n }\n }\n\n this.cachedModelInfo = {\n id: model.id || \"unknown\",\n contextWindow: contextWindow,\n maxTokens: model.max_tokens,\n };\n\n log.info(\n `Local LLM model detected: ${this.cachedModelInfo.id}, ` +\n `context window: ${contextWindow?.toLocaleString() || \"unknown (may use default)\"}`\n );\n\n return this.cachedModelInfo;\n } catch (err) {\n log.warn(`Failed to fetch model info: ${err}`);\n return null;\n }\n }\n\n /**\n * Check if the configured model is available and get its actual context window.\n * Warns if there's a mismatch between expected and actual context.\n */\n async validateModelConfig(expectedContextWindow?: number): Promise<{\n available: boolean;\n actualContextWindow?: number;\n warnings: string[];\n }> {\n const warnings: string[] = [];\n\n const modelInfo = await this.getLoadedModelInfo();\n if (!modelInfo) {\n return { available: false, warnings: [\"Could not query local LLM for model info\"] };\n }\n\n // If we have expected context and the server reports one, check for mismatch\n if (expectedContextWindow && modelInfo.contextWindow) {\n if (modelInfo.contextWindow < expectedContextWindow) {\n warnings.push(\n `Context window mismatch: Model ${modelInfo.id} supports ${modelInfo.contextWindow.toLocaleString()} tokens, ` +\n `but engram is configured for ${expectedContextWindow.toLocaleString()}. ` +\n `Set localLlmMaxContext: ${modelInfo.contextWindow} in config to avoid errors.`\n );\n }\n }\n\n // Warn if server doesn't report context window (common with some local LLM setups)\n if (!modelInfo.contextWindow) {\n warnings.push(\n `Local LLM server did not report context window for ${modelInfo.id}. ` +\n `If you get \"context length exceeded\" errors, set localLlmMaxContext in config.`\n );\n }\n\n return {\n available: true,\n actualContextWindow: modelInfo.contextWindow,\n warnings,\n };\n }\n\n /**\n * Make a chat completion request to local LLM\n */\n async chatCompletion(\n messages: Array<{ role: string; content: string }>,\n options: LocalLlmChatCompletionOptions = {},\n ): Promise<LocalLlmChatCompletionResult | null> {\n if (!resolveLocalLlmCapabilities(this.config).localLlm) {\n log.debug(\"local LLM: disabled, returning null\");\n return null;\n }\n\n const remainingMs = this.remainingCooldownMs();\n if (remainingMs > 0) {\n log.debug(`local LLM: cooldown active (${remainingMs}ms remaining), skipping request`);\n return null;\n }\n if (options.priority) {\n const priority = options.priority;\n return await new Promise<LocalLlmChatCompletionResult | null>((resolve) => {\n this.requestQueues[priority].push({\n messages,\n options,\n priority,\n enqueuedAtMs: Date.now(),\n resolve,\n });\n this.scheduleQueueDrain();\n });\n }\n\n return await this.runChatCompletionRequest(messages, options);\n }\n\n /**\n * Estimate tokens when local LLM doesn't return usage stats\n * Rough estimate: 1 token ≈ 4 characters\n */\n private estimateTokens(\n messages: Array<{ role: string; content: string }>,\n response: string\n ): { promptTokens: number; completionTokens: number; totalTokens: number } {\n const promptChars = messages.reduce((sum, m) => sum + m.content.length, 0);\n const promptTokens = Math.ceil(promptChars / 4);\n const completionTokens = Math.ceil(response.length / 4);\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n };\n }\n\n /**\n * Try local LLM first, fallback to cloud provider if configured\n */\n async withFallback<T>(\n localOperation: () => Promise<T | null>,\n fallbackOperation: () => Promise<T>,\n operationName: string\n ): Promise<T> {\n // Try local LLM first if enabled\n if (resolveLocalLlmCapabilities(this.config).localLlm) {\n const localResult = await localOperation();\n if (localResult !== null) {\n log.debug(`${operationName}: used local LLM`);\n return localResult;\n }\n\n // Local failed or unavailable\n if (this.config.localLlmFallback) {\n log.info(`${operationName}: local LLM unavailable, falling back to cloud`);\n } else {\n throw new Error(`${operationName}: local LLM unavailable and fallback disabled`);\n }\n }\n\n // Use fallback (cloud provider)\n return fallbackOperation();\n }\n}\n"],"mappings":"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","names":["backoffMs","promptChars"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/transcript.ts"],"sourcesContent":["import { appendFile, mkdir, readdir, readFile, stat, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { log } from \"./logger.js\";\nimport type { TranscriptEntry, Checkpoint, PluginConfig } from \"./types.js\";\nimport { analyzeSessionIntegrity, type SessionIntegrityReport } from \"./session-integrity.js\";\nimport { resolveSafeStoragePath } from \"./storage-paths.js\";\nimport { sessionStoragePaths } from \"./session-identity.js\";\n\ntype DirectorySessionStatus = \"missing\" | \"empty\" | \"matches\" | \"occupied\";\ntype DirectoryOwnershipCacheEntry = {\n status: \"empty\" | \"matches\";\n fileSizes: Map<string, number>;\n};\n\n/**\n * De-duplicate JSONL lines collected across multiple read-back candidate\n * directories. A partially-applied migration (issue #1496) can leave the SAME\n * raw row in both the primary `session/<hash>` tree and a legacy read-back dir\n * (`other/default` or an old `parts.length >= 3` directory) when the source was\n * copied but not yet trimmed. Without this guard, `readRecent`, `readToolUse`,\n * footprint estimation, and the summarizer fetch would return that row twice\n * (cursor review on PR #1504). The exact raw JSONL line is the stable identity:\n * the migration preserves byte content per session, so a copied-but-not-trimmed\n * row is byte-identical in both locations. Returns `true` the first time a line\n * is seen, `false` for every subsequent duplicate.\n */\nfunction makeRawLineDeduper(): (rawLine: string) => boolean {\n const seen = new Set<string>();\n return (rawLine: string): boolean => {\n if (seen.has(rawLine)) return false;\n seen.add(rawLine);\n return true;\n };\n}\n\n/**\n * Compute the half-open UTC instant range `[start, end)` that covers an entire\n * calendar day, suitable for {@link TranscriptManager.readRange}.\n *\n * `readRange` filters with an EXCLUSIVE upper bound (`entryTime < end`, CLAUDE.md\n * rule #35 / AGENTS.md rule 23). The end is therefore the NEXT day's\n * `00:00:00Z`, not `${date}T23:59:59Z`: a literal `23:59:59Z` end (== `.000Z`)\n * would drop any entry stamped in the final second of the day\n * (`23:59:59.000Z`–`23:59:59.999Z`). Using the next day's midnight keeps the\n * `[start, end)` semantics intact while including the whole day.\n *\n * @param date - A `YYYY-MM-DD` calendar day (UTC).\n */\nexport function utcDayRange(date: string): { start: string; end: string } {\n const start = `${date}T00:00:00Z`;\n const startMs = Date.parse(start);\n if (Number.isNaN(startMs)) {\n // Malformed date: preserve the pre-existing \"empty range\" behavior (an\n // unparseable bound makes `readRange` match nothing) rather than throwing.\n return { start, end: start };\n }\n const next = new Date(startMs);\n next.setUTCDate(next.getUTCDate() + 1);\n return { start, end: `${next.toISOString().slice(0, 10)}T00:00:00Z` };\n}\n\n/**\n * Manages conversation transcript storage, checkpointing, and recall formatting.\n *\n * Transcripts are stored as JSONL files in a hierarchical structure:\n * transcripts/{channelType}/{channelId}.jsonl\n *\n * Channel types are extracted from sessionKey (discord, slack, cron, main, etc.)\n * Checkpoints are used to preserve conversation context across compaction events.\n */\nexport class TranscriptManager {\n private transcriptsDir: string;\n private checkpointPath: string;\n private stateDir: string;\n private toolUsageDir: string;\n private config: PluginConfig;\n private sessionFootprintCache = new Map<\n string,\n { totalBytes: number; fileBytes: Map<string, number>; fileSizes: Map<string, number> }\n >();\n private directoryOwnershipCache = new Map<string, DirectoryOwnershipCacheEntry>();\n\n /** Default checkpoint TTL in hours */\n private static readonly DEFAULT_CHECKPOINT_TTL_HOURS = 24;\n /** Approximate characters per token for rough estimation */\n private static readonly CHARS_PER_TOKEN = 4;\n\n constructor(config: PluginConfig) {\n this.config = config;\n this.transcriptsDir = path.join(config.memoryDir, \"transcripts\");\n this.stateDir = path.join(config.memoryDir, \"state\");\n this.checkpointPath = path.join(this.stateDir, \"checkpoint.json\");\n this.toolUsageDir = path.join(this.stateDir, \"tool-usage\");\n }\n\n /**\n * Resolve the storage path pieces for a sessionKey via the shared\n * {@link sessionStoragePaths} helper (issue #1496, rule #22).\n *\n * Legacy `agent:<id>:...` shapes keep their existing readable channel paths.\n * Arbitrary / non-legacy keys (e.g. `pi-geek:abc123`) route to\n * `transcripts/session/<hash>/YYYY-MM-DD.jsonl` from the FIRST write — they\n * never start life in `other/default`. For those keys a list of\n * read-back-only `readbackDirs` is returned so data written by older builds\n * (under `other/default`, or under an old `parts.length >= 3` directory such\n * as `baz/default`) stays discoverable until migrated. New writes never\n * target read-back dirs. See `session-identity.ts`.\n *\n * @returns Object with raw channel identifiers and encoded storage path pieces.\n */\n getTranscriptPath(sessionKey: string): {\n dir: string;\n file: string;\n channelType: string;\n channelId: string;\n alternateDir: string;\n legacyDir?: string;\n readbackDirs: string[];\n } {\n const paths = sessionStoragePaths(sessionKey);\n\n // Daily rotation: transcripts/{channelType}/{channelId}/YYYY-MM-DD.jsonl\n const today = new Date().toISOString().slice(0, 10);\n\n return {\n dir: paths.dir,\n file: `${today}.jsonl`,\n channelType: paths.channelType,\n channelId: paths.channelId,\n alternateDir: paths.alternateDir,\n legacyDir: paths.legacyDir,\n readbackDirs: paths.readbackDirs,\n };\n }\n\n /**\n * Initialize the transcript manager by ensuring directories exist.\n */\n async initialize(): Promise<void> {\n await mkdir(this.transcriptsDir, { recursive: true });\n await mkdir(this.stateDir, { recursive: true });\n await mkdir(this.toolUsageDir, { recursive: true });\n log.info(\"transcript manager initialized\");\n }\n\n /**\n * Best-effort list of sessionKeys that have transcript files on disk.\n * This is used by cron-style tooling (hourly summaries, conversation indexing)\n * to iterate across \"active\" sessions.\n */\n async listSessionKeys(): Promise<string[]> {\n const transcriptDir = this.transcriptsDir;\n const sessionKeys = new Set<string>();\n\n try {\n const typeEntries = await readdir(transcriptDir, { withFileTypes: true });\n for (const typeEnt of typeEntries) {\n if (!typeEnt.isDirectory()) continue;\n const typeDir = path.join(transcriptDir, typeEnt.name);\n const idEntries = await readdir(typeDir, { withFileTypes: true });\n for (const idEnt of idEntries) {\n if (!idEnt.isDirectory()) continue;\n const chanDir = path.join(typeDir, idEnt.name);\n const files = (await readdir(chanDir)).filter((f) => f.endsWith(\".jsonl\")).sort();\n const last = files[files.length - 1];\n if (!last) continue;\n try {\n const raw = await readFile(path.join(chanDir, last), \"utf-8\");\n const firstLine = raw.split(\"\\n\").find((l) => l.trim().length > 0);\n if (!firstLine) continue;\n const entry = JSON.parse(firstLine) as TranscriptEntry;\n if (typeof entry.sessionKey === \"string\" && entry.sessionKey.length > 0) {\n sessionKeys.add(entry.sessionKey);\n }\n } catch {\n // ignore\n }\n }\n }\n } catch {\n return [];\n }\n\n return Array.from(sessionKeys);\n }\n\n getToolUsagePath(sessionKey: string): {\n dir: string;\n file: string;\n alternateDir: string;\n legacyDir?: string;\n readbackDirs: string[];\n } {\n const p = this.getTranscriptPath(sessionKey);\n return {\n dir: p.dir,\n file: p.file,\n alternateDir: p.alternateDir,\n legacyDir: p.legacyDir,\n readbackDirs: p.readbackDirs,\n };\n }\n\n private async selectStorageDirForWrite(\n root: string,\n dir: string,\n legacyDir?: string,\n sessionKey?: string,\n alternateDir?: string,\n ): Promise<{ dir: string; channelDir: string }> {\n const channelDir = await resolveSafeStoragePath(root, dir);\n const encodedStatus = await this.directorySessionStatus(root, dir, sessionKey);\n if (encodedStatus === \"matches\" || encodedStatus === \"empty\") return { dir, channelDir };\n\n if (legacyDir) {\n const legacyChannelDir = await resolveSafeStoragePath(root, legacyDir);\n if ((await this.directorySessionStatus(root, legacyDir, sessionKey)) === \"matches\") {\n return { dir: legacyDir, channelDir: legacyChannelDir };\n }\n }\n\n if (encodedStatus === \"missing\") return { dir, channelDir };\n\n if (alternateDir) {\n const alternateChannelDir = await resolveSafeStoragePath(root, alternateDir);\n const alternateStatus = await this.directorySessionStatus(root, alternateDir, sessionKey);\n if (\n alternateStatus === \"missing\" ||\n alternateStatus === \"empty\" ||\n alternateStatus === \"matches\"\n ) {\n return { dir: alternateDir, channelDir: alternateChannelDir };\n }\n }\n\n throw new Error(`transcript storage path collision for session: ${sessionKey ?? \"(unknown)\"}`);\n }\n\n private async directorySessionStatus(\n root: string,\n dir: string,\n sessionKey?: string,\n ): Promise<DirectorySessionStatus> {\n let channelDir: string;\n try {\n channelDir = await resolveSafeStoragePath(root, dir);\n if (!(await stat(channelDir)).isDirectory()) return \"occupied\";\n } catch (err) {\n const code =\n err && typeof err === \"object\" && \"code\" in err\n ? (err as { code?: string }).code\n : undefined;\n if (code === \"ENOENT\") return \"missing\";\n throw err;\n }\n\n let names: string[];\n try {\n names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\"));\n } catch {\n return \"occupied\";\n }\n\n const fileSizes = await this.directoryJsonlFileSizes(root, dir, names);\n if (!fileSizes) return \"occupied\";\n\n if (!sessionKey) return \"matches\";\n const cacheKey = this.directoryOwnershipCacheKey(root, dir, sessionKey);\n const cached = this.directoryOwnershipCache.get(cacheKey);\n if (cached && this.sameFileSizes(cached.fileSizes, fileSizes)) {\n return cached.status;\n }\n\n let hasEntries = false;\n let hasMatchingEntry = false;\n\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, dir, name).catch(() => null);\n if (filePath === null) return \"occupied\";\n try {\n const raw = await readFile(filePath, \"utf-8\");\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n hasEntries = true;\n try {\n const obj = JSON.parse(line) as { sessionKey?: string };\n if (obj.sessionKey === sessionKey) {\n hasMatchingEntry = true;\n } else {\n return \"occupied\";\n }\n } catch {\n return \"occupied\";\n }\n }\n } catch {\n return \"occupied\";\n }\n }\n\n const status = hasMatchingEntry ? \"matches\" : hasEntries ? \"occupied\" : \"empty\";\n if (status === \"matches\" || status === \"empty\") {\n this.directoryOwnershipCache.set(cacheKey, { status, fileSizes });\n }\n return status;\n }\n\n private directoryOwnershipCacheKey(root: string, dir: string, sessionKey: string): string {\n return `${path.resolve(root)}\\0${dir}\\0${sessionKey}`;\n }\n\n private sameFileSizes(left: Map<string, number>, right: Map<string, number>): boolean {\n if (left.size !== right.size) return false;\n for (const [name, size] of left) {\n if (right.get(name) !== size) return false;\n }\n return true;\n }\n\n private async directoryJsonlFileSizes(\n root: string,\n dir: string,\n names: string[],\n ): Promise<Map<string, number> | null> {\n const fileSizes = new Map<string, number>();\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, dir, name).catch(() => null);\n if (filePath === null) return null;\n const fileInfo = await stat(filePath).catch(() => null);\n if (!fileInfo?.isFile()) return null;\n fileSizes.set(name, Math.max(0, fileInfo.size));\n }\n return fileSizes;\n }\n\n private async rememberDirectoryOwnership(\n root: string,\n dir: string,\n sessionKey: string,\n ): Promise<void> {\n try {\n const channelDir = await resolveSafeStoragePath(root, dir);\n const names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\"));\n const fileSizes = await this.directoryJsonlFileSizes(root, dir, names);\n if (!fileSizes) return;\n this.directoryOwnershipCache.set(\n this.directoryOwnershipCacheKey(root, dir, sessionKey),\n { status: \"matches\", fileSizes },\n );\n } catch {\n // Cache refresh is best-effort; write path correctness does not depend on it.\n }\n }\n\n private async getSessionStorageFiles(\n root: string,\n dir: string,\n legacyDir?: string,\n alternateDir?: string,\n readbackDirs: string[] = [],\n ): Promise<Array<{ cacheKey: string; name: string; path: string }>> {\n const files: Array<{ cacheKey: string; name: string; path: string }> = [];\n const seenDirs = new Set<string>();\n\n for (const candidateDir of [dir, alternateDir, legacyDir, ...readbackDirs]) {\n if (!candidateDir || seenDirs.has(candidateDir)) continue;\n seenDirs.add(candidateDir);\n\n let channelDir: string;\n try {\n channelDir = await resolveSafeStoragePath(root, candidateDir);\n } catch {\n continue;\n }\n\n let names: string[];\n try {\n names = (await readdir(channelDir)).filter((file) => file.endsWith(\".jsonl\")).sort();\n } catch {\n continue;\n }\n\n for (const name of names) {\n const filePath = await resolveSafeStoragePath(root, candidateDir, name).catch(() => null);\n if (filePath === null) continue;\n files.push({\n cacheKey: path.join(candidateDir, name),\n name,\n path: filePath,\n });\n }\n }\n\n return files.sort((a, b) => a.cacheKey.localeCompare(b.cacheKey));\n }\n\n async appendToolUse(entry: { timestamp: string; sessionKey: string; tool: string }): Promise<void> {\n const { dir, file, alternateDir, legacyDir } = this.getToolUsagePath(entry.sessionKey);\n const { dir: writeDir, channelDir } = await this.selectStorageDirForWrite(\n this.toolUsageDir,\n dir,\n legacyDir,\n entry.sessionKey,\n alternateDir,\n );\n await mkdir(channelDir, { recursive: true });\n const filePath = await resolveSafeStoragePath(this.toolUsageDir, writeDir, file);\n await appendFile(filePath, JSON.stringify(entry) + \"\\n\", \"utf-8\");\n await this.rememberDirectoryOwnership(this.toolUsageDir, writeDir, entry.sessionKey);\n }\n\n async readToolUse(\n sessionKey: string,\n startTime: Date,\n endTime: Date,\n ): Promise<Array<{ timestamp: string; sessionKey: string; tool: string }>> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getToolUsagePath(sessionKey);\n try {\n const files = await this.getSessionStorageFiles(\n this.toolUsageDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n const out: Array<{ timestamp: string; sessionKey: string; tool: string }> = [];\n // Dedup identical raw rows that a partially-applied migration may have left\n // in both the primary dir and a read-back dir (issue #1496, cursor review).\n const keepRawLine = makeRawLineDeduper();\n for (const file of files) {\n const raw = await readFile(file.path, \"utf-8\");\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line) as any;\n const ts = new Date(String(obj.timestamp ?? \"\")).getTime();\n if (!Number.isFinite(ts)) continue;\n if (ts >= startTime.getTime() && ts < endTime.getTime()) {\n if (typeof obj.tool === \"string\" && typeof obj.sessionKey === \"string\") {\n if (obj.sessionKey === sessionKey && keepRawLine(line)) {\n out.push({ timestamp: obj.timestamp, sessionKey: obj.sessionKey, tool: obj.tool });\n }\n }\n }\n } catch {\n // ignore\n }\n }\n }\n return out;\n } catch {\n return [];\n }\n }\n\n async estimateSessionFootprint(sessionKey: string): Promise<{ bytes: number; tokens: number }> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);\n let bytes = 0;\n\n // NOTE: this is a best-effort byte ESTIMATE for compaction sizing, not an\n // exact row count, and is maintained via an incremental per-file cache. A\n // copied-but-not-trimmed migration window (issue #1496) can transiently\n // over-estimate by counting a duplicated row in both the primary and a\n // read-back dir; that self-heals once the migration trims the source. The\n // exact-once guarantee that matters for callers lives in readRecent /\n // readToolUse / the summarizer fetch, which dedup by raw line.\n try {\n const files = await this.getSessionStorageFiles(\n this.transcriptsDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n const cached = this.sessionFootprintCache.get(sessionKey);\n if (!cached) {\n const fileBytes = new Map<string, number>();\n const fileSizes = new Map<string, number>();\n for (const file of files) {\n try {\n const fileInfo = await stat(file.path);\n const sessionBytes = await this.estimateSessionBytesInFile(\n file.path,\n sessionKey,\n );\n fileBytes.set(file.cacheKey, sessionBytes);\n fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));\n bytes += sessionBytes;\n } catch {\n // fail-open\n }\n }\n this.sessionFootprintCache.set(sessionKey, { totalBytes: bytes, fileBytes, fileSizes });\n } else {\n bytes = cached.totalBytes;\n const seen = new Set(files.map((file) => file.cacheKey));\n\n // Drop removed files from the cached total.\n for (const [cachedFile, cachedSessionBytes] of cached.fileBytes.entries()) {\n if (!seen.has(cachedFile)) {\n bytes -= cachedSessionBytes;\n cached.fileBytes.delete(cachedFile);\n cached.fileSizes.delete(cachedFile);\n }\n }\n\n // Read only newly discovered files.\n for (const file of files) {\n if (cached.fileBytes.has(file.cacheKey)) continue;\n try {\n const fileInfo = await stat(file.path);\n const sessionBytes = await this.estimateSessionBytesInFile(file.path, sessionKey);\n cached.fileBytes.set(file.cacheKey, sessionBytes);\n cached.fileSizes.set(file.cacheKey, Math.max(0, fileInfo.size));\n bytes += sessionBytes;\n } catch {\n // fail-open\n }\n }\n\n // Recompute any shard whose file size changed. A session can have both\n // encoded and legacy directories during migration, so path ordering does\n // not reliably identify the file that can grow.\n for (const file of files) {\n try {\n const fileInfo = await stat(file.path);\n const size = Math.max(0, fileInfo.size);\n const previousSessionBytes = cached.fileBytes.get(file.cacheKey) ?? 0;\n const previousSize = cached.fileSizes.get(file.cacheKey) ?? -1;\n if (size !== previousSize) {\n const sessionBytes = await this.estimateSessionBytesInFile(file.path, sessionKey);\n cached.fileBytes.set(file.cacheKey, sessionBytes);\n cached.fileSizes.set(file.cacheKey, size);\n bytes += sessionBytes - previousSessionBytes;\n }\n } catch {\n // fail-open\n }\n }\n\n if (bytes < 0) bytes = 0;\n cached.totalBytes = bytes;\n }\n } catch {\n // fail-open\n this.sessionFootprintCache.delete(sessionKey);\n }\n\n return {\n bytes,\n tokens: Math.floor(bytes / TranscriptManager.CHARS_PER_TOKEN),\n };\n }\n\n private async estimateSessionBytesInFile(filePath: string, sessionKey: string): Promise<number> {\n try {\n const raw = await readFile(filePath, \"utf-8\");\n let total = 0;\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line) as { sessionKey?: string };\n if (parsed.sessionKey === sessionKey) {\n total += Buffer.byteLength(`${line}\\n`, \"utf-8\");\n }\n } catch {\n // fail-open for malformed lines\n }\n }\n return total;\n } catch {\n return 0;\n }\n }\n\n /**\n * Check if a file is a legacy flat transcript file (YYYY-MM-DD.jsonl format).\n */\n private isLegacyTranscriptFile(filename: string): boolean {\n return /^\\d{4}-\\d{2}-\\d{2}\\.jsonl$/.test(filename);\n }\n\n /**\n * Append a turn to the appropriate transcript file.\n * Files are stored hierarchically: transcripts/{channelType}/{channelId}.jsonl\n *\n * Skips channel types in config.transcriptSkipChannelTypes (e.g., \"cron\").\n */\n async append(entry: TranscriptEntry): Promise<void> {\n try {\n const { dir, file, channelType, alternateDir, legacyDir } = this.getTranscriptPath(entry.sessionKey);\n\n // Skip if this channel type is in the skip list\n if (this.config.transcriptSkipChannelTypes.includes(channelType)) {\n return;\n }\n\n const { dir: writeDir, channelDir } = await this.selectStorageDirForWrite(\n this.transcriptsDir,\n dir,\n legacyDir,\n entry.sessionKey,\n alternateDir,\n );\n const filePath = await resolveSafeStoragePath(this.transcriptsDir, writeDir, file);\n\n // Ensure channel directory exists\n await mkdir(channelDir, { recursive: true });\n\n const line = JSON.stringify(entry) + \"\\n\";\n await appendFile(filePath, line, \"utf-8\");\n await this.rememberDirectoryOwnership(this.transcriptsDir, writeDir, entry.sessionKey);\n log.debug(`appended transcript entry for ${entry.sessionKey}: ${entry.turnId}`);\n } catch (err) {\n log.error(\"failed to append transcript entry:\", err);\n throw err;\n }\n }\n\n /**\n * Get all transcript files from the hierarchical directory structure.\n * Recursively finds all .jsonl files in transcripts/{channelType}/{channelId}/ subdirectories.\n */\n private async getAllTranscriptFiles(): Promise<string[]> {\n const files: string[] = [];\n\n try {\n const entries = await readdir(this.transcriptsDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n // This is a channel type directory (discord, slack, cron, main, etc.)\n const channelTypeDir = path.join(this.transcriptsDir, entry.name);\n try {\n const channelTypeEntries = await readdir(channelTypeDir, { withFileTypes: true });\n\n for (const channelTypeEntry of channelTypeEntries) {\n if (channelTypeEntry.isDirectory()) {\n // This is a channel ID directory - contains daily transcript files\n const channelDir = path.join(channelTypeDir, channelTypeEntry.name);\n try {\n const channelFiles = await readdir(channelDir);\n for (const file of channelFiles) {\n if (file.endsWith(\".jsonl\")) {\n files.push(path.join(entry.name, channelTypeEntry.name, file));\n }\n }\n } catch {\n // Skip unreadable directories\n }\n } else if (channelTypeEntry.isFile() && channelTypeEntry.name.endsWith(\".jsonl\")) {\n // Legacy: channel type dir contains .jsonl files directly\n files.push(path.join(entry.name, channelTypeEntry.name));\n }\n }\n } catch {\n // Skip unreadable directories\n }\n } else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n // Legacy flat file - still include for backward compatibility\n files.push(entry.name);\n }\n }\n } catch {\n // Directory doesn't exist or is unreadable\n }\n\n return files;\n }\n\n /**\n * Read transcript entries for a date range.\n * Returns entries within the time range, optionally filtered by sessionKey.\n * Reads from all channel subdirectories in the hierarchical structure.\n *\n * The window is half-open `[start, end)`: the upper bound is exclusive so\n * caller-specified ranges (explicit dates, analytics buckets) don't\n * double-count at shared boundaries (CLAUDE.md rule #35 / AGENTS.md rule 23).\n * readRecent's inclusive-of-now behavior is handled by readRecent itself\n * extending its end, not by relaxing this bound.\n */\n async readRange(startTime: string, endTime: string, sessionKey?: string): Promise<TranscriptEntry[]> {\n const start = new Date(startTime);\n const end = new Date(endTime);\n const entries: TranscriptEntry[] = [];\n\n // When a sessionKey is given, a partially-applied #1496 migration can leave\n // the SAME raw row in both the primary `session/<hash>` dir and a read-back\n // dir (`other/default` or an old `parts.length >= 3` dir). `readRange` scans\n // EVERY transcript file, so it would append that row once per directory.\n // Dedup by exact raw line to give the same exact-once guarantee `readRecent`\n // / `readToolUse` / the summarizer fetch already provide (cursor review on\n // PR #1504). Only applied for the session-scoped path so unfiltered range\n // scans (each file already enumerated once) keep their existing behavior.\n const keepRawLine = sessionKey ? makeRawLineDeduper() : undefined;\n\n try {\n // Get all transcript files from the hierarchical structure\n const transcriptFiles = await this.getAllTranscriptFiles();\n\n // Read each relevant file\n for (const relativePath of transcriptFiles) {\n const filePath = path.join(this.transcriptsDir, relativePath);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const entryTime = new Date(entry.timestamp);\n\n // Check if entry is within time range (half-open: exclusive end)\n if (entryTime >= start && entryTime < end) {\n // Filter by sessionKey if provided\n if (!sessionKey || entry.sessionKey === sessionKey) {\n if (keepRawLine && !keepRawLine(line)) continue;\n entries.push(entry);\n }\n }\n } catch {\n // Skip malformed lines\n log.debug(`skipped malformed transcript line in ${relativePath}`);\n }\n }\n } catch {\n // File doesn't exist or is unreadable - skip\n }\n }\n\n // Sort by timestamp\n entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n\n log.debug(`read ${entries.length} transcript entries from ${transcriptFiles.length} file(s)`);\n return entries;\n } catch (err) {\n log.error(\"failed to read transcript range:\", err);\n return [];\n }\n }\n\n /**\n * Read the last N hours of transcript.\n *\n * Fast path: when sessionKey is given, reads only the 1-2 daily files for that\n * specific channel instead of scanning all 95+ transcript files across all channels.\n */\n async readRecent(hours: number, sessionKey?: string): Promise<TranscriptEntry[]> {\n const now = new Date();\n // The recent-read window is inclusive of \"now\": a turn that is buffered and\n // then immediately read back can carry the same millisecond timestamp as\n // this read, so a half-open [start, now) filter would drop it. Extend the\n // upper bound by 1ms so the exclusive [start, end) filters in\n // readRecentForSession / readRange (which stay half-open for\n // caller-specified ranges, CLAUDE.md rule #35) still capture an entry\n // stamped at exactly \"now\". This is the only place the \"up to now\" boundary\n // is widened; explicit ranges are unaffected.\n const end = new Date(now.getTime() + 1);\n const start = new Date(now.getTime() - hours * 60 * 60 * 1000);\n\n if (sessionKey) {\n return this.readRecentForSession(start, end, sessionKey);\n }\n return this.readRange(start.toISOString(), end.toISOString(), undefined);\n }\n\n /**\n * Optimized read for a specific session: only looks in that session's channel\n * directory and only reads files whose date falls within the lookback window.\n */\n private async readRecentForSession(\n start: Date,\n end: Date,\n sessionKey: string,\n ): Promise<TranscriptEntry[]> {\n const { dir, alternateDir, legacyDir, readbackDirs } = this.getTranscriptPath(sessionKey);\n\n // Build set of date strings that overlap with [start, end].\n // Always include end's date to handle midnight-crossing lookbacks\n // (e.g. start=23:30 yesterday, end=00:30 today).\n const dateStrings = new Set<string>();\n const cursor = new Date(start);\n while (cursor <= end) {\n dateStrings.add(cursor.toISOString().slice(0, 10));\n cursor.setDate(cursor.getDate() + 1);\n }\n dateStrings.add(end.toISOString().slice(0, 10));\n\n const entries: TranscriptEntry[] = [];\n const files = await this.getSessionStorageFiles(\n this.transcriptsDir,\n dir,\n legacyDir,\n alternateDir,\n readbackDirs,\n );\n\n // Dedup identical raw rows that a partially-applied migration may have left\n // in both the primary dir and a read-back dir (issue #1496, cursor review).\n const keepRawLine = makeRawLineDeduper();\n for (const file of files) {\n // Only read files whose date is within the window\n const dateStr = file.name.slice(0, 10);\n if (!dateStrings.has(dateStr)) continue;\n\n try {\n const content = await readFile(file.path, \"utf-8\");\n for (const line of content.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const ts = new Date(entry.timestamp);\n // Half-open window: exclusive end. readRecent widens its own end by\n // 1ms so a just-written \"now\" entry is still captured here without\n // relaxing this bound for direct callers (tests/transcript-boundary).\n if (ts >= start && ts < end && entry.sessionKey === sessionKey && keepRawLine(line)) {\n entries.push(entry);\n }\n } catch {\n // skip malformed line\n }\n }\n } catch {\n // skip unreadable file\n }\n }\n\n entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n log.debug(`readRecentForSession: ${entries.length} entries from ${files.length} file(s) in ${dir}`);\n return entries;\n }\n\n /**\n * Cleanup old transcript entries that are older than retentionDays.\n * For hierarchical structure, reads each file and rewrites without old entries.\n * Legacy flat files are deleted if their date is older than retentionDays.\n * Returns the number of files processed (cleaned or deleted).\n */\n async cleanup(retentionDays: number): Promise<number> {\n if (retentionDays <= 0) {\n log.warn(\"cleanup called with invalid retentionDays:\", retentionDays);\n return 0;\n }\n\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - retentionDays);\n cutoff.setHours(0, 0, 0, 0);\n\n let processed = 0;\n\n try {\n const entries = await readdir(this.transcriptsDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n // This is a channel type directory (discord, slack, cron, main, etc.)\n const channelTypeDir = path.join(this.transcriptsDir, entry.name);\n try {\n const channelTypeEntries = await readdir(channelTypeDir, { withFileTypes: true });\n\n for (const channelTypeEntry of channelTypeEntries) {\n if (channelTypeEntry.isDirectory()) {\n // This is a channel ID directory - contains daily transcript files\n const channelDir = path.join(channelTypeDir, channelTypeEntry.name);\n try {\n const channelFiles = await readdir(channelDir);\n for (const file of channelFiles) {\n if (!file.endsWith(\".jsonl\")) continue;\n\n const filePath = path.join(channelDir, file);\n\n // Check if file is a daily transcript file (YYYY-MM-DD.jsonl)\n if (this.isLegacyTranscriptFile(file)) {\n const dateStr = file.slice(0, 10);\n const fileDate = new Date(dateStr);\n\n if (!isNaN(fileDate.getTime()) && fileDate < cutoff) {\n try {\n await unlink(filePath);\n processed++;\n log.debug(`deleted old daily transcript file: ${entry.name}/${channelTypeEntry.name}/${file}`);\n } catch (err) {\n log.error(`failed to delete transcript file ${filePath}:`, err);\n }\n }\n } else {\n // Legacy file in new structure - clean up old entries\n const cleaned = await this.cleanupTranscriptFile(filePath, cutoff);\n if (cleaned) {\n processed++;\n }\n }\n }\n } catch (err) {\n log.debug(`failed to process channel directory ${entry.name}/${channelTypeEntry.name}:`, err);\n }\n } else if (channelTypeEntry.isFile() && channelTypeEntry.name.endsWith(\".jsonl\")) {\n // Legacy: channel type dir contains .jsonl files directly\n const filePath = path.join(channelTypeDir, channelTypeEntry.name);\n const cleaned = await this.cleanupTranscriptFile(filePath, cutoff);\n if (cleaned) {\n processed++;\n }\n }\n }\n } catch (err) {\n log.debug(`failed to process channel type directory ${entry.name}:`, err);\n }\n } else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) {\n // Handle legacy flat files - delete if older than retentionDays\n if (this.isLegacyTranscriptFile(entry.name)) {\n const dateStr = entry.name.slice(0, 10);\n const fileDate = new Date(dateStr);\n\n if (!isNaN(fileDate.getTime()) && fileDate < cutoff) {\n const filePath = path.join(this.transcriptsDir, entry.name);\n try {\n await unlink(filePath);\n processed++;\n log.debug(`deleted old legacy transcript file: ${entry.name}`);\n } catch (err) {\n log.error(`failed to delete legacy transcript file ${entry.name}:`, err);\n }\n }\n }\n }\n }\n\n if (processed > 0) {\n log.info(`cleaned up ${processed} transcript file(s) older than ${retentionDays} days`);\n }\n\n return processed;\n } catch (err) {\n log.error(\"failed to cleanup old transcripts:\", err);\n return 0;\n }\n }\n\n /**\n * Clean up old entries from a single transcript file.\n * Reads the file, filters out entries older than cutoff, and rewrites if needed.\n * Returns true if the file was processed (cleaned or deleted).\n */\n private async cleanupTranscriptFile(filePath: string, cutoff: Date): Promise<boolean> {\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n\n const validLines: string[] = [];\n let hasOldEntries = false;\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line) as TranscriptEntry;\n const entryTime = new Date(entry.timestamp);\n\n if (entryTime >= cutoff) {\n validLines.push(line);\n } else {\n hasOldEntries = true;\n }\n } catch {\n // Keep malformed lines to avoid data loss\n validLines.push(line);\n }\n }\n\n if (validLines.length === 0) {\n // No valid entries left, delete the file\n try {\n await unlink(filePath);\n log.debug(`deleted empty transcript file: ${filePath}`);\n return true;\n } catch (err) {\n log.error(`failed to delete empty transcript file ${filePath}:`, err);\n return false;\n }\n }\n\n if (hasOldEntries) {\n // Rewrite file without old entries\n await writeFile(filePath, validLines.join(\"\\n\") + \"\\n\", \"utf-8\");\n log.debug(`cleaned old entries from transcript file: ${filePath}`);\n return true;\n }\n\n // No old entries found, no action needed\n return false;\n } catch (err) {\n // File doesn't exist or is unreadable\n return false;\n }\n }\n\n /**\n * Save a checkpoint to preserve conversation context.\n * Called when compaction is detected.\n */\n async saveCheckpoint(checkpoint: Checkpoint): Promise<void> {\n try {\n await writeFile(this.checkpointPath, JSON.stringify(checkpoint, null, 2), \"utf-8\");\n log.info(`saved checkpoint for session ${checkpoint.sessionKey} with ${checkpoint.turns.length} turn(s)`);\n } catch (err) {\n log.error(\"failed to save checkpoint:\", err);\n throw err;\n }\n }\n\n /**\n * Load a checkpoint if one exists and is not expired.\n * Returns null if no checkpoint exists or if it has expired.\n */\n async loadCheckpoint(sessionKey?: string): Promise<Checkpoint | null> {\n try {\n const raw = await readFile(this.checkpointPath, \"utf-8\");\n const checkpoint = JSON.parse(raw) as Checkpoint;\n\n // Validate checkpoint structure\n if (!checkpoint.sessionKey || !checkpoint.capturedAt || !checkpoint.ttl || !Array.isArray(checkpoint.turns)) {\n log.warn(\"checkpoint file has invalid structure\");\n return null;\n }\n\n // Check if checkpoint is for the requested session (if specified)\n if (sessionKey && checkpoint.sessionKey !== sessionKey) {\n log.debug(`checkpoint session mismatch: ${checkpoint.sessionKey} vs ${sessionKey}`);\n return null;\n }\n\n // Check if checkpoint has expired\n const ttl = new Date(checkpoint.ttl);\n if (isNaN(ttl.getTime())) {\n log.warn(\"checkpoint has invalid TTL format\");\n return null;\n }\n\n if (ttl < new Date()) {\n log.info(`checkpoint expired at ${checkpoint.ttl}`);\n return null;\n }\n\n log.info(`loaded checkpoint with ${checkpoint.turns.length} turn(s), expires at ${checkpoint.ttl}`);\n return checkpoint;\n } catch (err) {\n // File doesn't exist or is unreadable - that's fine\n log.debug(\"no valid checkpoint found\");\n return null;\n }\n }\n\n /**\n * Clear (delete) the checkpoint file.\n * Called after successful injection of checkpoint context.\n */\n async clearCheckpoint(): Promise<void> {\n try {\n await unlink(this.checkpointPath);\n log.info(\"cleared checkpoint\");\n } catch (err) {\n // File doesn't exist - that's fine\n log.debug(\"no checkpoint to clear\");\n }\n }\n\n /**\n * Format entries for recall injection.\n * Returns a formatted string suitable for injecting into agent context.\n *\n * Format:\n * ## Recent Conversation (last X hours)\n * [10:32] User: message content\n * [10:33] Assistant: response content\n *\n * Content is trimmed to approximately maxTokens.\n */\n formatForRecall(entries: TranscriptEntry[], maxTokens: number): string {\n if (entries.length === 0) {\n return \"\";\n }\n\n const maxChars = maxTokens * TranscriptManager.CHARS_PER_TOKEN;\n const lines: string[] = [];\n\n // Calculate time range for header\n const firstEntry = new Date(entries[0].timestamp);\n const lastEntry = new Date(entries[entries.length - 1].timestamp);\n const hoursDiff = Math.round((lastEntry.getTime() - firstEntry.getTime()) / (60 * 60 * 1000));\n\n // Add header\n if (hoursDiff < 1) {\n lines.push(\"## Recent Conversation (last few minutes)\");\n } else {\n lines.push(`## Recent Conversation (last ${hoursDiff} hour${hoursDiff === 1 ? \"\" : \"s\"})`);\n }\n lines.push(\"\");\n\n // Format each entry\n const formattedEntries: string[] = [];\n for (const entry of entries) {\n const time = new Date(entry.timestamp);\n const timeStr = time.toLocaleTimeString(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n const roleLabel = entry.role === \"user\" ? \"User\" : \"Assistant\";\n formattedEntries.push(`[${timeStr}] ${roleLabel}: ${entry.content}`);\n }\n\n // Build output, trimming from the beginning if too long\n // (we want to keep the most recent context)\n let totalChars = lines.join(\"\\n\").length;\n const selectedEntries: string[] = [];\n\n for (let i = formattedEntries.length - 1; i >= 0; i--) {\n const entry = formattedEntries[i];\n const entryChars = entry.length + 1; // +1 for newline\n\n if (totalChars + entryChars > maxChars && selectedEntries.length > 0) {\n // Adding this entry would exceed limit, and we have some entries already\n break;\n }\n\n selectedEntries.unshift(entry);\n totalChars += entryChars;\n }\n\n lines.push(...selectedEntries);\n lines.push(\"\"); // Trailing newline\n\n const result = lines.join(\"\\n\");\n log.debug(`formatted ${selectedEntries.length}/${entries.length} transcript entries for recall (~${result.length} chars)`);\n\n return result;\n }\n\n /**\n * Create a checkpoint from the current buffer state.\n * Helper method for creating checkpoints before compaction.\n */\n createCheckpoint(sessionKey: string, turns: TranscriptEntry[], ttlHours?: number): Checkpoint {\n const ttl = ttlHours ?? TranscriptManager.DEFAULT_CHECKPOINT_TTL_HOURS;\n const expiresAt = new Date();\n expiresAt.setHours(expiresAt.getHours() + ttl);\n\n return {\n sessionKey,\n capturedAt: new Date().toISOString(),\n turns: [...turns], // Copy turns to avoid mutation\n ttl: expiresAt.toISOString(),\n };\n }\n\n /**\n * Get statistics about stored transcripts.\n * Returns counts from the hierarchical directory structure.\n */\n async getStats(): Promise<{\n totalFiles: number;\n totalEntries: number;\n oldestFile: string | null;\n newestFile: string | null;\n channelTypes: Record<string, number>;\n }> {\n try {\n const allFiles = await this.getAllTranscriptFiles();\n\n if (allFiles.length === 0) {\n return {\n totalFiles: 0,\n totalEntries: 0,\n oldestFile: null,\n newestFile: null,\n channelTypes: {},\n };\n }\n\n // Sort files by path\n const sortedFiles = allFiles.sort();\n\n let totalEntries = 0;\n const channelTypes: Record<string, number> = {};\n\n for (const relativePath of allFiles) {\n const filePath = path.join(this.transcriptsDir, relativePath);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n totalEntries += lines.length;\n\n // Count by channel type (first directory in path)\n const channelType = relativePath.includes(path.sep)\n ? relativePath.split(path.sep)[0]\n : \"legacy\";\n channelTypes[channelType] = (channelTypes[channelType] || 0) + 1;\n } catch {\n // Skip unreadable files\n }\n }\n\n return {\n totalFiles: allFiles.length,\n totalEntries,\n oldestFile: sortedFiles[0],\n newestFile: sortedFiles[sortedFiles.length - 1],\n channelTypes,\n };\n } catch (err) {\n log.error(\"failed to get transcript stats:\", err);\n return {\n totalFiles: 0,\n totalEntries: 0,\n oldestFile: null,\n newestFile: null,\n channelTypes: {},\n };\n }\n }\n\n async analyzeIntegrity(): Promise<SessionIntegrityReport> {\n return analyzeSessionIntegrity({ memoryDir: this.config.memoryDir });\n }\n\n async getRecoverySummary(sessionKey?: string): Promise<{\n generatedAt: string;\n sessionKey?: string;\n healthy: boolean;\n issueCount: number;\n incompleteTurns: number;\n brokenChains: number;\n checkpointHealthy: boolean;\n }> {\n const report = await this.analyzeIntegrity();\n const selectedSessions = sessionKey\n ? report.sessions.filter((session) => session.sessionKey === sessionKey)\n : report.sessions;\n const incompleteTurns = selectedSessions.reduce((sum, session) => sum + session.incompleteTurns, 0);\n const brokenChains = selectedSessions.reduce((sum, session) => sum + session.brokenChains, 0);\n const filteredIssues = report.issues.filter((issue) => !sessionKey || issue.sessionKey === sessionKey);\n const issueCount = filteredIssues.length;\n const severeIssueCount = filteredIssues.filter((issue) => issue.severity !== \"info\").length;\n return {\n generatedAt: report.generatedAt,\n sessionKey,\n healthy: sessionKey ? severeIssueCount === 0 && report.checkpoint.healthy : report.healthy,\n issueCount,\n incompleteTurns,\n brokenChains,\n checkpointHealthy: report.checkpoint.healthy,\n };\n }\n}\n"],"mappings":"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","names":[]}
|