@remnic/core 9.3.689 → 9.3.691
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-boundary.d.ts +7 -6
- package/dist/access-boundary.js +19 -18
- package/dist/access-cli.js +111 -47
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +25 -24
- package/dist/access-mcp.d.ts +13 -5
- package/dist/access-mcp.js +22 -21
- package/dist/access-operations.d.ts +13 -7
- package/dist/access-operations.js +23 -20
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DmCHJ4cH.d.ts → access-service-CKnFdxH-.d.ts} +63 -3
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +18 -17
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-surface-catalog.js +2 -0
- package/dist/access-surface-catalog.js.map +1 -1
- 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/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-NUQWSFQD.js → auto-sync-RWXT6P5M.js} +5 -5
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +2 -1
- package/dist/briefing.js +6 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +2 -2
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-COqWZlZ6.d.ts → catalog-BEWLjdOr.d.ts} +2 -2
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +9 -8
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-GTDH3IUH.js → chunk-2D27ODNX.js} +2 -2
- package/dist/{chunk-3K37VPM3.js → chunk-3BRA3N5A.js} +2 -2
- package/dist/{chunk-4FE2K57M.js → chunk-3VVH6ZQW.js} +2 -2
- package/dist/{chunk-FIVDN2SM.js → chunk-4RGWCRIJ.js} +4 -4
- package/dist/{chunk-AGJH5ISO.js → chunk-5KBNNUXS.js} +2 -2
- package/dist/{chunk-OV4D5T7V.js → chunk-66HROLW5.js} +2 -2
- package/dist/{chunk-Z2M6YTAJ.js → chunk-7UVDIWLL.js} +34 -5
- package/dist/chunk-7UVDIWLL.js.map +1 -0
- package/dist/{chunk-QANVLERJ.js → chunk-ADUAF5EV.js} +2 -2
- package/dist/{chunk-LLONI6PY.js → chunk-AFWEUTMS.js} +2 -2
- package/dist/{chunk-W63OY3J7.js → chunk-D3DNGQNQ.js} +3 -3
- package/dist/{chunk-T422SYM6.js → chunk-DCWIQFNA.js} +5 -5
- package/dist/{chunk-NGFEWFNK.js → chunk-DQHZA7Y3.js} +1 -1
- package/dist/chunk-DQHZA7Y3.js.map +1 -0
- package/dist/{chunk-OBXTMFZQ.js → chunk-ECZPWSB5.js} +2 -2
- package/dist/{chunk-BFVPIKDY.js → chunk-GCOSQBQ2.js} +3 -3
- package/dist/{chunk-SAEZIIID.js → chunk-IPJD7WJH.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-MDYG7VI7.js → chunk-IREVVFTM.js} +10 -1
- package/dist/{chunk-MDYG7VI7.js.map → chunk-IREVVFTM.js.map} +1 -1
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-RHXFYIHA.js → chunk-K7I24ZO2.js} +42 -27
- package/dist/chunk-K7I24ZO2.js.map +1 -0
- package/dist/{chunk-XU7363OX.js → chunk-KO6OL4I7.js} +617 -23
- package/dist/chunk-KO6OL4I7.js.map +1 -0
- package/dist/{chunk-UTYBJR7M.js → chunk-MCV2DD6F.js} +2 -2
- package/dist/{chunk-3FC6LW6T.js → chunk-MRFVMSTR.js} +2 -2
- package/dist/{chunk-WDXCNJSF.js → chunk-MXZWKOOR.js} +2 -2
- package/dist/{chunk-QSQW54U5.js → chunk-NCRDCTEJ.js} +33 -10
- package/dist/chunk-NCRDCTEJ.js.map +1 -0
- package/dist/{chunk-6O6A6YUO.js → chunk-NUZ3N445.js} +7 -7
- package/dist/{chunk-UHUZXWDX.js → chunk-OOO7ZKOD.js} +16 -5
- package/dist/chunk-OOO7ZKOD.js.map +1 -0
- package/dist/{chunk-GS55WYRL.js → chunk-QFG23CRE.js} +2 -2
- package/dist/{chunk-UDJLF3BO.js → chunk-QLRYXOAD.js} +2 -2
- package/dist/chunk-QNG35H27.js +2190 -0
- package/dist/chunk-QNG35H27.js.map +1 -0
- package/dist/{chunk-A4HH2EWA.js → chunk-QVG4LAQO.js} +2 -2
- package/dist/{chunk-4N3TFFPH.js → chunk-SAC5V6JT.js} +2 -2
- package/dist/chunk-SVOZFLIQ.js +16 -0
- package/dist/chunk-SVOZFLIQ.js.map +1 -0
- package/dist/{chunk-JZBFL7RI.js → chunk-TJ5IIQIR.js} +468 -2265
- package/dist/chunk-TJ5IIQIR.js.map +1 -0
- package/dist/{chunk-UXLZOVCN.js → chunk-TSGREOYY.js} +38 -6
- package/dist/chunk-TSGREOYY.js.map +1 -0
- package/dist/{chunk-B4XVLHJA.js → chunk-U2OBZSGG.js} +2 -2
- package/dist/{chunk-STOEE37X.js → chunk-UOCLRM74.js} +2 -2
- package/dist/{chunk-A7UZR6RA.js → chunk-UTUYAPLY.js} +14 -1
- package/dist/chunk-UTUYAPLY.js.map +1 -0
- package/dist/{chunk-JPETDXED.js → chunk-UUMYB3FR.js} +2 -2
- package/dist/{chunk-BLIWOONZ.js → chunk-VFVG4NS2.js} +33 -20
- package/dist/chunk-VFVG4NS2.js.map +1 -0
- package/dist/{chunk-OWFY6NGQ.js → chunk-VH2DHBHG.js} +2 -2
- package/dist/chunk-VH2DHBHG.js.map +1 -0
- package/dist/{chunk-WIHPNY65.js → chunk-W4ELOUNX.js} +2 -2
- package/dist/{chunk-VX6OBUDW.js → chunk-WIKNVFBS.js} +2 -2
- package/dist/{chunk-2SNKUSQC.js → chunk-X5J3JZR3.js} +4 -4
- package/dist/{chunk-PH3HOKYW.js → chunk-ZLXGFAML.js} +2 -2
- package/dist/{cli-D8nZ2MPH.d.ts → cli-CrSW_KRq.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +40 -38
- package/dist/compounding/engine.d.ts +2 -1
- package/dist/compounding/engine.js +6 -5
- package/dist/compounding/preference-consolidator.d.ts +1 -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 +6 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +6 -5
- package/dist/consolidation-provenance-check.d.ts +2 -1
- package/dist/consolidation-undo.d.ts +2 -1
- package/dist/contradiction/index.d.ts +2 -1
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- 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/day-summary.d.ts +1 -1
- package/dist/delinearize.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 -1
- package/dist/entity-retrieval.js +6 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/event-time.d.ts +35 -0
- package/dist/event-time.js +325 -0
- package/dist/event-time.js.map +1 -0
- package/dist/explicit-capture.d.ts +5 -4
- 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-judge.js +3 -3
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +10 -9
- package/dist/index.js +78 -75
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +6 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -5
- package/dist/maintenance/rebuild-memory-projection.js +7 -6
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- 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-worth-outcomes.d.ts +2 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -2
- package/dist/namespaces/migrate.js +11 -10
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +3 -2
- package/dist/namespaces/storage.js +6 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -1
- package/dist/operator-toolkit.js +15 -13
- package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-DvswGK76.d.ts} +147 -3
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +37 -35
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.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 +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- 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/schemas.d.ts +10 -10
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +3 -3
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-CWch5uM7.d.ts → semantic-consolidation-D5U0Mrdb.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +7 -6
- package/dist/semantic-rule-promotion.js +6 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +6 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +6 -28
- package/dist/storage.js +8 -6
- package/dist/structured-attributes.d.ts +29 -0
- package/dist/structured-attributes.js +8 -0
- package/dist/structured-attributes.js.map +1 -0
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -1
- package/dist/temporal-supersession.js +2 -2
- package/dist/temporal-validity.d.ts +22 -2
- package/dist/temporal-validity.js +3 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/{types-Dm5xxVrr.d.ts → types-2NoBoRBM.d.ts} +38 -0
- 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 +6 -5
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -1
- package/src/access-cli.test.ts +40 -0
- package/src/access-cli.ts +90 -2
- package/src/access-http.ts +35 -6
- package/src/access-mcp.ts +34 -0
- package/src/access-operations.ts +45 -0
- package/src/access-service.ts +60 -0
- package/src/access-surface-catalog.test.ts +1 -1
- package/src/access-surface-catalog.ts +2 -0
- package/src/bitemporal-frontmatter.test.ts +177 -0
- package/src/cli.ts +18 -0
- package/src/coding/architecture-card.test.ts +544 -0
- package/src/coding/architecture-card.ts +687 -0
- package/src/coding/architecture-surfaces.test.ts +579 -0
- package/src/coding/architecture-surfaces.ts +457 -0
- package/src/config.ts +13 -0
- package/src/event-time.test.ts +514 -0
- package/src/event-time.ts +548 -0
- package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
- package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
- package/src/maintenance/namespace-planner.ts +74 -16
- package/src/operator-toolkit.ts +25 -0
- package/src/orchestrator.ts +171 -1
- package/src/storage.ts +40 -20
- package/src/structured-attributes.ts +39 -0
- package/src/temporal-validity.ts +34 -0
- package/src/types.ts +38 -0
- package/dist/chunk-A7UZR6RA.js.map +0 -1
- package/dist/chunk-BLIWOONZ.js.map +0 -1
- package/dist/chunk-JZBFL7RI.js.map +0 -1
- package/dist/chunk-NGFEWFNK.js.map +0 -1
- package/dist/chunk-OWFY6NGQ.js.map +0 -1
- package/dist/chunk-QSQW54U5.js.map +0 -1
- package/dist/chunk-RHXFYIHA.js.map +0 -1
- package/dist/chunk-UHUZXWDX.js.map +0 -1
- package/dist/chunk-UXLZOVCN.js.map +0 -1
- package/dist/chunk-XU7363OX.js.map +0 -1
- package/dist/chunk-Z2M6YTAJ.js.map +0 -1
- /package/dist/{auto-sync-NUQWSFQD.js.map → auto-sync-RWXT6P5M.js.map} +0 -0
- /package/dist/{chunk-GTDH3IUH.js.map → chunk-2D27ODNX.js.map} +0 -0
- /package/dist/{chunk-3K37VPM3.js.map → chunk-3BRA3N5A.js.map} +0 -0
- /package/dist/{chunk-4FE2K57M.js.map → chunk-3VVH6ZQW.js.map} +0 -0
- /package/dist/{chunk-FIVDN2SM.js.map → chunk-4RGWCRIJ.js.map} +0 -0
- /package/dist/{chunk-AGJH5ISO.js.map → chunk-5KBNNUXS.js.map} +0 -0
- /package/dist/{chunk-OV4D5T7V.js.map → chunk-66HROLW5.js.map} +0 -0
- /package/dist/{chunk-QANVLERJ.js.map → chunk-ADUAF5EV.js.map} +0 -0
- /package/dist/{chunk-LLONI6PY.js.map → chunk-AFWEUTMS.js.map} +0 -0
- /package/dist/{chunk-W63OY3J7.js.map → chunk-D3DNGQNQ.js.map} +0 -0
- /package/dist/{chunk-T422SYM6.js.map → chunk-DCWIQFNA.js.map} +0 -0
- /package/dist/{chunk-OBXTMFZQ.js.map → chunk-ECZPWSB5.js.map} +0 -0
- /package/dist/{chunk-BFVPIKDY.js.map → chunk-GCOSQBQ2.js.map} +0 -0
- /package/dist/{chunk-SAEZIIID.js.map → chunk-IPJD7WJH.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-UTYBJR7M.js.map → chunk-MCV2DD6F.js.map} +0 -0
- /package/dist/{chunk-3FC6LW6T.js.map → chunk-MRFVMSTR.js.map} +0 -0
- /package/dist/{chunk-WDXCNJSF.js.map → chunk-MXZWKOOR.js.map} +0 -0
- /package/dist/{chunk-6O6A6YUO.js.map → chunk-NUZ3N445.js.map} +0 -0
- /package/dist/{chunk-GS55WYRL.js.map → chunk-QFG23CRE.js.map} +0 -0
- /package/dist/{chunk-UDJLF3BO.js.map → chunk-QLRYXOAD.js.map} +0 -0
- /package/dist/{chunk-A4HH2EWA.js.map → chunk-QVG4LAQO.js.map} +0 -0
- /package/dist/{chunk-4N3TFFPH.js.map → chunk-SAC5V6JT.js.map} +0 -0
- /package/dist/{chunk-B4XVLHJA.js.map → chunk-U2OBZSGG.js.map} +0 -0
- /package/dist/{chunk-STOEE37X.js.map → chunk-UOCLRM74.js.map} +0 -0
- /package/dist/{chunk-JPETDXED.js.map → chunk-UUMYB3FR.js.map} +0 -0
- /package/dist/{chunk-WIHPNY65.js.map → chunk-W4ELOUNX.js.map} +0 -0
- /package/dist/{chunk-VX6OBUDW.js.map → chunk-WIKNVFBS.js.map} +0 -0
- /package/dist/{chunk-2SNKUSQC.js.map → chunk-X5J3JZR3.js.map} +0 -0
- /package/dist/{chunk-PH3HOKYW.js.map → chunk-ZLXGFAML.js.map} +0 -0
|
@@ -4,10 +4,7 @@ import {
|
|
|
4
4
|
loadSyncState,
|
|
5
5
|
syncWearableSource,
|
|
6
6
|
wearableSourceLabel
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import {
|
|
9
|
-
migrateFromEngram
|
|
10
|
-
} from "./chunk-7HYPN2GC.js";
|
|
7
|
+
} from "./chunk-5KBNNUXS.js";
|
|
11
8
|
import {
|
|
12
9
|
ensureContradictionScanCron,
|
|
13
10
|
ensureDaySummaryCron,
|
|
@@ -17,9 +14,12 @@ import {
|
|
|
17
14
|
ensureProceduralMiningCron,
|
|
18
15
|
graphEdgeDecayCadenceToCronExpr
|
|
19
16
|
} from "./chunk-666A3MOW.js";
|
|
17
|
+
import {
|
|
18
|
+
migrateFromEngram
|
|
19
|
+
} from "./chunk-7HYPN2GC.js";
|
|
20
20
|
import {
|
|
21
21
|
LcmEngine
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-X5J3JZR3.js";
|
|
23
23
|
import {
|
|
24
24
|
chunkTranscriptEntries
|
|
25
25
|
} from "./chunk-TECVW3JP.js";
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
CompoundingEngine,
|
|
31
31
|
defaultTierMigrationCycleBudget
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-UOCLRM74.js";
|
|
33
33
|
import {
|
|
34
34
|
SharedContextManager
|
|
35
35
|
} from "./chunk-UFS7OXGL.js";
|
|
@@ -38,6 +38,15 @@ import {
|
|
|
38
38
|
applyUtilityRankingRuntimeDelta,
|
|
39
39
|
loadUtilityRuntimeValues
|
|
40
40
|
} from "./chunk-LXOM6IQU.js";
|
|
41
|
+
import {
|
|
42
|
+
applyTemporalSupersession,
|
|
43
|
+
normalizeSupersessionKey,
|
|
44
|
+
shouldFilterSupersededFromRecall
|
|
45
|
+
} from "./chunk-MRFVMSTR.js";
|
|
46
|
+
import {
|
|
47
|
+
isValidAsOf,
|
|
48
|
+
isValidityExpiredNow
|
|
49
|
+
} from "./chunk-IREVVFTM.js";
|
|
41
50
|
import {
|
|
42
51
|
TierMigrationExecutor
|
|
43
52
|
} from "./chunk-VOUOLGIP.js";
|
|
@@ -52,19 +61,11 @@ import {
|
|
|
52
61
|
} from "./chunk-UHGBNIOS.js";
|
|
53
62
|
import {
|
|
54
63
|
HourlySummarizer
|
|
55
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-QVG4LAQO.js";
|
|
56
65
|
import {
|
|
57
66
|
buildTargetedFactRecallSection,
|
|
58
67
|
shouldRecallTargetedFactEvidence
|
|
59
68
|
} from "./chunk-R5DB26G6.js";
|
|
60
|
-
import {
|
|
61
|
-
applyTemporalSupersession,
|
|
62
|
-
normalizeSupersessionKey,
|
|
63
|
-
shouldFilterSupersededFromRecall
|
|
64
|
-
} from "./chunk-3FC6LW6T.js";
|
|
65
|
-
import {
|
|
66
|
-
isValidAsOf
|
|
67
|
-
} from "./chunk-MDYG7VI7.js";
|
|
68
69
|
import {
|
|
69
70
|
SessionObserverState
|
|
70
71
|
} from "./chunk-DKTSR7EK.js";
|
|
@@ -140,6 +141,18 @@ import {
|
|
|
140
141
|
import {
|
|
141
142
|
classifyMemoryKind
|
|
142
143
|
} from "./chunk-YAZNBMNF.js";
|
|
144
|
+
import {
|
|
145
|
+
ExtractionEngine
|
|
146
|
+
} from "./chunk-MXZWKOOR.js";
|
|
147
|
+
import {
|
|
148
|
+
parseMemoryActionEligibilityContext
|
|
149
|
+
} from "./chunk-ROZJACKP.js";
|
|
150
|
+
import {
|
|
151
|
+
ModelRegistry
|
|
152
|
+
} from "./chunk-PWFWCGOO.js";
|
|
153
|
+
import {
|
|
154
|
+
LocalLlmClient
|
|
155
|
+
} from "./chunk-UPTZYUYJ.js";
|
|
143
156
|
import {
|
|
144
157
|
buildFocusedListRecallSection,
|
|
145
158
|
shouldRecallFocusedListEvidence
|
|
@@ -160,19 +173,7 @@ import {
|
|
|
160
173
|
getVerdictKind,
|
|
161
174
|
judgeFactDurability,
|
|
162
175
|
validateProcedureExtraction
|
|
163
|
-
} from "./chunk-
|
|
164
|
-
import {
|
|
165
|
-
ExtractionEngine
|
|
166
|
-
} from "./chunk-WDXCNJSF.js";
|
|
167
|
-
import {
|
|
168
|
-
parseMemoryActionEligibilityContext
|
|
169
|
-
} from "./chunk-ROZJACKP.js";
|
|
170
|
-
import {
|
|
171
|
-
ModelRegistry
|
|
172
|
-
} from "./chunk-PWFWCGOO.js";
|
|
173
|
-
import {
|
|
174
|
-
LocalLlmClient
|
|
175
|
-
} from "./chunk-UPTZYUYJ.js";
|
|
176
|
+
} from "./chunk-ECZPWSB5.js";
|
|
176
177
|
import {
|
|
177
178
|
tryDirectAnswer
|
|
178
179
|
} from "./chunk-OQH5XUH3.js";
|
|
@@ -183,7 +184,7 @@ import {
|
|
|
183
184
|
buildEntityRecallSection,
|
|
184
185
|
entityRecentTranscriptLookbackHours,
|
|
185
186
|
readRecentEntityTranscriptEntries
|
|
186
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-U2OBZSGG.js";
|
|
187
188
|
import {
|
|
188
189
|
formatDaySummaryMemories
|
|
189
190
|
} from "./chunk-EO5QWINU.js";
|
|
@@ -196,14 +197,14 @@ import {
|
|
|
196
197
|
import {
|
|
197
198
|
compareVerifiedEpisodeResults,
|
|
198
199
|
searchVerifiedEpisodes
|
|
199
|
-
} from "./chunk-
|
|
200
|
+
} from "./chunk-3VVH6ZQW.js";
|
|
200
201
|
import {
|
|
201
202
|
ThreadingManager
|
|
202
203
|
} from "./chunk-QRDOSYOR.js";
|
|
203
204
|
import {
|
|
204
205
|
compareVerifiedSemanticRuleResults,
|
|
205
206
|
searchVerifiedSemanticRules
|
|
206
|
-
} from "./chunk-
|
|
207
|
+
} from "./chunk-ZLXGFAML.js";
|
|
207
208
|
import {
|
|
208
209
|
searchWorkProductLedgerEntries
|
|
209
210
|
} from "./chunk-ZRWB5D4H.js";
|
|
@@ -214,10 +215,15 @@ import {
|
|
|
214
215
|
PolicyRuntimeManager
|
|
215
216
|
} from "./chunk-33JBK2XP.js";
|
|
216
217
|
import {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
NamespaceCatalog,
|
|
219
|
+
planNamespaceMaintenance,
|
|
220
|
+
runNamespaceMaintenanceBatchPlan,
|
|
221
|
+
runNamespaceMaintenanceFanout,
|
|
222
|
+
summarizeNamespaceMaintenanceHealth
|
|
223
|
+
} from "./chunk-QNG35H27.js";
|
|
224
|
+
import {
|
|
225
|
+
NamespaceStorageRouter
|
|
226
|
+
} from "./chunk-W4ELOUNX.js";
|
|
221
227
|
import {
|
|
222
228
|
isAboveImportanceThreshold,
|
|
223
229
|
scoreImportance
|
|
@@ -262,7 +268,7 @@ import {
|
|
|
262
268
|
materializeAfterSemanticConsolidation,
|
|
263
269
|
parseConsolidationResponse,
|
|
264
270
|
parseOperatorAwareConsolidationResponse
|
|
265
|
-
} from "./chunk-
|
|
271
|
+
} from "./chunk-QFG23CRE.js";
|
|
266
272
|
import {
|
|
267
273
|
buildBehaviorSignalsForMemory,
|
|
268
274
|
dedupeBehaviorSignalsByMemoryAndHash
|
|
@@ -289,7 +295,7 @@ import {
|
|
|
289
295
|
FallbackLlmClient,
|
|
290
296
|
fallbackLlmRuntimeContextFromConfig,
|
|
291
297
|
gatewayTaskChainOptions
|
|
292
|
-
} from "./chunk-
|
|
298
|
+
} from "./chunk-4RGWCRIJ.js";
|
|
293
299
|
import {
|
|
294
300
|
compileCorrectionRule,
|
|
295
301
|
correctionsFilePath,
|
|
@@ -320,18 +326,18 @@ import {
|
|
|
320
326
|
} from "./chunk-FF4KLI5W.js";
|
|
321
327
|
import {
|
|
322
328
|
buildXraySnapshot
|
|
323
|
-
} from "./chunk-
|
|
329
|
+
} from "./chunk-AFWEUTMS.js";
|
|
324
330
|
import {
|
|
325
331
|
objectiveStateStoreOverrideForNamespace,
|
|
326
332
|
searchObjectiveStateSnapshots
|
|
327
333
|
} from "./chunk-6HMYUWXR.js";
|
|
328
334
|
import {
|
|
329
335
|
NamespaceSearchRouter
|
|
330
|
-
} from "./chunk-
|
|
336
|
+
} from "./chunk-IPJD7WJH.js";
|
|
331
337
|
import {
|
|
332
338
|
createConversationIndexRuntime,
|
|
333
339
|
createSearchBackend
|
|
334
|
-
} from "./chunk-
|
|
340
|
+
} from "./chunk-UUMYB3FR.js";
|
|
335
341
|
import {
|
|
336
342
|
NoopSearchBackend
|
|
337
343
|
} from "./chunk-CYEPCZN5.js";
|
|
@@ -346,12 +352,12 @@ import {
|
|
|
346
352
|
import {
|
|
347
353
|
parseQmdExplain
|
|
348
354
|
} from "./chunk-K43PI6DQ.js";
|
|
349
|
-
import {
|
|
350
|
-
shouldSkipImplicitExtraction
|
|
351
|
-
} from "./chunk-U33LWTQQ.js";
|
|
352
355
|
import {
|
|
353
356
|
ProfilingCollector
|
|
354
357
|
} from "./chunk-PNLCEFE4.js";
|
|
358
|
+
import {
|
|
359
|
+
shouldSkipImplicitExtraction
|
|
360
|
+
} from "./chunk-U33LWTQQ.js";
|
|
355
361
|
import {
|
|
356
362
|
buildProcedurePersistBody
|
|
357
363
|
} from "./chunk-QDW3E4RD.js";
|
|
@@ -363,16 +369,15 @@ import {
|
|
|
363
369
|
} from "./chunk-YN4ZT4CW.js";
|
|
364
370
|
import {
|
|
365
371
|
buildChainFollowupGenerator
|
|
366
|
-
} from "./chunk-
|
|
372
|
+
} from "./chunk-66HROLW5.js";
|
|
367
373
|
import {
|
|
368
374
|
ContentHashIndex,
|
|
369
375
|
StorageManager,
|
|
370
376
|
compareEntityTimestamps,
|
|
371
377
|
fingerprintEntityStructuredFacts,
|
|
372
378
|
normalizeAttributePairs,
|
|
373
|
-
parseEntityFile
|
|
374
|
-
|
|
375
|
-
} from "./chunk-BLIWOONZ.js";
|
|
379
|
+
parseEntityFile
|
|
380
|
+
} from "./chunk-VFVG4NS2.js";
|
|
376
381
|
import {
|
|
377
382
|
isValidTranscriptDate,
|
|
378
383
|
loadSpeakerRegistry,
|
|
@@ -383,6 +388,9 @@ import {
|
|
|
383
388
|
import {
|
|
384
389
|
assertPathInsideRoot
|
|
385
390
|
} from "./chunk-5GPPACXK.js";
|
|
391
|
+
import {
|
|
392
|
+
stripAttributesSuffix
|
|
393
|
+
} from "./chunk-SVOZFLIQ.js";
|
|
386
394
|
import {
|
|
387
395
|
attachCitation,
|
|
388
396
|
hasCitationForTemplate,
|
|
@@ -390,10 +398,7 @@ import {
|
|
|
390
398
|
} from "./chunk-J6A3CX5N.js";
|
|
391
399
|
import {
|
|
392
400
|
confidenceTier
|
|
393
|
-
} from "./chunk-
|
|
394
|
-
import {
|
|
395
|
-
displayErrorDetail
|
|
396
|
-
} from "./chunk-6KYMPV2O.js";
|
|
401
|
+
} from "./chunk-DQHZA7Y3.js";
|
|
397
402
|
import {
|
|
398
403
|
buildQmdRecallCacheKey,
|
|
399
404
|
getCachedQmdRecall,
|
|
@@ -421,7 +426,6 @@ import {
|
|
|
421
426
|
parseFlexibleIsoTimestamp
|
|
422
427
|
} from "./chunk-P7FMDTKL.js";
|
|
423
428
|
import {
|
|
424
|
-
ALL_CATEGORY_DIRS,
|
|
425
429
|
RECALL_FALLBACK_DIRS,
|
|
426
430
|
categoryDirName
|
|
427
431
|
} from "./chunk-VS2IYZRU.js";
|
|
@@ -455,19 +459,19 @@ import {
|
|
|
455
459
|
} from "./chunk-PVGDJXVK.js";
|
|
456
460
|
|
|
457
461
|
// src/orchestrator.ts
|
|
458
|
-
import
|
|
462
|
+
import path2 from "path";
|
|
459
463
|
import os from "os";
|
|
460
|
-
import { createHash
|
|
464
|
+
import { createHash, randomBytes } from "crypto";
|
|
461
465
|
import { existsSync, readFileSync } from "fs";
|
|
462
466
|
import {
|
|
463
|
-
lstat
|
|
464
|
-
mkdir as
|
|
465
|
-
readdir
|
|
466
|
-
readFile as
|
|
467
|
-
realpath
|
|
468
|
-
stat
|
|
469
|
-
unlink
|
|
470
|
-
writeFile as
|
|
467
|
+
lstat,
|
|
468
|
+
mkdir as mkdir2,
|
|
469
|
+
readdir,
|
|
470
|
+
readFile as readFile2,
|
|
471
|
+
realpath,
|
|
472
|
+
stat,
|
|
473
|
+
unlink,
|
|
474
|
+
writeFile as writeFile2
|
|
471
475
|
} from "fs/promises";
|
|
472
476
|
|
|
473
477
|
// src/procedural/procedure-recall.ts
|
|
@@ -1409,2084 +1413,161 @@ Install it alongside Remnic:
|
|
|
1409
1413
|
* and/or day. Includes pending_review candidates — the whole point of
|
|
1410
1414
|
* review mode is seeing what's queued.
|
|
1411
1415
|
*/
|
|
1412
|
-
async transcriptMemories(options = {}) {
|
|
1413
|
-
if (options.date !== void 0 && !isValidTranscriptDate(options.date)) {
|
|
1414
|
-
throw new WearablesInputError(`invalid date '${options.date}' \u2014 expected YYYY-MM-DD`);
|
|
1415
|
-
}
|
|
1416
|
-
if (options.source !== void 0) assertValidSourceId(options.source);
|
|
1417
|
-
const limit = clampLimit(
|
|
1418
|
-
options.limit,
|
|
1419
|
-
MEMORY_LIST_DEFAULT_LIMIT,
|
|
1420
|
-
MEMORY_LIST_MAX_LIMIT,
|
|
1421
|
-
"limit"
|
|
1422
|
-
);
|
|
1423
|
-
const storage = await this.deps.getStorage();
|
|
1424
|
-
const memories = await storage.readAllMemories();
|
|
1425
|
-
const results = [];
|
|
1426
|
-
for (const memory of memories) {
|
|
1427
|
-
const source = memory.frontmatter.source;
|
|
1428
|
-
if (typeof source !== "string" || !source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`)) {
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
const attrs = memory.frontmatter.structuredAttributes ?? {};
|
|
1432
|
-
const sourceId = attrs.wearableSource;
|
|
1433
|
-
if (options.source !== void 0) {
|
|
1434
|
-
if (sourceId !== options.source && source !== wearableSourceLabel(options.source) && source !== `${wearableSourceLabel(options.source)}:native`) {
|
|
1435
|
-
continue;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
if (options.date !== void 0 && attrs.wearableDate !== options.date) {
|
|
1439
|
-
continue;
|
|
1440
|
-
}
|
|
1441
|
-
results.push({
|
|
1442
|
-
id: memory.frontmatter.id,
|
|
1443
|
-
source: sourceId ?? source,
|
|
1444
|
-
date: attrs.wearableDate,
|
|
1445
|
-
conversationId: attrs.wearableConversationId,
|
|
1446
|
-
status: memory.frontmatter.status,
|
|
1447
|
-
content: memory.content,
|
|
1448
|
-
created: memory.frontmatter.created
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
results.sort((a, b) => {
|
|
1452
|
-
if (a.created > b.created) return -1;
|
|
1453
|
-
if (a.created < b.created) return 1;
|
|
1454
|
-
if (a.id < b.id) return -1;
|
|
1455
|
-
if (a.id > b.id) return 1;
|
|
1456
|
-
return 0;
|
|
1457
|
-
});
|
|
1458
|
-
return results.slice(0, limit);
|
|
1459
|
-
}
|
|
1460
|
-
// -- speakers -------------------------------------------------------------
|
|
1461
|
-
async listSpeakers() {
|
|
1462
|
-
const storage = await this.deps.getStorage();
|
|
1463
|
-
return loadSpeakerRegistry(storage.dir);
|
|
1464
|
-
}
|
|
1465
|
-
async setSpeaker(sourceId, speakerKey, name, opts = {}) {
|
|
1466
|
-
if (typeof name !== "string" || name.trim().length === 0) {
|
|
1467
|
-
throw new WearablesInputError("speaker name must be a non-empty string");
|
|
1468
|
-
}
|
|
1469
|
-
if (typeof speakerKey !== "string" || speakerKey.trim().length === 0) {
|
|
1470
|
-
throw new WearablesInputError("speaker key must be a non-empty string");
|
|
1471
|
-
}
|
|
1472
|
-
const storage = await this.deps.getStorage();
|
|
1473
|
-
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1474
|
-
registry.speakers[speakerRegistryKey(sourceId, speakerKey.trim())] = {
|
|
1475
|
-
name: name.trim(),
|
|
1476
|
-
...opts.isSelf === true ? { isSelf: true } : {},
|
|
1477
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1478
|
-
};
|
|
1479
|
-
await saveSpeakerRegistry(storage.dir, registry);
|
|
1480
|
-
return registry;
|
|
1481
|
-
}
|
|
1482
|
-
async setSelfName(name) {
|
|
1483
|
-
if (typeof name !== "string" || name.trim().length === 0) {
|
|
1484
|
-
throw new WearablesInputError("self name must be a non-empty string");
|
|
1485
|
-
}
|
|
1486
|
-
const storage = await this.deps.getStorage();
|
|
1487
|
-
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1488
|
-
registry.selfName = name.trim();
|
|
1489
|
-
await saveSpeakerRegistry(storage.dir, registry);
|
|
1490
|
-
return registry;
|
|
1491
|
-
}
|
|
1492
|
-
async removeSpeaker(sourceId, speakerKey) {
|
|
1493
|
-
const storage = await this.deps.getStorage();
|
|
1494
|
-
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1495
|
-
const key = speakerRegistryKey(sourceId, speakerKey.trim());
|
|
1496
|
-
if (!(key in registry.speakers)) {
|
|
1497
|
-
throw new WearablesInputError(`no speaker override stored for '${key}'`);
|
|
1498
|
-
}
|
|
1499
|
-
delete registry.speakers[key];
|
|
1500
|
-
await saveSpeakerRegistry(storage.dir, registry);
|
|
1501
|
-
return registry;
|
|
1502
|
-
}
|
|
1503
|
-
// -- corrections ----------------------------------------------------------
|
|
1504
|
-
async listCorrections() {
|
|
1505
|
-
const storage = await this.deps.getStorage();
|
|
1506
|
-
return {
|
|
1507
|
-
fromConfig: this.deps.config.corrections,
|
|
1508
|
-
fromState: await loadCorrectionsFile(storage.dir),
|
|
1509
|
-
stateFilePath: correctionsFilePath(storage.dir)
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
async addCorrection(rule) {
|
|
1513
|
-
compileCorrectionRule(rule, "correction");
|
|
1514
|
-
const storage = await this.deps.getStorage();
|
|
1515
|
-
const rules = await loadCorrectionsFile(storage.dir);
|
|
1516
|
-
const duplicate = rules.some(
|
|
1517
|
-
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
1518
|
-
);
|
|
1519
|
-
if (duplicate) {
|
|
1520
|
-
throw new WearablesInputError(
|
|
1521
|
-
`an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
rules.push(rule);
|
|
1525
|
-
await saveCorrectionsFile(storage.dir, rules);
|
|
1526
|
-
}
|
|
1527
|
-
async removeCorrection(index) {
|
|
1528
|
-
if (!Number.isInteger(index) || index < 0) {
|
|
1529
|
-
throw new WearablesInputError(`invalid correction index '${index}'`);
|
|
1530
|
-
}
|
|
1531
|
-
const storage = await this.deps.getStorage();
|
|
1532
|
-
const rules = await loadCorrectionsFile(storage.dir);
|
|
1533
|
-
if (index >= rules.length) {
|
|
1534
|
-
throw new WearablesInputError(
|
|
1535
|
-
`correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
|
|
1536
|
-
);
|
|
1537
|
-
}
|
|
1538
|
-
const [removed] = rules.splice(index, 1);
|
|
1539
|
-
await saveCorrectionsFile(storage.dir, rules);
|
|
1540
|
-
return removed;
|
|
1541
|
-
}
|
|
1542
|
-
};
|
|
1543
|
-
function clampLimit(value, fallback, max, label) {
|
|
1544
|
-
if (value === void 0) return fallback;
|
|
1545
|
-
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
|
|
1546
|
-
throw new WearablesInputError(
|
|
1547
|
-
`invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
return value;
|
|
1551
|
-
}
|
|
1552
|
-
function locateTranscriptPath(hitPath) {
|
|
1553
|
-
const normalized = hitPath.replace(/\\/g, "/");
|
|
1554
|
-
const match = normalized.match(
|
|
1555
|
-
/(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
|
|
1556
|
-
);
|
|
1557
|
-
if (!match) return null;
|
|
1558
|
-
if (!isValidTranscriptDate(match[2])) return null;
|
|
1559
|
-
return { source: match[1], date: match[2] };
|
|
1560
|
-
}
|
|
1561
|
-
function extractSnippet(body, index, matchLength) {
|
|
1562
|
-
const start = Math.max(0, index - 80);
|
|
1563
|
-
const end = Math.min(body.length, index + matchLength + 80);
|
|
1564
|
-
const prefix = start > 0 ? "\u2026" : "";
|
|
1565
|
-
const suffix = end < body.length ? "\u2026" : "";
|
|
1566
|
-
return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// src/namespaces/catalog.ts
|
|
1570
|
-
import path2 from "path";
|
|
1571
|
-
import { randomUUID } from "crypto";
|
|
1572
|
-
import {
|
|
1573
|
-
appendFile,
|
|
1574
|
-
lstat,
|
|
1575
|
-
mkdir as mkdir2,
|
|
1576
|
-
open,
|
|
1577
|
-
readdir,
|
|
1578
|
-
readFile as readFile2,
|
|
1579
|
-
realpath,
|
|
1580
|
-
rename,
|
|
1581
|
-
stat,
|
|
1582
|
-
unlink,
|
|
1583
|
-
utimes,
|
|
1584
|
-
writeFile as writeFile2
|
|
1585
|
-
} from "fs/promises";
|
|
1586
|
-
var NAMESPACE_KINDS = [
|
|
1587
|
-
"default",
|
|
1588
|
-
"self",
|
|
1589
|
-
"shared",
|
|
1590
|
-
"project",
|
|
1591
|
-
"branch",
|
|
1592
|
-
"team-project",
|
|
1593
|
-
"explicit",
|
|
1594
|
-
"legacy"
|
|
1595
|
-
];
|
|
1596
|
-
var NAMESPACE_DISCOVERY_SOURCES = [
|
|
1597
|
-
"config",
|
|
1598
|
-
"write",
|
|
1599
|
-
"read",
|
|
1600
|
-
"scan",
|
|
1601
|
-
"migration"
|
|
1602
|
-
];
|
|
1603
|
-
var CATALOG_FILE = "namespaces.jsonl";
|
|
1604
|
-
var STATE_DIR = "state";
|
|
1605
|
-
var REBUILD_LOCK_FILE = "namespaces.rebuild.lock";
|
|
1606
|
-
var REBUILD_LOCK_STALE_MS = 3e4;
|
|
1607
|
-
var REBUILD_LOCK_MAX_WAIT_MS = 5e3;
|
|
1608
|
-
var REBUILD_LOCK_POLL_MS = 50;
|
|
1609
|
-
var REBUILD_LOCK_HEARTBEAT_MS = 1e4;
|
|
1610
|
-
var MEMORY_DATA_CHILDREN = [
|
|
1611
|
-
...ALL_CATEGORY_DIRS,
|
|
1612
|
-
"entities",
|
|
1613
|
-
"artifacts",
|
|
1614
|
-
"identity",
|
|
1615
|
-
"config",
|
|
1616
|
-
"summaries",
|
|
1617
|
-
"profile.md",
|
|
1618
|
-
"state"
|
|
1619
|
-
];
|
|
1620
|
-
function isCatalogEnabled(config) {
|
|
1621
|
-
if (config.namespacesEnabled !== true) return false;
|
|
1622
|
-
return config.namespaceCatalogEnabled !== false;
|
|
1623
|
-
}
|
|
1624
|
-
var FILE_MEMORY_DATA_CHILDREN = /* @__PURE__ */ new Set(["profile.md"]);
|
|
1625
|
-
function isNotFoundError(err) {
|
|
1626
|
-
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1627
|
-
}
|
|
1628
|
-
async function inspectMemoryDataMarker(rootDir, child) {
|
|
1629
|
-
const childPath = path2.join(rootDir, child);
|
|
1630
|
-
let entry;
|
|
1631
|
-
try {
|
|
1632
|
-
entry = await lstat(childPath);
|
|
1633
|
-
} catch (err) {
|
|
1634
|
-
return isNotFoundError(err) ? { state: "absent" } : { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1635
|
-
}
|
|
1636
|
-
if (entry.isSymbolicLink()) return { state: "invalid", detail: `${child}: symlink` };
|
|
1637
|
-
if (FILE_MEMORY_DATA_CHILDREN.has(child)) {
|
|
1638
|
-
return entry.isFile() ? { state: "valid" } : { state: "invalid", detail: `${child}: expected file` };
|
|
1639
|
-
}
|
|
1640
|
-
if (!entry.isDirectory()) return { state: "invalid", detail: `${child}: expected directory` };
|
|
1641
|
-
try {
|
|
1642
|
-
const rootReal = await realpath(rootDir);
|
|
1643
|
-
const childReal = await realpath(childPath);
|
|
1644
|
-
return isPathInside(rootReal, childReal) ? { state: "valid" } : { state: "invalid", detail: `${child}: escapes namespace root` };
|
|
1645
|
-
} catch (err) {
|
|
1646
|
-
return { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
async function inspectMemoryDataRoot(rootDir) {
|
|
1650
|
-
let hasData = false;
|
|
1651
|
-
for (const child of MEMORY_DATA_CHILDREN) {
|
|
1652
|
-
const marker = await inspectMemoryDataMarker(rootDir, child);
|
|
1653
|
-
if (marker.state === "invalid") {
|
|
1654
|
-
return { hasData: false, invalidMarker: marker.detail };
|
|
1655
|
-
}
|
|
1656
|
-
if (marker.state === "valid") {
|
|
1657
|
-
hasData = true;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
return { hasData };
|
|
1661
|
-
}
|
|
1662
|
-
async function hasMemoryData(rootDir) {
|
|
1663
|
-
return (await inspectMemoryDataRoot(rootDir)).hasData;
|
|
1664
|
-
}
|
|
1665
|
-
function isValidIsoTimestamp(value) {
|
|
1666
|
-
const ms = Date.parse(value);
|
|
1667
|
-
return Number.isFinite(ms);
|
|
1668
|
-
}
|
|
1669
|
-
function isNamespaceKind(value) {
|
|
1670
|
-
return typeof value === "string" && NAMESPACE_KINDS.includes(value);
|
|
1671
|
-
}
|
|
1672
|
-
function isNamespaceDiscoverySource(value) {
|
|
1673
|
-
return typeof value === "string" && NAMESPACE_DISCOVERY_SOURCES.includes(value);
|
|
1674
|
-
}
|
|
1675
|
-
function coerceRecord(value) {
|
|
1676
|
-
if (typeof value !== "object" || value === null) return null;
|
|
1677
|
-
const v = value;
|
|
1678
|
-
if (typeof v.namespace !== "string") return null;
|
|
1679
|
-
const namespace = normalizeNamespaceIdentity(v.namespace);
|
|
1680
|
-
if (namespace.length === 0) return null;
|
|
1681
|
-
if (typeof v.identityToken !== "string" || v.identityToken.length === 0) return null;
|
|
1682
|
-
const expectedIdentityToken = namespaceIdentityToken(namespace);
|
|
1683
|
-
if (v.identityToken !== expectedIdentityToken) return null;
|
|
1684
|
-
if (typeof v.storageDir !== "string" || v.storageDir.length === 0) return null;
|
|
1685
|
-
if (typeof v.createdAt !== "string" || v.createdAt.length === 0) return null;
|
|
1686
|
-
if (!isValidIsoTimestamp(v.createdAt)) return null;
|
|
1687
|
-
const kind = v.kind === void 0 ? "explicit" : isNamespaceKind(v.kind) ? v.kind : null;
|
|
1688
|
-
if (!kind) return null;
|
|
1689
|
-
const discoveredBy = v.discoveredBy === void 0 ? "scan" : isNamespaceDiscoverySource(v.discoveredBy) ? v.discoveredBy : null;
|
|
1690
|
-
if (!discoveredBy) return null;
|
|
1691
|
-
const record = {
|
|
1692
|
-
namespace,
|
|
1693
|
-
identityToken: expectedIdentityToken,
|
|
1694
|
-
kind,
|
|
1695
|
-
createdAt: v.createdAt,
|
|
1696
|
-
storageDir: v.storageDir,
|
|
1697
|
-
discoveredBy
|
|
1698
|
-
};
|
|
1699
|
-
if (typeof v.principal === "string") record.principal = v.principal;
|
|
1700
|
-
if (typeof v.projectId === "string") record.projectId = v.projectId;
|
|
1701
|
-
if (typeof v.branch === "string") record.branch = v.branch;
|
|
1702
|
-
if (typeof v.parentNamespace === "string") record.parentNamespace = v.parentNamespace;
|
|
1703
|
-
if (typeof v.lastReadAt === "string" && isValidIsoTimestamp(v.lastReadAt)) {
|
|
1704
|
-
record.lastReadAt = v.lastReadAt;
|
|
1705
|
-
}
|
|
1706
|
-
if (typeof v.lastWriteAt === "string" && isValidIsoTimestamp(v.lastWriteAt)) {
|
|
1707
|
-
record.lastWriteAt = v.lastWriteAt;
|
|
1708
|
-
}
|
|
1709
|
-
if (v.lastMaintenanceAt && typeof v.lastMaintenanceAt === "object") {
|
|
1710
|
-
const out = {};
|
|
1711
|
-
for (const [k, val] of Object.entries(v.lastMaintenanceAt)) {
|
|
1712
|
-
if (typeof val === "string" && isValidIsoTimestamp(val)) out[k] = val;
|
|
1713
|
-
}
|
|
1714
|
-
if (Object.keys(out).length > 0) record.lastMaintenanceAt = out;
|
|
1715
|
-
}
|
|
1716
|
-
return record;
|
|
1717
|
-
}
|
|
1718
|
-
function laterIso(a, b) {
|
|
1719
|
-
if (!a) return b;
|
|
1720
|
-
if (!b) return a;
|
|
1721
|
-
const am = Date.parse(a);
|
|
1722
|
-
const bm = Date.parse(b);
|
|
1723
|
-
if (!Number.isFinite(am)) return b;
|
|
1724
|
-
if (!Number.isFinite(bm)) return a;
|
|
1725
|
-
return bm > am ? b : a;
|
|
1726
|
-
}
|
|
1727
|
-
function mergeNewerTouchFields(base, fresh) {
|
|
1728
|
-
const merged = { ...base };
|
|
1729
|
-
const lr = laterIso(base.lastReadAt, fresh.lastReadAt);
|
|
1730
|
-
if (lr) merged.lastReadAt = lr;
|
|
1731
|
-
const lw = laterIso(base.lastWriteAt, fresh.lastWriteAt);
|
|
1732
|
-
if (lw) merged.lastWriteAt = lw;
|
|
1733
|
-
if (base.lastMaintenanceAt || fresh.lastMaintenanceAt) {
|
|
1734
|
-
const jobs = { ...base.lastMaintenanceAt ?? {} };
|
|
1735
|
-
for (const [job, ts] of Object.entries(fresh.lastMaintenanceAt ?? {})) {
|
|
1736
|
-
const latest = laterIso(jobs[job], ts);
|
|
1737
|
-
if (latest) jobs[job] = latest;
|
|
1738
|
-
}
|
|
1739
|
-
if (Object.keys(jobs).length > 0) merged.lastMaintenanceAt = jobs;
|
|
1740
|
-
}
|
|
1741
|
-
return merged;
|
|
1742
|
-
}
|
|
1743
|
-
function serializeRecord(record) {
|
|
1744
|
-
const ordered = {};
|
|
1745
|
-
const source = record;
|
|
1746
|
-
for (const key of Object.keys(source).sort()) {
|
|
1747
|
-
const value = source[key];
|
|
1748
|
-
if (value === void 0) continue;
|
|
1749
|
-
if (key === "lastMaintenanceAt" && value && typeof value === "object") {
|
|
1750
|
-
const sortedJobs = {};
|
|
1751
|
-
for (const jobKey of Object.keys(value).sort()) {
|
|
1752
|
-
sortedJobs[jobKey] = value[jobKey];
|
|
1753
|
-
}
|
|
1754
|
-
ordered[key] = sortedJobs;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
ordered[key] = value;
|
|
1758
|
-
}
|
|
1759
|
-
return JSON.stringify(ordered);
|
|
1760
|
-
}
|
|
1761
|
-
function inferKind(namespace, config) {
|
|
1762
|
-
if (namespace === normalizeNamespaceIdentity(config.defaultNamespace)) return "default";
|
|
1763
|
-
if (namespace === normalizeNamespaceIdentity(config.sharedNamespace)) return "shared";
|
|
1764
|
-
if (config.namespacePolicies.some((p) => normalizeNamespaceIdentity(p.name) === namespace)) {
|
|
1765
|
-
return "explicit";
|
|
1766
|
-
}
|
|
1767
|
-
if (/-branch-|^project-[^-]+-branch-/.test(namespace) || namespace.includes("-branch-")) {
|
|
1768
|
-
return "branch";
|
|
1769
|
-
}
|
|
1770
|
-
if (/^team-.*-project-/.test(namespace) || /^team-.*project-/.test(namespace)) {
|
|
1771
|
-
return "team-project";
|
|
1772
|
-
}
|
|
1773
|
-
if (/^project-/.test(namespace) || /-project-/.test(namespace)) {
|
|
1774
|
-
return "project";
|
|
1775
|
-
}
|
|
1776
|
-
return "explicit";
|
|
1777
|
-
}
|
|
1778
|
-
var NamespaceCatalog = class {
|
|
1779
|
-
constructor(config) {
|
|
1780
|
-
this.config = config;
|
|
1781
|
-
this.memoryDir = config.memoryDir;
|
|
1782
|
-
this.stateDir = path2.join(this.memoryDir, STATE_DIR);
|
|
1783
|
-
this.catalogPath = path2.join(this.stateDir, CATALOG_FILE);
|
|
1784
|
-
this.rebuildLockPath = path2.join(this.stateDir, REBUILD_LOCK_FILE);
|
|
1785
|
-
this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
1786
|
-
}
|
|
1787
|
-
config;
|
|
1788
|
-
memoryDir;
|
|
1789
|
-
stateDir;
|
|
1790
|
-
catalogPath;
|
|
1791
|
-
rebuildLockPath;
|
|
1792
|
-
// Per-INSTANCE lock owner id (round 6, codex P2 — NBsGP). The rebuild lock
|
|
1793
|
-
// file records this id, not just `process.pid`, so two NamespaceCatalog
|
|
1794
|
-
// instances in the SAME process sharing a memoryDir are NOT mistaken for each
|
|
1795
|
-
// other: a touch on instance B must still wait for instance A's rebuild lock
|
|
1796
|
-
// (different owner id, same PID) instead of skipping as "self-held".
|
|
1797
|
-
lockOwnerId = randomUUID();
|
|
1798
|
-
// Serialized write chain that recovers from rejection (CLAUDE.md rule #40)
|
|
1799
|
-
// so a single failed append cannot permanently poison subsequent writes.
|
|
1800
|
-
writeChain = Promise.resolve();
|
|
1801
|
-
// Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
|
|
1802
|
-
// section, after the lock is acquired but BEFORE the read→merge→append. A
|
|
1803
|
-
// deterministic concurrency test installs a hook here to widen the (otherwise
|
|
1804
|
-
// microscopic) window and prove that a cross-process rebuild CANNOT run its
|
|
1805
|
-
// load→rename while a touch holds the lock. Never set in production code.
|
|
1806
|
-
onTouchCriticalSectionForTest;
|
|
1807
|
-
// Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock
|
|
1808
|
-
// critical section, after the final cross-process re-merge `loadCompacted()` and
|
|
1809
|
-
// BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-
|
|
1810
|
-
// append touch (the old bug) would clobber its append. A deterministic test
|
|
1811
|
-
// installs a hook here to attempt a cross-instance touch in this window and
|
|
1812
|
-
// assert the held mutex blocks it. Never set in production code.
|
|
1813
|
-
onRebuildBeforeRenameForTest;
|
|
1814
|
-
// Test-only seam (NFgCT, codex P2): fires AFTER the lockless disk scan but
|
|
1815
|
-
// BEFORE the rebuild acquires the cross-process file lock for its final
|
|
1816
|
-
// load→merge→rename window. A deterministic test installs a hook here to attempt
|
|
1817
|
-
// a cross-instance touch DURING the scan window and assert it is NOT blocked or
|
|
1818
|
-
// dropped — proving the scan no longer holds the mutex. Never set in production.
|
|
1819
|
-
onRebuildAfterScanForTest;
|
|
1820
|
-
// Test-only seam (NG7Bg, codex P2): fires inside `breakStaleRebuildLock` AFTER it
|
|
1821
|
-
// has judged the lock stale and captured its identity, but BEFORE the final
|
|
1822
|
-
// re-validation+unlink. A deterministic test installs a hook here to REPLACE the
|
|
1823
|
-
// lock file (a fresh holder created a new lock in the race window) and assert the
|
|
1824
|
-
// break is skipped — the replacement's active lock is not deleted. Never set in
|
|
1825
|
-
// production.
|
|
1826
|
-
onBeforeBreakStaleUnlinkForTest;
|
|
1827
|
-
// Normalized (trimmed) default namespace identity (NH-FH, cursor Medium).
|
|
1828
|
-
// Catalog records key namespaces by their NORMALIZED identity
|
|
1829
|
-
// (`normalizeNamespaceIdentity`), but several default-namespace exemptions and
|
|
1830
|
-
// memoryDir-ownership checks compared against the RAW `config.defaultNamespace`.
|
|
1831
|
-
// If the configured default name carries surrounding whitespace the record key
|
|
1832
|
-
// is trimmed while the comparison string is not, so the default row is
|
|
1833
|
-
// misclassified, dropped at read time, or given the wrong storage root. Compare
|
|
1834
|
-
// against this normalized form everywhere instead.
|
|
1835
|
-
defaultNamespaceIdentity;
|
|
1836
|
-
/** Whether the catalog is active (namespaces enabled and catalog not opted out). */
|
|
1837
|
-
get enabled() {
|
|
1838
|
-
return isCatalogEnabled(this.config);
|
|
1839
|
-
}
|
|
1840
|
-
// ── Public enumeration API ──────────────────────────────────────────────
|
|
1841
|
-
/**
|
|
1842
|
-
* Sanitize a record at the enumeration boundary (round 5, cursor Medium + codex
|
|
1843
|
-
* P2; round 6 — NDXHe). Reads return whatever is in `namespaces.jsonl` after
|
|
1844
|
-
* schema checks only, so a tampered or pre-fix row could surface unsafe data to
|
|
1845
|
-
* maintenance/QMD until a rewrite occurs. Two distinct defenses:
|
|
1846
|
-
*
|
|
1847
|
-
* 1. UNSAFE NAMESPACE NAME (NGZqr, codex P2): an unsafe non-default namespace
|
|
1848
|
-
* (e.g. `../evil`, a name with separators, or >64 chars) is REJECTED outright
|
|
1849
|
-
* — return `null` so the caller drops it. The disk SCAN and the hot touch
|
|
1850
|
-
* path both reject such names with the SAME default-exempt `isSafeRouteNamespace`
|
|
1851
|
-
* gate, so the read boundary MUST agree, or `listNamespaces()`/`getNamespaceRecord()`
|
|
1852
|
-
* would expose a namespace those paths reject (note `isStorageDirForNamespace`
|
|
1853
|
-
* can still build a tokenized root even for `../evil`, so storageDir sanitation
|
|
1854
|
-
* alone does not catch it). The default namespace is exempt (it may be a
|
|
1855
|
-
* non-route literal), matching every other validation site.
|
|
1856
|
-
*
|
|
1857
|
-
* 2. UNSAFE storageDir: for an otherwise-valid namespace, apply the SAME contract
|
|
1858
|
-
* as the write path — full containment (`isContainedStorageDir`: lexical +
|
|
1859
|
-
* symlink/realpath) AND namespace ownership (`isStorageDirForNamespace`). When
|
|
1860
|
-
* a record fails EITHER check we substitute the trusted resolved-and-safe root
|
|
1861
|
-
* for that namespace (rule 42: read and write stay symmetric).
|
|
1862
|
-
*/
|
|
1863
|
-
async sanitizeRecordForRead(record) {
|
|
1864
|
-
if (record.namespace !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(record.namespace)) {
|
|
1865
|
-
return null;
|
|
1866
|
-
}
|
|
1867
|
-
if (await this.isContainedStorageDir(record.storageDir) && await this.isStorageDirForNamespace(record.namespace, record.storageDir)) {
|
|
1868
|
-
return record;
|
|
1869
|
-
}
|
|
1870
|
-
const safe = await this.resolveSafeStorageDir(record.namespace);
|
|
1871
|
-
return { ...record, storageDir: safe };
|
|
1872
|
-
}
|
|
1873
|
-
storageRootOwnershipRank(record, resolvedStorageDir, configured) {
|
|
1874
|
-
if (resolvedStorageDir === path2.resolve(this.memoryDir)) {
|
|
1875
|
-
return record.namespace === this.defaultNamespaceIdentity ? 0 : 3;
|
|
1876
|
-
}
|
|
1877
|
-
const leaf = path2.basename(resolvedStorageDir);
|
|
1878
|
-
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
1879
|
-
if (tokenOwnsRoot && configured.has(record.namespace)) {
|
|
1880
|
-
return 0;
|
|
1881
|
-
}
|
|
1882
|
-
if (record.namespace === leaf) return 1;
|
|
1883
|
-
if (tokenOwnsRoot) return 2;
|
|
1884
|
-
return 3;
|
|
1885
|
-
}
|
|
1886
|
-
configuredNamespaceIdentities() {
|
|
1887
|
-
return new Set(
|
|
1888
|
-
[
|
|
1889
|
-
this.config.defaultNamespace,
|
|
1890
|
-
this.config.sharedNamespace,
|
|
1891
|
-
...this.config.namespacePolicies.map((p) => p.name)
|
|
1892
|
-
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
preferStorageRootOwner(current, candidate, resolvedStorageDir, configured) {
|
|
1896
|
-
const currentRank = this.storageRootOwnershipRank(current, resolvedStorageDir, configured);
|
|
1897
|
-
const candidateRank = this.storageRootOwnershipRank(candidate, resolvedStorageDir, configured);
|
|
1898
|
-
if (candidateRank < currentRank) return candidate;
|
|
1899
|
-
if (candidateRank > currentRank) return current;
|
|
1900
|
-
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
1901
|
-
if (byName < 0) return candidate;
|
|
1902
|
-
if (byName > 0) return current;
|
|
1903
|
-
return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
|
|
1904
|
-
}
|
|
1905
|
-
dropDuplicateStorageRootAliases(records) {
|
|
1906
|
-
const byStorageDir = /* @__PURE__ */ new Map();
|
|
1907
|
-
const configured = this.configuredNamespaceIdentities();
|
|
1908
|
-
for (const record of records) {
|
|
1909
|
-
const resolvedStorageDir = path2.resolve(record.storageDir);
|
|
1910
|
-
const current = byStorageDir.get(resolvedStorageDir);
|
|
1911
|
-
if (!current) {
|
|
1912
|
-
byStorageDir.set(resolvedStorageDir, record);
|
|
1913
|
-
continue;
|
|
1914
|
-
}
|
|
1915
|
-
const owner = this.preferStorageRootOwner(current, record, resolvedStorageDir, configured);
|
|
1916
|
-
const alias = owner === current ? record : current;
|
|
1917
|
-
byStorageDir.set(resolvedStorageDir, mergeNewerTouchFields(owner, alias));
|
|
1918
|
-
}
|
|
1919
|
-
return [...byStorageDir.values()];
|
|
1920
|
-
}
|
|
1921
|
-
async loadSanitizedRecords() {
|
|
1922
|
-
const records = await this.loadCompacted();
|
|
1923
|
-
const sanitized = await Promise.all(
|
|
1924
|
-
[...records.values()].map((r) => this.sanitizeRecordForRead(r))
|
|
1925
|
-
);
|
|
1926
|
-
return this.dropDuplicateStorageRootAliases(
|
|
1927
|
-
sanitized.filter((r) => r !== null)
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
1930
|
-
async listNamespaces(filter) {
|
|
1931
|
-
if (!this.enabled) return [];
|
|
1932
|
-
let out = await this.loadSanitizedRecords();
|
|
1933
|
-
if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);
|
|
1934
|
-
if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);
|
|
1935
|
-
if (filter?.writtenSince) {
|
|
1936
|
-
const sinceMs = filter.writtenSince.getTime();
|
|
1937
|
-
out = out.filter((r) => {
|
|
1938
|
-
if (!r.lastWriteAt) return false;
|
|
1939
|
-
const ms = Date.parse(r.lastWriteAt);
|
|
1940
|
-
return Number.isFinite(ms) && ms >= sinceMs;
|
|
1941
|
-
});
|
|
1942
|
-
}
|
|
1943
|
-
return out.sort((a, b) => {
|
|
1944
|
-
const byName = a.namespace.localeCompare(b.namespace);
|
|
1945
|
-
if (byName !== 0) return byName;
|
|
1946
|
-
return a.identityToken.localeCompare(b.identityToken);
|
|
1947
|
-
});
|
|
1948
|
-
}
|
|
1949
|
-
async getNamespaceRecord(namespace) {
|
|
1950
|
-
if (!this.enabled) return null;
|
|
1951
|
-
const ns = normalizeNamespaceIdentity(namespace);
|
|
1952
|
-
return (await this.loadSanitizedRecords()).find((record) => record.namespace === ns) ?? null;
|
|
1953
|
-
}
|
|
1954
|
-
// ── Touch API (cheap, failure-tolerant) ─────────────────────────────────
|
|
1955
|
-
async markRead(namespace, metadata) {
|
|
1956
|
-
await this.touch(namespace, "read", metadata);
|
|
1957
|
-
}
|
|
1958
|
-
async markWrite(namespace, metadata) {
|
|
1959
|
-
await this.touch(namespace, "write", metadata);
|
|
1960
|
-
}
|
|
1961
|
-
async markMaintenance(namespace, jobName, at) {
|
|
1962
|
-
if (typeof jobName !== "string" || jobName.trim().length === 0) {
|
|
1963
|
-
throw new Error("markMaintenance requires a non-empty jobName");
|
|
1964
|
-
}
|
|
1965
|
-
await this.touch(namespace, "maintenance", { at }, jobName.trim());
|
|
1966
|
-
}
|
|
1967
|
-
/**
|
|
1968
|
-
* Register namespaces known purely from config (default, shared, explicit
|
|
1969
|
-
* policies). Source `config`. Cheap and idempotent.
|
|
1970
|
-
*/
|
|
1971
|
-
async registerConfiguredNamespaces() {
|
|
1972
|
-
if (!this.enabled) return;
|
|
1973
|
-
const names = /* @__PURE__ */ new Set([
|
|
1974
|
-
this.config.defaultNamespace,
|
|
1975
|
-
this.config.sharedNamespace,
|
|
1976
|
-
...this.config.namespacePolicies.map((p) => p.name)
|
|
1977
|
-
]);
|
|
1978
|
-
for (const ns of names) {
|
|
1979
|
-
if (!ns) continue;
|
|
1980
|
-
if (normalizeNamespaceIdentity(ns) !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
1981
|
-
continue;
|
|
1982
|
-
}
|
|
1983
|
-
try {
|
|
1984
|
-
await this.register(ns, { discoveredBy: "config" });
|
|
1985
|
-
} catch {
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Register a namespace whose storage was just resolved by the router. Used as
|
|
1991
|
-
* the router's integration hook (`discoveredBy: config`). Storage dir is
|
|
1992
|
-
* provided so we do not re-resolve it. Failure-tolerant. Returns whether the
|
|
1993
|
-
* registration actually APPENDED (round 6, codex P2 — NEFoX), so the router's
|
|
1994
|
-
* resolve-hook dedup only marks a namespace notified when it truly persisted —
|
|
1995
|
-
* a dropped append (disabled catalog or rebuild-lock-timeout drop) returns
|
|
1996
|
-
* `false` and is retried on the next resolve.
|
|
1997
|
-
*/
|
|
1998
|
-
async registerResolved(namespace, storageDir) {
|
|
1999
|
-
if (!this.enabled) return false;
|
|
2000
|
-
return this.register(namespace, { discoveredBy: "config", storageDir });
|
|
2001
|
-
}
|
|
2002
|
-
/**
|
|
2003
|
-
* Generic register/touch without changing read/write timestamps unless the
|
|
2004
|
-
* source implies it. Validates the namespace and resolves a storage dir.
|
|
2005
|
-
* Returns whether the touch actually appended.
|
|
2006
|
-
*/
|
|
2007
|
-
async register(namespace, metadata) {
|
|
2008
|
-
return this.touch(namespace, "register", metadata);
|
|
2009
|
-
}
|
|
2010
|
-
validateNamespace(namespace) {
|
|
2011
|
-
const ns = normalizeNamespaceIdentity(namespace);
|
|
2012
|
-
if (ns.length === 0) throw new Error("empty namespace");
|
|
2013
|
-
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
2014
|
-
throw new Error(`unsafe namespace: ${ns}`);
|
|
2015
|
-
}
|
|
2016
|
-
return ns;
|
|
2017
|
-
}
|
|
2018
|
-
/**
|
|
2019
|
-
* Resolve the on-disk storage dir for a namespace WITHOUT trusting caller
|
|
2020
|
-
* input. The default namespace may use the legacy memoryDir root; everything
|
|
2021
|
-
* else lives under `<memoryDir>/namespaces/<token>`. Containment is enforced
|
|
2022
|
-
* by rejecting separators/parent-refs in the token.
|
|
2023
|
-
*/
|
|
2024
|
-
resolveStorageDir(namespace) {
|
|
2025
|
-
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2026
|
-
return this.memoryDir;
|
|
2027
|
-
}
|
|
2028
|
-
const token = namespaceIdentityToken(namespace);
|
|
2029
|
-
return this.namespaceTokenDir(token);
|
|
2030
|
-
}
|
|
2031
|
-
namespaceTokenDir(token) {
|
|
2032
|
-
if (token.length === 0 || token.includes("/") || token.includes("\\") || token.includes("..") || path2.isAbsolute(token)) {
|
|
2033
|
-
throw new Error(`unsafe namespace token: ${token}`);
|
|
2034
|
-
}
|
|
2035
|
-
return path2.join(this.memoryDir, "namespaces", token);
|
|
2036
|
-
}
|
|
2037
|
-
/**
|
|
2038
|
-
* Whether a candidate storage dir is LEXICALLY contained: it is either the
|
|
2039
|
-
* legacy default root (`memoryDir`) or a strict descendant of
|
|
2040
|
-
* `<memoryDir>/namespaces/`. The router legitimately resolves a namespace to
|
|
2041
|
-
* EITHER the tokenized dir or a legacy raw-name dir under `namespaces/`, so we
|
|
2042
|
-
* accept any contained child rather than a single exact token path. This is a
|
|
2043
|
-
* pure string check — symlink escape is checked separately via realpath.
|
|
2044
|
-
*/
|
|
2045
|
-
isLexicallyContained(candidate) {
|
|
2046
|
-
const resolved = path2.resolve(candidate);
|
|
2047
|
-
if (resolved === path2.resolve(this.memoryDir)) return true;
|
|
2048
|
-
const nsBase = path2.resolve(path2.join(this.memoryDir, "namespaces"));
|
|
2049
|
-
const rel = path2.relative(nsBase, resolved);
|
|
2050
|
-
return rel.length > 0 && !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
2051
|
-
}
|
|
2052
|
-
/**
|
|
2053
|
-
* Whether a candidate storage dir satisfies the catalog containment contract,
|
|
2054
|
-
* including SYMLINK-escape rejection (round 5, codex P2). A lexically-contained
|
|
2055
|
-
* path that is actually a symlink to an outside directory would let maintenance
|
|
2056
|
-
* or QMD follow it outside `memoryDir`. We mirror `rebuildFromDisk`'s posture:
|
|
2057
|
-
* the path must be lexically contained AND, if it exists on disk, neither the
|
|
2058
|
-
* path itself a symlink nor its realpath escaping the memory root. Non-existent
|
|
2059
|
-
* paths pass the realpath stage (nothing to follow yet) but still must be
|
|
2060
|
-
* lexically contained.
|
|
2061
|
-
*/
|
|
2062
|
-
async isContainedStorageDir(candidate) {
|
|
2063
|
-
if (!this.isLexicallyContained(candidate)) return false;
|
|
2064
|
-
if (path2.resolve(candidate) === path2.resolve(this.memoryDir)) return true;
|
|
2065
|
-
let memoryReal;
|
|
2066
|
-
try {
|
|
2067
|
-
memoryReal = await realpath(this.memoryDir);
|
|
2068
|
-
} catch {
|
|
2069
|
-
memoryReal = path2.resolve(this.memoryDir);
|
|
2070
|
-
}
|
|
2071
|
-
if (await this.hasSymlinkedAncestor(candidate)) return false;
|
|
2072
|
-
try {
|
|
2073
|
-
const stat4 = await lstat(candidate);
|
|
2074
|
-
if (stat4.isSymbolicLink()) return false;
|
|
2075
|
-
if (!stat4.isDirectory()) return false;
|
|
2076
|
-
} catch {
|
|
2077
|
-
return this.isNearestExistingAncestorContained(candidate, memoryReal);
|
|
2078
|
-
}
|
|
2079
|
-
try {
|
|
2080
|
-
const real = await realpath(candidate);
|
|
2081
|
-
return isPathInside(memoryReal, real);
|
|
2082
|
-
} catch {
|
|
2083
|
-
return false;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
/**
|
|
2087
|
-
* Reject a candidate whose path crosses a SYMLINKED ancestor strictly between
|
|
2088
|
-
* memoryDir and the leaf (codex NVuq5). `realpath`-based containment accepts a
|
|
2089
|
-
* symlinked `<memoryDir>/namespaces` that currently resolves back inside
|
|
2090
|
-
* memoryDir, but the disk scanner rejects such a root and a later retarget would
|
|
2091
|
-
* escape the memory tree — so refuse it here too. The leaf itself is
|
|
2092
|
-
* symlink-checked by the caller; this walks only the intermediate ancestors.
|
|
2093
|
-
*/
|
|
2094
|
-
async hasSymlinkedAncestor(candidate) {
|
|
2095
|
-
const stopAt = path2.resolve(this.memoryDir);
|
|
2096
|
-
let dir = path2.dirname(path2.resolve(candidate));
|
|
2097
|
-
const root = path2.parse(dir).root;
|
|
2098
|
-
while (dir !== stopAt && dir !== root && dir !== path2.dirname(dir)) {
|
|
2099
|
-
try {
|
|
2100
|
-
if ((await lstat(dir)).isSymbolicLink()) return true;
|
|
2101
|
-
} catch {
|
|
2102
|
-
}
|
|
2103
|
-
dir = path2.dirname(dir);
|
|
2104
|
-
}
|
|
2105
|
-
return false;
|
|
2106
|
-
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Walk up from a not-yet-existing candidate to the nearest ancestor that exists
|
|
2109
|
-
* on disk and verify its realpath stays inside `memoryReal` (round 6, codex P2
|
|
2110
|
-
* — NDo79). Rejects a non-existent leaf whose existing parent chain escapes
|
|
2111
|
-
* memoryDir via a symlink. Stops at memoryDir's resolved root.
|
|
2112
|
-
*
|
|
2113
|
-
* The nearest existing ancestor must also be a DIRECTORY (NHIdt, codex P2): if
|
|
2114
|
-
* an existing parent such as `<memoryDir>/namespaces` is a regular FILE (or
|
|
2115
|
-
* socket/fifo), `realpath(parent)` still succeeds and resolves inside memoryDir,
|
|
2116
|
-
* so a containment-only check would ACCEPT a leaf that can never be created — you
|
|
2117
|
-
* cannot mkdir a child under a file. We `lstat` the nearest existing ancestor and
|
|
2118
|
-
* reject when it is not a directory, mirroring the leaf non-directory rejection
|
|
2119
|
-
* (NF21i) and the disk scan, so every containment consumer agrees.
|
|
2120
|
-
*/
|
|
2121
|
-
async isNearestExistingAncestorContained(candidate, memoryReal) {
|
|
2122
|
-
let dir = path2.resolve(candidate);
|
|
2123
|
-
const root = path2.parse(dir).root;
|
|
2124
|
-
for (; ; ) {
|
|
2125
|
-
const parent = path2.dirname(dir);
|
|
2126
|
-
if (parent === dir || dir === root) return false;
|
|
2127
|
-
let real;
|
|
2128
|
-
try {
|
|
2129
|
-
real = await realpath(parent);
|
|
2130
|
-
} catch {
|
|
2131
|
-
dir = parent;
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
if (!(isPathInside(memoryReal, real) || real === memoryReal)) return false;
|
|
2135
|
-
try {
|
|
2136
|
-
const stat4 = await lstat(real);
|
|
2137
|
-
return stat4.isDirectory();
|
|
2138
|
-
} catch {
|
|
2139
|
-
return false;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Resolve the storage dir to persist for a touch, validating any caller-
|
|
2145
|
-
* provided `metadata.storageDir` against the catalog containment contract
|
|
2146
|
-
* (round 4 + round 5, codex P2). `markWrite`/`registerResolved` accept an
|
|
2147
|
-
* explicit storageDir, but persisting it verbatim would let a bad hook or
|
|
2148
|
-
* external consumer write an arbitrary path — including one outside `memoryDir`
|
|
2149
|
-
* or a symlink that escapes it — into the catalog, handing maintenance/QMD an
|
|
2150
|
-
* unsafe root. We accept an explicit (or previously-stored) dir ONLY when it
|
|
2151
|
-
* stays contained under memoryDir (lexically AND via realpath); otherwise we
|
|
2152
|
-
* drop it and fall back to the trusted resolved dir.
|
|
2153
|
-
*/
|
|
2154
|
-
async resolveTouchStorageDir(namespace, explicit, existingDir) {
|
|
2155
|
-
if (explicit !== void 0 && await this.isContainedStorageDir(explicit) && await this.isStorageDirForNamespace(namespace, explicit)) {
|
|
2156
|
-
return explicit;
|
|
2157
|
-
}
|
|
2158
|
-
if (existingDir !== void 0 && await this.isContainedStorageDir(existingDir) && await this.isStorageDirForNamespace(namespace, existingDir)) {
|
|
2159
|
-
return existingDir;
|
|
2160
|
-
}
|
|
2161
|
-
return this.resolveSafeStorageDir(namespace);
|
|
2162
|
-
}
|
|
2163
|
-
/**
|
|
2164
|
-
* Whether `candidate` is a legitimate storage root FOR `namespace` (round 6,
|
|
2165
|
-
* codex P2 — NDATT). Accepts the namespace's router-resolved root, its canonical
|
|
2166
|
-
* lexical tokenized dir, and (for the default namespace only) memoryDir. This
|
|
2167
|
-
* prevents a contained-but-CROSS-NAMESPACE path — another namespace's tree, or
|
|
2168
|
-
* memoryDir for a non-default namespace — from being persisted as this
|
|
2169
|
-
* namespace's root. Compared on resolved (absolute) paths.
|
|
2170
|
-
*/
|
|
2171
|
-
async isStorageDirForNamespace(namespace, candidate) {
|
|
2172
|
-
const resolvedCandidate = path2.resolve(candidate);
|
|
2173
|
-
const valid = /* @__PURE__ */ new Set();
|
|
2174
|
-
try {
|
|
2175
|
-
valid.add(path2.resolve(this.namespaceTokenDir(namespaceIdentityToken(namespace))));
|
|
2176
|
-
} catch {
|
|
2177
|
-
}
|
|
2178
|
-
try {
|
|
2179
|
-
valid.add(path2.resolve(this.namespaceTokenDir(namespace)));
|
|
2180
|
-
} catch {
|
|
2181
|
-
}
|
|
2182
|
-
try {
|
|
2183
|
-
valid.add(path2.resolve(await resolveNamespaceStorageRoot(this.config, namespace)));
|
|
2184
|
-
} catch {
|
|
2185
|
-
}
|
|
2186
|
-
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2187
|
-
valid.add(path2.resolve(this.memoryDir));
|
|
2188
|
-
try {
|
|
2189
|
-
valid.add(path2.resolve(await resolveDefaultNamespaceRoot(this.config)));
|
|
2190
|
-
} catch {
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
return valid.has(resolvedCandidate);
|
|
2194
|
-
}
|
|
2195
|
-
/**
|
|
2196
|
-
* Resolve the canonical storage dir for a namespace as the LIVE ROUTER would,
|
|
2197
|
-
* but NEVER return a path that escapes the memory root.
|
|
2198
|
-
*
|
|
2199
|
-
* Router alignment (round 4, cursor Medium): a read/register touch with no
|
|
2200
|
-
* explicit storageDir previously used the lexical `resolveStorageDir`, which
|
|
2201
|
-
* always picks `<memoryDir>/namespaces/<token>` (or `memoryDir` for the
|
|
2202
|
-
* default). That diverges from `NamespaceStorageRouter`, which can route to a
|
|
2203
|
-
* legacy raw-name dir or a migrated default root — so a recall touch could
|
|
2204
|
-
* record a contained-but-WRONG root that maintenance/rebuild then targets. We
|
|
2205
|
-
* now delegate to the shared `resolveNamespaceStorageRoot` (the very helper the
|
|
2206
|
-
* router uses) so the catalog records the same on-disk root the router serves.
|
|
2207
|
-
*
|
|
2208
|
-
* Containment (round 5, codex P2): the resolved path can still be a symlink
|
|
2209
|
-
* escaping memoryDir, so we run the full (lexical + realpath) containment
|
|
2210
|
-
* contract. When it FAILS we fall back to a NAMESPACE-SPECIFIC safe root, NOT
|
|
2211
|
-
* a blanket `memoryDir`. Recording `memoryDir` for a non-default namespace
|
|
2212
|
-
* would point enumeration/maintenance at the DEFAULT namespace's tree (round 5,
|
|
2213
|
-
* cursor/codex Medium/P2) — a cross-namespace fanout error. The correct safe
|
|
2214
|
-
* root is the namespace's own lexical tokenized dir
|
|
2215
|
-
* (`<memoryDir>/namespaces/<token>`), which is always contained and is that
|
|
2216
|
-
* namespace's canonical location (we record the lexical PATH as metadata; we do
|
|
2217
|
-
* not follow the escaping symlink). Only the default namespace — or a token so
|
|
2218
|
-
* unsafe even the lexical dir cannot be built — falls back to `memoryDir`.
|
|
2219
|
-
*/
|
|
2220
|
-
async resolveSafeStorageDir(namespace) {
|
|
2221
|
-
let resolved;
|
|
2222
|
-
try {
|
|
2223
|
-
resolved = await resolveNamespaceStorageRoot(this.config, namespace);
|
|
2224
|
-
} catch {
|
|
2225
|
-
return this.safeFallbackStorageDir(namespace);
|
|
2226
|
-
}
|
|
2227
|
-
if (await this.isContainedStorageDir(resolved)) return resolved;
|
|
2228
|
-
return this.safeFallbackStorageDir(namespace);
|
|
2229
|
-
}
|
|
2230
|
-
/**
|
|
2231
|
-
* The namespace-specific contained fallback root, used when the router-resolved
|
|
2232
|
-
* root fails containment (round 5, cursor/codex Medium/P2).
|
|
2233
|
-
*
|
|
2234
|
-
* Preference order:
|
|
2235
|
-
* 1. The namespace's OWN lexical tokenized dir (`namespaces/<token>`) — so a
|
|
2236
|
-
* non-default namespace is NOT pointed at the DEFAULT namespace's `memoryDir`
|
|
2237
|
-
* tree (which would misdirect maintenance fanout). Returned only when the
|
|
2238
|
-
* token dir itself stays CONTAINED (it is not a symlink, and its realpath
|
|
2239
|
-
* does not escape memoryDir — e.g. via a symlinked `namespaces/` parent).
|
|
2240
|
-
* 2. `memoryDir` as a LAST resort — for the default namespace, an unsafe token
|
|
2241
|
-
* that cannot build a contained path, OR the irreparable case where the
|
|
2242
|
-
* token dir's realpath escapes the root (so even its lexical path resolves
|
|
2243
|
-
* outside). NF21m note (codex P2): we deliberately do NOT record the lexical
|
|
2244
|
-
* token dir in that irreparable case — its realpath escapes memoryDir, and
|
|
2245
|
-
* the NDo79 contract REQUIRES that an escaping path is never persisted (a
|
|
2246
|
-
* later mkdir/maintenance/QMD op would follow it outside the root). Since no
|
|
2247
|
-
* contained namespace-specific path exists, containment wins: `memoryDir` is
|
|
2248
|
-
* the only safe root left. A namespace whose token dir's realpath escapes is
|
|
2249
|
-
* an irreparable on-disk state; recording the contained default root is
|
|
2250
|
-
* strictly safer than persisting an escaping one. The common case where the
|
|
2251
|
-
* token dir IS contained is handled by branch 1, so a healthy non-default
|
|
2252
|
-
* namespace never reaches `memoryDir`.
|
|
2253
|
-
*/
|
|
2254
|
-
async safeFallbackStorageDir(namespace) {
|
|
2255
|
-
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) return this.memoryDir;
|
|
2256
|
-
let tokenDir;
|
|
2257
|
-
try {
|
|
2258
|
-
tokenDir = this.namespaceTokenDir(namespaceIdentityToken(namespace));
|
|
2259
|
-
} catch {
|
|
2260
|
-
return this.memoryDir;
|
|
2261
|
-
}
|
|
2262
|
-
if (await this.isContainedStorageDir(tokenDir)) return tokenDir;
|
|
2263
|
-
return this.memoryDir;
|
|
2264
|
-
}
|
|
2265
|
-
/**
|
|
2266
|
-
* Re-check, NOW, whether a namespace's storage root currently EXISTS on disk
|
|
2267
|
-
* with the SAME safety the directory scan uses (NFJV8, codex P2).
|
|
2268
|
-
*
|
|
2269
|
-
* The rebuild's final re-merge runs under the held lock and folds the freshly
|
|
2270
|
-
* re-read log (`latest`) into the scanned `rebuilt` set. A namespace present in
|
|
2271
|
-
* `latest` (a live touch row) but ABSENT from `rebuilt` is normally PURGED as
|
|
2272
|
-
* deleted (the NATqU "disk scan is authoritative" rule). But there is a TOCTOU
|
|
2273
|
-
* window: a dynamic namespace can be CREATED on disk AFTER `rebuildFromDisk()`
|
|
2274
|
-
* already enumerated `namespaces/` but BEFORE this re-merge. The scan snapshot
|
|
2275
|
-
* missed its new root, yet a gateway `markWrite` already appended a row for it.
|
|
2276
|
-
* Blindly purging that row would rewrite the catalog WITHOUT a live namespace
|
|
2277
|
-
* that now has data on disk, so `writtenSince`/maintenance/QMD consumers miss
|
|
2278
|
-
* it until another touch or rebuild.
|
|
2279
|
-
*
|
|
2280
|
-
* So before purging, we re-resolve the namespace's safe storage root (the same
|
|
2281
|
-
* router-aligned, containment-checked path the scan would have catalogued) and
|
|
2282
|
-
* confirm it is a real, contained, non-symlink directory that actually holds
|
|
2283
|
-
* memory data RIGHT NOW. If so the namespace was created-after-scan and is LIVE
|
|
2284
|
-
* — KEEP its row. This is the precise inverse of NATqU and does NOT reintroduce
|
|
2285
|
-
* it: a touch on a REMOVED root re-checks as ABSENT (no data on disk) and is
|
|
2286
|
-
* still purged; only a root that EXISTS on a fresh re-check is kept.
|
|
2287
|
-
*
|
|
2288
|
-
* Mirrors the per-entry scan checks (symlink rejection + realpath containment +
|
|
2289
|
-
* `hasMemoryData`) so a symlinked/escaping root is never resurrected.
|
|
2290
|
-
*/
|
|
2291
|
-
async liveStorageRootExistsForRebuild(namespace, memoryReal) {
|
|
2292
|
-
let root;
|
|
2293
|
-
try {
|
|
2294
|
-
root = await this.resolveSafeStorageDir(namespace);
|
|
2295
|
-
} catch {
|
|
2296
|
-
return false;
|
|
2297
|
-
}
|
|
2298
|
-
if (normalizeNamespaceIdentity(namespace) !== this.defaultNamespaceIdentity && path2.resolve(root) === path2.resolve(this.memoryDir)) {
|
|
2299
|
-
return false;
|
|
2300
|
-
}
|
|
2301
|
-
let stat4;
|
|
2302
|
-
try {
|
|
2303
|
-
stat4 = await lstat(root);
|
|
2304
|
-
} catch {
|
|
2305
|
-
return false;
|
|
2306
|
-
}
|
|
2307
|
-
if (stat4.isSymbolicLink()) return false;
|
|
2308
|
-
if (!stat4.isDirectory()) return false;
|
|
2309
|
-
try {
|
|
2310
|
-
const real = await realpath(root);
|
|
2311
|
-
if (memoryReal && !isPathInside(memoryReal, real)) return false;
|
|
2312
|
-
} catch {
|
|
2313
|
-
return false;
|
|
2314
|
-
}
|
|
2315
|
-
return hasMemoryData(root);
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Record a namespace touch. Returns whether the touch actually APPENDED to the
|
|
2319
|
-
* log (round 6, codex P2 — NEFoX): a disabled catalog or a dropped append (the
|
|
2320
|
-
* NAUf7 rebuild-lock-timeout drop) returns `false`, so callers (e.g. the router
|
|
2321
|
-
* resolve-hook dedup) can avoid marking a dropped registration as completed and
|
|
2322
|
-
* suppressing its retry.
|
|
2323
|
-
*/
|
|
2324
|
-
async touch(namespace, kind, metadata, jobName) {
|
|
2325
|
-
if (!this.enabled) return false;
|
|
2326
|
-
const ns = this.validateNamespace(namespace);
|
|
2327
|
-
const nowIso = (metadata?.at ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2328
|
-
return this.queueCritical(
|
|
2329
|
-
async () => this.withHeldCatalogLock(async (acquired) => {
|
|
2330
|
-
if (!acquired) return false;
|
|
2331
|
-
if (this.onTouchCriticalSectionForTest) {
|
|
2332
|
-
await this.onTouchCriticalSectionForTest();
|
|
2333
|
-
}
|
|
2334
|
-
const records = await this.loadCompacted();
|
|
2335
|
-
const existing = records.get(ns);
|
|
2336
|
-
const storageDir = await this.resolveTouchStorageDir(
|
|
2337
|
-
ns,
|
|
2338
|
-
metadata?.storageDir,
|
|
2339
|
-
existing?.storageDir
|
|
2340
|
-
);
|
|
2341
|
-
const record = existing ? { ...existing } : {
|
|
2342
|
-
namespace: ns,
|
|
2343
|
-
identityToken: namespaceIdentityToken(ns),
|
|
2344
|
-
kind: metadata?.kind ?? inferKind(ns, this.config),
|
|
2345
|
-
createdAt: nowIso,
|
|
2346
|
-
storageDir,
|
|
2347
|
-
discoveredBy: metadata?.discoveredBy ?? (kind === "register" ? "config" : kind === "maintenance" ? "scan" : kind)
|
|
2348
|
-
};
|
|
2349
|
-
record.storageDir = storageDir;
|
|
2350
|
-
if (metadata?.kind) record.kind = metadata.kind;
|
|
2351
|
-
if (metadata?.principal !== void 0) record.principal = metadata.principal;
|
|
2352
|
-
if (metadata?.projectId !== void 0) record.projectId = metadata.projectId;
|
|
2353
|
-
if (metadata?.branch !== void 0) record.branch = metadata.branch;
|
|
2354
|
-
if (metadata?.parentNamespace !== void 0)
|
|
2355
|
-
record.parentNamespace = metadata.parentNamespace;
|
|
2356
|
-
if (kind === "write" && existing && record.discoveredBy === "config") {
|
|
2357
|
-
record.discoveredBy = "write";
|
|
2358
|
-
}
|
|
2359
|
-
if (kind === "read") record.lastReadAt = nowIso;
|
|
2360
|
-
if (kind === "write") record.lastWriteAt = nowIso;
|
|
2361
|
-
if (kind === "maintenance" && jobName) {
|
|
2362
|
-
record.lastMaintenanceAt = { ...record.lastMaintenanceAt ?? {}, [jobName]: nowIso };
|
|
2363
|
-
}
|
|
2364
|
-
await this.appendUnchained(record);
|
|
2365
|
-
return true;
|
|
2366
|
-
})
|
|
2367
|
-
);
|
|
2368
|
-
}
|
|
2369
|
-
// ── Rebuild from disk ────────────────────────────────────────────────────
|
|
2370
|
-
async rebuildFromDisk(options) {
|
|
2371
|
-
const dryRun = options?.dryRun === true;
|
|
2372
|
-
if (!this.enabled) {
|
|
2373
|
-
return { dryRun, records: [], skipped: [], applied: false };
|
|
2374
|
-
}
|
|
2375
|
-
if (dryRun) {
|
|
2376
|
-
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, false));
|
|
2377
|
-
}
|
|
2378
|
-
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, true));
|
|
2379
|
-
}
|
|
2380
|
-
/**
|
|
2381
|
-
* Body of `rebuildFromDisk`, run inside a single `queueCritical` turn. MUST
|
|
2382
|
-
* only be invoked from within the serialized chain so the load and the
|
|
2383
|
-
* rewrite are atomic with respect to concurrent touches (in-process).
|
|
2384
|
-
*
|
|
2385
|
-
* `wantMutate` is true for an `--apply` (the caller intends to rewrite). The
|
|
2386
|
-
* cross-process file lock is acquired LATE — only around the final
|
|
2387
|
-
* load→merge→rename window (NFgCT, codex P2) — never across the disk scan, so a
|
|
2388
|
-
* long scan does not force concurrent gateway touches to wait (and drop their
|
|
2389
|
-
* append). Whether the rewrite actually happened is reported via the result's
|
|
2390
|
-
* `applied`: true only when `wantMutate` AND the lock was acquired.
|
|
2391
|
-
*/
|
|
2392
|
-
async rebuildInsideChain(dryRun, wantMutate) {
|
|
2393
|
-
const existing = await this.loadCompacted();
|
|
2394
|
-
const skipped = [];
|
|
2395
|
-
const rebuilt = /* @__PURE__ */ new Map();
|
|
2396
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2397
|
-
let memoryReal = null;
|
|
2398
|
-
try {
|
|
2399
|
-
memoryReal = await realpath(this.memoryDir);
|
|
2400
|
-
} catch {
|
|
2401
|
-
memoryReal = this.memoryDir;
|
|
2402
|
-
}
|
|
2403
|
-
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
2404
|
-
const configured = new Set(
|
|
2405
|
-
[
|
|
2406
|
-
this.config.defaultNamespace,
|
|
2407
|
-
this.config.sharedNamespace,
|
|
2408
|
-
...this.config.namespacePolicies.map((p) => p.name)
|
|
2409
|
-
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
2410
|
-
);
|
|
2411
|
-
const resolvedDefaultRoot = await resolveDefaultNamespaceRoot(this.config);
|
|
2412
|
-
const defaultStorageDir = await this.isContainedStorageDir(resolvedDefaultRoot) ? resolvedDefaultRoot : this.memoryDir;
|
|
2413
|
-
const legacyDefaultHasData = defaultStorageDir === this.memoryDir;
|
|
2414
|
-
for (const ns of configured) {
|
|
2415
|
-
if (!ns) continue;
|
|
2416
|
-
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) {
|
|
2417
|
-
let token;
|
|
2418
|
-
try {
|
|
2419
|
-
token = namespaceIdentityToken(ns);
|
|
2420
|
-
} catch {
|
|
2421
|
-
token = ns;
|
|
2422
|
-
}
|
|
2423
|
-
skipped.push({ token, reason: "unsafe", detail: ns });
|
|
2424
|
-
continue;
|
|
2425
|
-
}
|
|
2426
|
-
let storageDir;
|
|
2427
|
-
if (ns === defaultNs) {
|
|
2428
|
-
storageDir = defaultStorageDir;
|
|
2429
|
-
} else {
|
|
2430
|
-
try {
|
|
2431
|
-
storageDir = await resolveNamespaceStorageRoot(this.config, ns);
|
|
2432
|
-
} catch {
|
|
2433
|
-
storageDir = this.namespaceTokenDir(namespaceIdentityToken(ns));
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
if (!await this.isContainedStorageDir(storageDir)) {
|
|
2437
|
-
if (ns === defaultNs) {
|
|
2438
|
-
storageDir = this.memoryDir;
|
|
2439
|
-
} else {
|
|
2440
|
-
skipped.push({ token: namespaceIdentityToken(ns), reason: "escape", detail: storageDir });
|
|
2441
|
-
continue;
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
rebuilt.set(
|
|
2445
|
-
ns,
|
|
2446
|
-
this.mergeForRebuild(existing.get(ns), {
|
|
2447
|
-
namespace: ns,
|
|
2448
|
-
identityToken: namespaceIdentityToken(ns),
|
|
2449
|
-
kind: inferKind(ns, this.config),
|
|
2450
|
-
createdAt: existing.get(ns)?.createdAt ?? nowIso,
|
|
2451
|
-
storageDir,
|
|
2452
|
-
discoveredBy: "config"
|
|
2453
|
-
})
|
|
2454
|
-
);
|
|
2455
|
-
}
|
|
2456
|
-
const namespacesDir = path2.join(this.memoryDir, "namespaces");
|
|
2457
|
-
let entries = [];
|
|
2458
|
-
let namespacesDirSafe = true;
|
|
2459
|
-
try {
|
|
2460
|
-
const rootStat = await lstat(namespacesDir);
|
|
2461
|
-
if (rootStat.isSymbolicLink()) {
|
|
2462
|
-
namespacesDirSafe = false;
|
|
2463
|
-
} else {
|
|
2464
|
-
const realNamespacesDir = await realpath(namespacesDir);
|
|
2465
|
-
if (memoryReal && !isPathInside(memoryReal, realNamespacesDir)) {
|
|
2466
|
-
namespacesDirSafe = false;
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
} catch {
|
|
2470
|
-
namespacesDirSafe = true;
|
|
2471
|
-
}
|
|
2472
|
-
if (!namespacesDirSafe) {
|
|
2473
|
-
skipped.push({ token: "namespaces", reason: "symlink", detail: namespacesDir });
|
|
2474
|
-
} else {
|
|
2475
|
-
try {
|
|
2476
|
-
entries = await readdir(namespacesDir, { withFileTypes: true });
|
|
2477
|
-
} catch {
|
|
2478
|
-
entries = [];
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
const scannedFromTokenized = /* @__PURE__ */ new Set();
|
|
2482
|
-
for (const entry of entries) {
|
|
2483
|
-
const token = entry.name;
|
|
2484
|
-
const fullPath = path2.join(namespacesDir, token);
|
|
2485
|
-
let stat4;
|
|
2486
|
-
try {
|
|
2487
|
-
stat4 = await lstat(fullPath);
|
|
2488
|
-
} catch (err) {
|
|
2489
|
-
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
if (stat4.isSymbolicLink()) {
|
|
2493
|
-
skipped.push({ token, reason: "symlink", detail: fullPath });
|
|
2494
|
-
continue;
|
|
2495
|
-
}
|
|
2496
|
-
if (!stat4.isDirectory()) continue;
|
|
2497
|
-
try {
|
|
2498
|
-
const real = await realpath(fullPath);
|
|
2499
|
-
if (memoryReal && !isPathInside(memoryReal, real)) {
|
|
2500
|
-
skipped.push({ token, reason: "escape", detail: real });
|
|
2501
|
-
continue;
|
|
2502
|
-
}
|
|
2503
|
-
} catch (err) {
|
|
2504
|
-
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2505
|
-
continue;
|
|
2506
|
-
}
|
|
2507
|
-
const literalRecord = existing.get(token);
|
|
2508
|
-
const literalOwnsRoot = configured.has(token) || literalRecord !== void 0 && path2.resolve(literalRecord.storageDir) === path2.resolve(fullPath);
|
|
2509
|
-
const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);
|
|
2510
|
-
const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;
|
|
2511
|
-
const decoded = normalizeNamespaceIdentity(rawDecoded);
|
|
2512
|
-
if (decoded.length === 0 || rawDecoded !== decoded) {
|
|
2513
|
-
skipped.push({ token, reason: "unsafe", detail: rawDecoded });
|
|
2514
|
-
continue;
|
|
2515
|
-
}
|
|
2516
|
-
if (decoded !== defaultNs && !isSafeRouteNamespace(decoded)) {
|
|
2517
|
-
skipped.push({ token, reason: "unsafe", detail: decoded });
|
|
2518
|
-
continue;
|
|
2519
|
-
}
|
|
2520
|
-
const memoryData = await inspectMemoryDataRoot(fullPath);
|
|
2521
|
-
if (memoryData.invalidMarker) {
|
|
2522
|
-
skipped.push({
|
|
2523
|
-
token,
|
|
2524
|
-
reason: "unsafe",
|
|
2525
|
-
detail: `invalid memory marker: ${memoryData.invalidMarker}`
|
|
2526
|
-
});
|
|
2527
|
-
continue;
|
|
2528
|
-
}
|
|
2529
|
-
if (!memoryData.hasData) continue;
|
|
2530
|
-
if (decoded === defaultNs) {
|
|
2531
|
-
const def = rebuilt.get(defaultNs);
|
|
2532
|
-
if (def) {
|
|
2533
|
-
def.storageDir = defaultStorageDir;
|
|
2534
|
-
def.kind = "default";
|
|
2535
|
-
}
|
|
2536
|
-
continue;
|
|
2537
|
-
}
|
|
2538
|
-
const isTokenizedEntry = token === namespaceIdentityToken(decoded);
|
|
2539
|
-
if (rebuilt.has(decoded) && scannedFromTokenized.has(decoded) && !isTokenizedEntry) {
|
|
2540
|
-
continue;
|
|
2541
|
-
}
|
|
2542
|
-
if (isTokenizedEntry) scannedFromTokenized.add(decoded);
|
|
2543
|
-
const prior = existing.get(decoded);
|
|
2544
|
-
rebuilt.set(
|
|
2545
|
-
decoded,
|
|
2546
|
-
this.mergeForRebuild(prior, {
|
|
2547
|
-
namespace: decoded,
|
|
2548
|
-
identityToken: namespaceIdentityToken(decoded),
|
|
2549
|
-
kind: inferKind(decoded, this.config),
|
|
2550
|
-
createdAt: prior?.createdAt ?? nowIso,
|
|
2551
|
-
storageDir: fullPath,
|
|
2552
|
-
// Configured-and-present namespaces keep config provenance; purely
|
|
2553
|
-
// discovered ones are scan.
|
|
2554
|
-
discoveredBy: configured.has(decoded) ? "config" : prior?.discoveredBy ?? "scan"
|
|
2555
|
-
})
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
if (legacyDefaultHasData && defaultStorageDir === this.memoryDir) {
|
|
2559
|
-
const def = rebuilt.get(defaultNs);
|
|
2560
|
-
if (def) def.kind = "default";
|
|
2561
|
-
}
|
|
2562
|
-
if (!wantMutate) {
|
|
2563
|
-
return this.finishRebuild(rebuilt, skipped, dryRun, false, memoryReal, nowIso);
|
|
2564
|
-
}
|
|
2565
|
-
if (this.onRebuildAfterScanForTest) {
|
|
2566
|
-
await this.onRebuildAfterScanForTest();
|
|
2567
|
-
}
|
|
2568
|
-
return this.withHeldCatalogLock(
|
|
2569
|
-
(acquired) => this.finishRebuild(rebuilt, skipped, dryRun, acquired, memoryReal, nowIso)
|
|
2570
|
-
);
|
|
2571
|
-
}
|
|
2572
|
-
/**
|
|
2573
|
-
* Final load→merge→rename window of a rebuild, factored out so the caller can
|
|
2574
|
-
* run it WITHIN the cross-process file lock (NFgCT, codex P2) without holding
|
|
2575
|
-
* that lock across the preceding disk scan. Re-reads the latest on-disk state,
|
|
2576
|
-
* folds concurrent touches, then (when `canMutate`) atomically rewrites the log.
|
|
2577
|
-
*
|
|
2578
|
-
* `canMutate` records that the cross-process lock was actually held. The
|
|
2579
|
-
* re-merge + rewrite run only when it is true — a dry-run, or an unlocked apply
|
|
2580
|
-
* (lock-acquisition timeout), computes records but does NOT rename, so it can
|
|
2581
|
-
* never clobber a concurrent lock holder's window. `applied` mirrors `canMutate`.
|
|
2582
|
-
*/
|
|
2583
|
-
async finishRebuild(rebuilt, skipped, dryRun, canMutate, memoryReal, nowIso) {
|
|
2584
|
-
if (canMutate) {
|
|
2585
|
-
const latest = await this.loadCompacted();
|
|
2586
|
-
for (const [ns, fresh] of latest) {
|
|
2587
|
-
const current = rebuilt.get(ns);
|
|
2588
|
-
if (!current) {
|
|
2589
|
-
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
2590
|
-
continue;
|
|
2591
|
-
}
|
|
2592
|
-
if (await this.liveStorageRootExistsForRebuild(ns, memoryReal)) {
|
|
2593
|
-
const safeDir = await this.resolveSafeStorageDir(ns);
|
|
2594
|
-
const resolvedSafe = path2.resolve(safeDir);
|
|
2595
|
-
let owningNamespace = null;
|
|
2596
|
-
for (const [otherNs, otherRec] of rebuilt) {
|
|
2597
|
-
if (otherNs !== ns && path2.resolve(otherRec.storageDir) === resolvedSafe) {
|
|
2598
|
-
owningNamespace = otherNs;
|
|
2599
|
-
break;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
if (owningNamespace) {
|
|
2603
|
-
const owner = rebuilt.get(owningNamespace);
|
|
2604
|
-
if (owner) rebuilt.set(owningNamespace, mergeNewerTouchFields(owner, fresh));
|
|
2605
|
-
continue;
|
|
2606
|
-
}
|
|
2607
|
-
rebuilt.set(ns, {
|
|
2608
|
-
...fresh,
|
|
2609
|
-
storageDir: safeDir,
|
|
2610
|
-
identityToken: namespaceIdentityToken(ns),
|
|
2611
|
-
kind: fresh.kind ?? inferKind(ns, this.config),
|
|
2612
|
-
createdAt: fresh.createdAt ?? nowIso
|
|
2613
|
-
});
|
|
2614
|
-
continue;
|
|
2615
|
-
}
|
|
2616
|
-
continue;
|
|
2617
|
-
}
|
|
2618
|
-
rebuilt.set(ns, mergeNewerTouchFields(current, fresh));
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
const records = [...rebuilt.values()].sort((a, b) => {
|
|
2622
|
-
const byName = a.namespace.localeCompare(b.namespace);
|
|
2623
|
-
if (byName !== 0) return byName;
|
|
2624
|
-
return a.identityToken.localeCompare(b.identityToken);
|
|
2625
|
-
});
|
|
2626
|
-
if (canMutate) {
|
|
2627
|
-
if (this.onRebuildBeforeRenameForTest) {
|
|
2628
|
-
await this.onRebuildBeforeRenameForTest();
|
|
2629
|
-
}
|
|
2630
|
-
await this.rewriteUnchained(records);
|
|
2631
|
-
}
|
|
2632
|
-
return { dryRun, records, skipped, applied: canMutate };
|
|
2633
|
-
}
|
|
2634
|
-
// ── Cross-process catalog write lock (held mutex) ────────────────────────
|
|
2635
|
-
/**
|
|
2636
|
-
* Run `fn` while HOLDING the shared cross-process advisory lock (round 5, codex
|
|
2637
|
-
* P2; generalized round 7 — NEZkA). This is the SINGLE mutex shared by BOTH the
|
|
2638
|
-
* touch read→merge→append window AND the rebuild final load→merge→rename window,
|
|
2639
|
-
* so a touch and a rebuild in different processes are mutually exclusive over
|
|
2640
|
-
* their respective critical sections — closing the check-then-append gap where a
|
|
2641
|
-
* polled-only touch could append into a rebuild's load→rename window.
|
|
2642
|
-
*
|
|
2643
|
-
* Acquisition is atomic via `open(..., "wx")`. A lock older than
|
|
2644
|
-
* `REBUILD_LOCK_STALE_MS` is treated as a crashed holder and broken. After
|
|
2645
|
-
* `REBUILD_LOCK_MAX_WAIT_MS` of contention we proceed best-effort WITHOUT the
|
|
2646
|
-
* lock rather than block forever. The lock is always released in `finally`.
|
|
2647
|
-
*
|
|
2648
|
-
* IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the
|
|
2649
|
-
* per-process `queueCritical` chain, which serializes all catalog mutations in
|
|
2650
|
-
* THIS process. So within one process only one logical holder attempts OS-lock
|
|
2651
|
-
* acquisition at a time — the file lock is never self-contended in-process, and
|
|
2652
|
-
* the lock is acquired and released within a single in-process turn. The file
|
|
2653
|
-
* lock adds only the missing CROSS-process exclusion.
|
|
2654
|
-
*
|
|
2655
|
-
* HEARTBEAT (round 5, cursor/codex Medium/P2): while WE hold the lock a timer
|
|
2656
|
-
* refreshes its mtime every `REBUILD_LOCK_HEARTBEAT_MS`, so a legitimately long
|
|
2657
|
-
* holder (> `REBUILD_LOCK_STALE_MS`) is not treated as a crashed holder and
|
|
2658
|
-
* unlinked by another process — which would let overlapping windows lose
|
|
2659
|
-
* appends. Heartbeat failures are swallowed; the timer is always cleared in
|
|
2660
|
-
* `finally`.
|
|
2661
|
-
*
|
|
2662
|
-
* ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE
|
|
2663
|
-
* actually hold the lock. When acquisition TIMED OUT (another holder is active),
|
|
2664
|
-
* a MUTATING rebuild must NOT perform its load/rename window unlocked, and a
|
|
2665
|
-
* touch must NOT append unlocked — both would recreate the lost-append race. The
|
|
2666
|
-
* caller uses `acquired` to run compute-only (rebuild) or DROP the append
|
|
2667
|
-
* (touch) when unlocked.
|
|
2668
|
-
*/
|
|
2669
|
-
async withHeldCatalogLock(fn) {
|
|
2670
|
-
const acquired = await this.acquireRebuildLock();
|
|
2671
|
-
let heartbeat;
|
|
2672
|
-
if (acquired) {
|
|
2673
|
-
heartbeat = setInterval(() => {
|
|
2674
|
-
const now = /* @__PURE__ */ new Date();
|
|
2675
|
-
utimes(this.rebuildLockPath, now, now).catch(() => void 0);
|
|
2676
|
-
}, REBUILD_LOCK_HEARTBEAT_MS);
|
|
2677
|
-
heartbeat.unref?.();
|
|
2678
|
-
}
|
|
2679
|
-
try {
|
|
2680
|
-
return await fn(acquired);
|
|
2681
|
-
} finally {
|
|
2682
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
2683
|
-
if (acquired) {
|
|
2684
|
-
try {
|
|
2685
|
-
if (await this.rebuildLockHeldBySelf()) {
|
|
2686
|
-
await unlink(this.rebuildLockPath);
|
|
2687
|
-
}
|
|
2688
|
-
} catch {
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
/** Try to acquire the rebuild lock; returns true if WE created it. */
|
|
2694
|
-
async acquireRebuildLock() {
|
|
2695
|
-
const deadline = Date.now() + REBUILD_LOCK_MAX_WAIT_MS;
|
|
2696
|
-
await mkdir2(this.stateDir, { recursive: true });
|
|
2697
|
-
for (; ; ) {
|
|
2698
|
-
try {
|
|
2699
|
-
const handle = await open(this.rebuildLockPath, "wx");
|
|
2700
|
-
try {
|
|
2701
|
-
await handle.writeFile(
|
|
2702
|
-
`${process.pid} ${this.lockOwnerId} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2703
|
-
`,
|
|
2704
|
-
"utf8"
|
|
2705
|
-
);
|
|
2706
|
-
} catch {
|
|
2707
|
-
} finally {
|
|
2708
|
-
await handle.close();
|
|
2709
|
-
}
|
|
2710
|
-
return true;
|
|
2711
|
-
} catch (err) {
|
|
2712
|
-
if (err?.code !== "EEXIST") {
|
|
2713
|
-
return false;
|
|
2714
|
-
}
|
|
2715
|
-
await this.breakStaleRebuildLock();
|
|
2716
|
-
if (Date.now() >= deadline) return false;
|
|
2717
|
-
await new Promise((r) => setTimeout(r, REBUILD_LOCK_POLL_MS));
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
/**
|
|
2722
|
-
* Remove the lock file if its mtime is older than the stale threshold.
|
|
2723
|
-
*
|
|
2724
|
-
* REPLACEMENT-SAFE (NG7Bg, codex P2): a plain `stat` → `unlink` has a TOCTOU
|
|
2725
|
-
* window — two processes can both observe the SAME stale lock; one removes it and
|
|
2726
|
-
* creates a FRESH lock, and the other's later `unlink` then deletes that fresh
|
|
2727
|
-
* holder's ACTIVE lock based on the stale identity it read earlier, leaving the
|
|
2728
|
-
* fresh holder running its critical section with no visible lock and reopening the
|
|
2729
|
-
* lost-update race the mutex prevents. We therefore capture the lock's IDENTITY
|
|
2730
|
-
* (its full content line: `<pid> <owner-uuid> <iso>`) when we judge it stale, then
|
|
2731
|
-
* RE-READ immediately before unlinking and only remove it when the content is
|
|
2732
|
-
* byte-identical AND still stale. A replacement lock has a different owner id /
|
|
2733
|
-
* timestamp, so its content differs and we leave it untouched. We never unlink a
|
|
2734
|
-
* lock whose mtime is now fresh (a heartbeat refreshed it) or whose identity
|
|
2735
|
-
* changed (a replacement was created). This is best-effort: any mismatch/vanish
|
|
2736
|
-
* simply skips the break and the caller polls again.
|
|
2737
|
-
*/
|
|
2738
|
-
async breakStaleRebuildLock() {
|
|
2739
|
-
let staleIdentity;
|
|
2740
|
-
try {
|
|
2741
|
-
const info = await stat(this.rebuildLockPath);
|
|
2742
|
-
if (Date.now() - info.mtimeMs <= REBUILD_LOCK_STALE_MS) {
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
staleIdentity = await readFile2(this.rebuildLockPath, "utf8");
|
|
2746
|
-
} catch {
|
|
2747
|
-
return;
|
|
2748
|
-
}
|
|
2749
|
-
if (this.onBeforeBreakStaleUnlinkForTest) {
|
|
2750
|
-
await this.onBeforeBreakStaleUnlinkForTest();
|
|
2751
|
-
}
|
|
2752
|
-
try {
|
|
2753
|
-
const current = await readFile2(this.rebuildLockPath, "utf8");
|
|
2754
|
-
if (current !== staleIdentity) return;
|
|
2755
|
-
const recheck = await stat(this.rebuildLockPath);
|
|
2756
|
-
if (Date.now() - recheck.mtimeMs <= REBUILD_LOCK_STALE_MS) return;
|
|
2757
|
-
await unlink(this.rebuildLockPath).catch(() => void 0);
|
|
2758
|
-
} catch {
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
/**
|
|
2762
|
-
* Whether the rebuild lock file was written by THIS instance (round 6, codex
|
|
2763
|
-
* P2 — NBsGP). Matches the per-instance owner id, NOT just `process.pid`: two
|
|
2764
|
-
* NamespaceCatalog instances in the same process share a PID, so a PID-only
|
|
2765
|
-
* check would wrongly treat instance A's lock as self-held by instance B and
|
|
2766
|
-
* let B's touch skip the wait and append into A's rebuild window. Falls back to
|
|
2767
|
-
* the legacy PID-only form for lock files written before owner ids existed.
|
|
2768
|
-
*/
|
|
2769
|
-
async rebuildLockHeldBySelf() {
|
|
2770
|
-
try {
|
|
2771
|
-
const body = await readFile2(this.rebuildLockPath, "utf8");
|
|
2772
|
-
const parts = body.trim().split(/\s+/);
|
|
2773
|
-
const pid = Number.parseInt(parts[0] ?? "", 10);
|
|
2774
|
-
const ownerId = parts[1];
|
|
2775
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2776
|
-
if (ownerId && UUID_RE.test(ownerId)) {
|
|
2777
|
-
return ownerId === this.lockOwnerId;
|
|
2778
|
-
}
|
|
2779
|
-
return Number.isFinite(pid) && pid === process.pid;
|
|
2780
|
-
} catch {
|
|
2781
|
-
return false;
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
/**
|
|
2785
|
-
* Merge a prior record's preserved metadata (timestamps, principal hints)
|
|
2786
|
-
* onto a freshly-discovered record. Disk-derived fields (storageDir, kind)
|
|
2787
|
-
* take precedence from the new record.
|
|
2788
|
-
*
|
|
2789
|
-
* PROVENANCE (round 3, cursor Low): `discoveredBy` and `createdAt` are
|
|
2790
|
-
* CREATION-ONLY — identical to the touch path's invariant. A rebuild must NOT
|
|
2791
|
-
* reset a namespace first seen via a `write`/`read` touch back to `config`
|
|
2792
|
-
* just because it is also listed in policies. So when a prior record exists we
|
|
2793
|
-
* carry its `discoveredBy` forward; only brand-new records keep the fresh
|
|
2794
|
-
* (config/scan) provenance.
|
|
2795
|
-
*/
|
|
2796
|
-
mergeForRebuild(prior, fresh) {
|
|
2797
|
-
if (!prior) return fresh;
|
|
2798
|
-
const merged = {
|
|
2799
|
-
...fresh,
|
|
2800
|
-
createdAt: prior.createdAt ?? fresh.createdAt,
|
|
2801
|
-
discoveredBy: prior.discoveredBy ?? fresh.discoveredBy
|
|
2802
|
-
};
|
|
2803
|
-
if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;
|
|
2804
|
-
if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;
|
|
2805
|
-
if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };
|
|
2806
|
-
if (prior.principal !== void 0) merged.principal = prior.principal;
|
|
2807
|
-
if (prior.projectId !== void 0) merged.projectId = prior.projectId;
|
|
2808
|
-
if (prior.branch !== void 0) merged.branch = prior.branch;
|
|
2809
|
-
if (prior.parentNamespace !== void 0) merged.parentNamespace = prior.parentNamespace;
|
|
2810
|
-
return merged;
|
|
2811
|
-
}
|
|
2812
|
-
// ── Persistence ──────────────────────────────────────────────────────────
|
|
2813
|
-
/** Load the JSONL log and fold it into current state (last-record-wins). */
|
|
2814
|
-
async loadCompacted() {
|
|
2815
|
-
const records = /* @__PURE__ */ new Map();
|
|
2816
|
-
let raw;
|
|
2817
|
-
try {
|
|
2818
|
-
raw = await readFile2(this.catalogPath, "utf8");
|
|
2819
|
-
} catch {
|
|
2820
|
-
return records;
|
|
2821
|
-
}
|
|
2822
|
-
for (const line of raw.split("\n")) {
|
|
2823
|
-
const trimmed = line.trim();
|
|
2824
|
-
if (trimmed.length === 0) continue;
|
|
2825
|
-
let parsed;
|
|
2826
|
-
try {
|
|
2827
|
-
parsed = JSON.parse(trimmed);
|
|
2828
|
-
} catch {
|
|
2829
|
-
continue;
|
|
2830
|
-
}
|
|
2831
|
-
const record = coerceRecord(parsed);
|
|
2832
|
-
if (!record) continue;
|
|
2833
|
-
const prior = records.get(record.namespace);
|
|
2834
|
-
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
2835
|
-
}
|
|
2836
|
-
return records;
|
|
2837
|
-
}
|
|
2838
|
-
/**
|
|
2839
|
-
* Serialize an arbitrary read-modify-write critical section through the single
|
|
2840
|
-
* write chain. Every catalog mutation (touch read+merge+append, full rewrite)
|
|
2841
|
-
* runs through this so they are mutually exclusive: a touch always reads the
|
|
2842
|
-
* latest persisted state before appending, and a rebuild rewrite cannot
|
|
2843
|
-
* interleave with a touch's append. The chain recovers from rejection
|
|
2844
|
-
* (CLAUDE.md rule #40) — one failed section never poisons subsequent ones —
|
|
2845
|
-
* while still surfacing the error to that section's awaited promise.
|
|
2846
|
-
*/
|
|
2847
|
-
queueCritical(fn) {
|
|
2848
|
-
const run = this.writeChain.then(fn);
|
|
2849
|
-
this.writeChain = run.then(
|
|
2850
|
-
() => void 0,
|
|
2851
|
-
() => void 0
|
|
2852
|
-
);
|
|
2853
|
-
return run;
|
|
2854
|
-
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Append a single record to the JSONL log WITHOUT re-serializing through the
|
|
2857
|
-
* write chain. MUST only be called from inside a `queueCritical(...)` section
|
|
2858
|
-
* (which already holds the serialized turn); calling it directly would bypass
|
|
2859
|
-
* the read-before-append ordering that prevents lost-field races.
|
|
2860
|
-
*/
|
|
2861
|
-
async appendUnchained(record) {
|
|
2862
|
-
const line = serializeRecord(record) + "\n";
|
|
2863
|
-
await mkdir2(this.stateDir, { recursive: true });
|
|
2864
|
-
await appendFile(this.catalogPath, line, "utf8");
|
|
2865
|
-
}
|
|
2866
|
-
/**
|
|
2867
|
-
* Atomic temp-file + rename rewrite (CLAUDE.md rule #54: write temp, then
|
|
2868
|
-
* rename — never delete-before-write) WITHOUT re-entering the write chain.
|
|
2869
|
-
* MUST only be called from inside a `queueCritical(...)` turn (e.g. the
|
|
2870
|
-
* rebuild critical section, which already holds the serialized turn so its
|
|
2871
|
-
* load and rewrite are atomic against concurrent touches). Re-entering the
|
|
2872
|
-
* chain from within a held turn would deadlock.
|
|
2873
|
-
*/
|
|
2874
|
-
async rewriteUnchained(records) {
|
|
2875
|
-
const body = records.map((r) => serializeRecord(r)).join("\n") + (records.length > 0 ? "\n" : "");
|
|
2876
|
-
await mkdir2(this.stateDir, { recursive: true });
|
|
2877
|
-
const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2878
|
-
await writeFile2(tmp, body, "utf8");
|
|
2879
|
-
await rename(tmp, this.catalogPath);
|
|
2880
|
-
}
|
|
2881
|
-
};
|
|
2882
|
-
function isPathInside(root, child) {
|
|
2883
|
-
const relative = path2.relative(root, child);
|
|
2884
|
-
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
// src/maintenance/namespace-planner.ts
|
|
2888
|
-
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2889
|
-
import { lstat as lstat2, mkdir as mkdir3, open as open2, readFile as readFile3, readdir as readdir2, rename as rename2, rm, rmdir, utimes as utimes2, writeFile as writeFile3 } from "fs/promises";
|
|
2890
|
-
import path3 from "path";
|
|
2891
|
-
var DEFAULT_MAX_NAMESPACES_PER_CYCLE = 20;
|
|
2892
|
-
var DEFAULT_LOCK_STALE_MS = 10 * 6e4;
|
|
2893
|
-
var LOCK_BASE = "maintenance-locks";
|
|
2894
|
-
var STATUS_BASE = "namespace-maintenance-status";
|
|
2895
|
-
var namespaceMaintenanceFs = { open: open2, rm };
|
|
2896
|
-
function configuredNamespaces(config) {
|
|
2897
|
-
return Array.from(
|
|
2898
|
-
new Set(
|
|
2899
|
-
[config.defaultNamespace, config.sharedNamespace, ...config.namespacePolicies.map((policy) => policy.name)].map((value) => value.trim()).filter(Boolean)
|
|
2900
|
-
)
|
|
2901
|
-
);
|
|
2902
|
-
}
|
|
2903
|
-
function inferConfiguredKind(config, namespace) {
|
|
2904
|
-
if (namespace === config.defaultNamespace.trim()) return "default";
|
|
2905
|
-
if (namespace === config.sharedNamespace.trim()) return "shared";
|
|
2906
|
-
return "explicit";
|
|
2907
|
-
}
|
|
2908
|
-
function maxNamespacesPerCycle(config) {
|
|
2909
|
-
return Math.max(
|
|
2910
|
-
1,
|
|
2911
|
-
Math.floor(
|
|
2912
|
-
typeof config.maintenanceMaxNamespacesPerCycle === "number" && Number.isFinite(config.maintenanceMaxNamespacesPerCycle) ? config.maintenanceMaxNamespacesPerCycle : DEFAULT_MAX_NAMESPACES_PER_CYCLE
|
|
2913
|
-
)
|
|
2914
|
-
);
|
|
2915
|
-
}
|
|
2916
|
-
function namespaceKindAllowed(config, kind) {
|
|
2917
|
-
switch (kind) {
|
|
2918
|
-
case "branch":
|
|
2919
|
-
return config.maintenanceIncludeBranchNamespaces === true;
|
|
2920
|
-
case "project":
|
|
2921
|
-
return config.maintenanceIncludeProjectNamespaces !== false;
|
|
2922
|
-
case "team-project":
|
|
2923
|
-
return config.maintenanceIncludeTeamProjectNamespaces !== false;
|
|
2924
|
-
default:
|
|
2925
|
-
return true;
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
function disabledReasonForKind(kind) {
|
|
2929
|
-
if (kind === "branch") return "branch_disabled";
|
|
2930
|
-
if (kind === "project") return "project_disabled";
|
|
2931
|
-
if (kind === "team-project") return "team_project_disabled";
|
|
2932
|
-
return "fanout_disabled";
|
|
2933
|
-
}
|
|
2934
|
-
async function catalogRootIsLive(config, record) {
|
|
2935
|
-
if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
|
|
2936
|
-
return false;
|
|
2937
|
-
}
|
|
2938
|
-
try {
|
|
2939
|
-
const liveRoot = await resolveNamespaceStorageRoot(config, record.namespace);
|
|
2940
|
-
if (path3.resolve(liveRoot) !== path3.resolve(record.storageDir)) return false;
|
|
2941
|
-
return hasMemoryData(liveRoot);
|
|
2942
|
-
} catch {
|
|
2943
|
-
return false;
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
function candidateSortKey(candidate) {
|
|
2947
|
-
const write = candidate.lastWriteAt ?? "";
|
|
2948
|
-
return `${write}\0${candidate.namespace}`;
|
|
2949
|
-
}
|
|
2950
|
-
function candidatePriority(candidate) {
|
|
2951
|
-
if (candidate.kind === "default") return 0;
|
|
2952
|
-
if (candidate.kind === "shared") return 1;
|
|
2953
|
-
if (candidate.source === "configured") return 2;
|
|
2954
|
-
if (candidate.kind === "team-project") return 3;
|
|
2955
|
-
if (candidate.kind === "project") return 4;
|
|
2956
|
-
if (candidate.kind === "self") return 5;
|
|
2957
|
-
if (candidate.kind === "legacy") return 6;
|
|
2958
|
-
if (candidate.kind === "branch") return 8;
|
|
2959
|
-
return 7;
|
|
2960
|
-
}
|
|
2961
|
-
function sortCandidates(a, b) {
|
|
2962
|
-
const priority = candidatePriority(a) - candidatePriority(b);
|
|
2963
|
-
if (priority !== 0) return priority;
|
|
2964
|
-
const am = Date.parse(a.lastMaintenanceAt ?? "");
|
|
2965
|
-
const bm = Date.parse(b.lastMaintenanceAt ?? "");
|
|
2966
|
-
const aMaintained = Number.isFinite(am);
|
|
2967
|
-
const bMaintained = Number.isFinite(bm);
|
|
2968
|
-
if (aMaintained && bMaintained && am !== bm) return am - bm;
|
|
2969
|
-
if (aMaintained !== bMaintained) return aMaintained ? 1 : -1;
|
|
2970
|
-
const aw = Date.parse(a.lastWriteAt ?? "");
|
|
2971
|
-
const bw = Date.parse(b.lastWriteAt ?? "");
|
|
2972
|
-
const aValid = Number.isFinite(aw);
|
|
2973
|
-
const bValid = Number.isFinite(bw);
|
|
2974
|
-
if (aValid && bValid && aw !== bw) return bw - aw;
|
|
2975
|
-
if (aValid !== bValid) return aValid ? -1 : 1;
|
|
2976
|
-
const byKey = candidateSortKey(a).localeCompare(candidateSortKey(b));
|
|
2977
|
-
if (byKey !== 0) return byKey;
|
|
2978
|
-
return a.namespace.localeCompare(b.namespace);
|
|
2979
|
-
}
|
|
2980
|
-
async function planNamespaceMaintenance(config, options) {
|
|
2981
|
-
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2982
|
-
const configured = configuredNamespaces(config);
|
|
2983
|
-
const byNamespace = /* @__PURE__ */ new Map();
|
|
2984
|
-
const skipped = [];
|
|
2985
|
-
for (const namespace of configured) {
|
|
2986
|
-
const kind = inferConfiguredKind(config, namespace);
|
|
2987
|
-
byNamespace.set(namespace, {
|
|
2988
|
-
namespace,
|
|
2989
|
-
kind,
|
|
2990
|
-
source: "configured"
|
|
2991
|
-
});
|
|
2992
|
-
}
|
|
2993
|
-
if (config.namespacesEnabled && config.maintenanceNamespaceFanoutEnabled !== false) {
|
|
2994
|
-
const configuredSet = new Set(configured);
|
|
2995
|
-
try {
|
|
2996
|
-
const records = options.catalog?.enabled ? await options.catalog.listNamespaces() : [];
|
|
2997
|
-
for (const record of records) {
|
|
2998
|
-
const namespace = record.namespace.trim();
|
|
2999
|
-
if (!namespace) continue;
|
|
3000
|
-
const isConfigured = configuredSet.has(namespace);
|
|
3001
|
-
const kind = isConfigured ? inferConfiguredKind(config, namespace) : record.kind;
|
|
3002
|
-
if (!namespaceKindAllowed(config, kind)) {
|
|
3003
|
-
skipped.push({
|
|
3004
|
-
namespace,
|
|
3005
|
-
kind,
|
|
3006
|
-
reason: disabledReasonForKind(kind)
|
|
3007
|
-
});
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
if (!isConfigured && !await catalogRootIsLive(config, record)) {
|
|
3011
|
-
skipped.push({
|
|
3012
|
-
namespace,
|
|
3013
|
-
kind,
|
|
3014
|
-
reason: "unsafe_or_stale_root"
|
|
3015
|
-
});
|
|
3016
|
-
continue;
|
|
3017
|
-
}
|
|
3018
|
-
byNamespace.set(namespace, {
|
|
3019
|
-
namespace,
|
|
3020
|
-
kind,
|
|
3021
|
-
storageDir: record.storageDir,
|
|
3022
|
-
source: isConfigured ? "configured" : "catalog",
|
|
3023
|
-
lastWriteAt: record.lastWriteAt,
|
|
3024
|
-
lastMaintenanceAt: record.lastMaintenanceAt?.[options.jobName]
|
|
3025
|
-
});
|
|
3026
|
-
}
|
|
3027
|
-
} catch (error) {
|
|
3028
|
-
skipped.push({
|
|
3029
|
-
namespace: "*",
|
|
3030
|
-
reason: "catalog_read_failed",
|
|
3031
|
-
detail: error instanceof Error ? error.message : String(error)
|
|
3032
|
-
});
|
|
3033
|
-
}
|
|
3034
|
-
} else if (config.namespacesEnabled) {
|
|
3035
|
-
skipped.push({
|
|
3036
|
-
namespace: "*",
|
|
3037
|
-
reason: "fanout_disabled"
|
|
3038
|
-
});
|
|
3039
|
-
}
|
|
3040
|
-
if (options.budgetMode !== "unbounded") {
|
|
3041
|
-
const latestStatusAtByNamespace = await readLatestStatusAtByNamespace(config, options.jobName);
|
|
3042
|
-
for (const candidate of byNamespace.values()) {
|
|
3043
|
-
if (!candidate.lastMaintenanceAt) {
|
|
3044
|
-
candidate.lastMaintenanceAt = latestStatusAtByNamespace.get(candidate.namespace);
|
|
3045
|
-
}
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
const candidates = [...byNamespace.values()].filter((candidate) => namespaceKindAllowed(config, candidate.kind)).sort(sortCandidates);
|
|
3049
|
-
const max = maxNamespacesPerCycle(config);
|
|
3050
|
-
const applyCycleBudget = options.budgetMode !== "unbounded";
|
|
3051
|
-
const selected = applyCycleBudget ? candidates.slice(0, max) : candidates;
|
|
3052
|
-
if (applyCycleBudget) {
|
|
3053
|
-
for (const candidate of candidates.slice(max)) {
|
|
3054
|
-
skipped.push({
|
|
3055
|
-
namespace: candidate.namespace,
|
|
3056
|
-
kind: candidate.kind,
|
|
3057
|
-
reason: "budget_exhausted"
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
return {
|
|
3062
|
-
jobName: options.jobName,
|
|
3063
|
-
generatedAt,
|
|
3064
|
-
namespaces: selected,
|
|
3065
|
-
skipped,
|
|
3066
|
-
budget: {
|
|
3067
|
-
maxNamespacesPerCycle: max,
|
|
3068
|
-
selected: selected.length
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
}
|
|
3072
|
-
function stablePathSegment(value) {
|
|
3073
|
-
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 128) || "unnamed";
|
|
3074
|
-
if (sanitized.length <= 128 && sanitized === value) return sanitized;
|
|
3075
|
-
return `${sanitized.slice(0, 80)}-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
|
|
3076
|
-
}
|
|
3077
|
-
function namespacePathSegment(namespace) {
|
|
3078
|
-
const token = namespaceIdentityToken(namespace);
|
|
3079
|
-
if (token.length <= 160) return token;
|
|
3080
|
-
return `ns-${createHash("sha256").update(namespace).digest("hex")}`;
|
|
3081
|
-
}
|
|
3082
|
-
function lockPath(config, jobName, namespace) {
|
|
3083
|
-
return path3.join(
|
|
3084
|
-
config.memoryDir,
|
|
3085
|
-
"state",
|
|
3086
|
-
LOCK_BASE,
|
|
3087
|
-
stablePathSegment(jobName),
|
|
3088
|
-
`${namespacePathSegment(namespace)}.lock`
|
|
3089
|
-
);
|
|
3090
|
-
}
|
|
3091
|
-
function namespaceMaintenanceLockStaleMs(config) {
|
|
3092
|
-
if (typeof config.maintenanceNamespaceLockStaleMs === "number" && Number.isFinite(config.maintenanceNamespaceLockStaleMs) && config.maintenanceNamespaceLockStaleMs > 0) {
|
|
3093
|
-
return Math.floor(config.maintenanceNamespaceLockStaleMs);
|
|
3094
|
-
}
|
|
3095
|
-
return DEFAULT_LOCK_STALE_MS;
|
|
3096
|
-
}
|
|
3097
|
-
function namespaceMaintenanceLockHeartbeatMs(config) {
|
|
3098
|
-
const staleMs = namespaceMaintenanceLockStaleMs(config);
|
|
3099
|
-
return Math.max(1, Math.min(3e4, Math.floor(staleMs / 3) || 1));
|
|
3100
|
-
}
|
|
3101
|
-
function errorCode(error) {
|
|
3102
|
-
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
3103
|
-
}
|
|
3104
|
-
async function withNamespaceMaintenanceLockHeartbeat(config, locks, task) {
|
|
3105
|
-
const activeLocks = Array.isArray(locks) ? locks : [locks];
|
|
3106
|
-
const interval = setInterval(() => {
|
|
3107
|
-
for (const lock of activeLocks) {
|
|
3108
|
-
void lock.touch().catch(() => void 0);
|
|
3109
|
-
}
|
|
3110
|
-
}, namespaceMaintenanceLockHeartbeatMs(config));
|
|
3111
|
-
interval.unref?.();
|
|
3112
|
-
try {
|
|
3113
|
-
return await task();
|
|
3114
|
-
} finally {
|
|
3115
|
-
clearInterval(interval);
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
async function removeStaleLockDirectory(filePath) {
|
|
3119
|
-
let entries;
|
|
3120
|
-
try {
|
|
3121
|
-
entries = await readdir2(filePath, { withFileTypes: true });
|
|
3122
|
-
} catch (error) {
|
|
3123
|
-
if (errorCode(error) === "ENOENT") return;
|
|
3124
|
-
throw error;
|
|
3125
|
-
}
|
|
3126
|
-
for (const entry of entries) {
|
|
3127
|
-
if (!entry.isFile()) continue;
|
|
3128
|
-
await namespaceMaintenanceFs.rm(path3.join(filePath, entry.name), { force: true });
|
|
3129
|
-
}
|
|
3130
|
-
try {
|
|
3131
|
-
await rmdir(filePath);
|
|
3132
|
-
} catch (error) {
|
|
3133
|
-
if (errorCode(error) === "ENOENT") return;
|
|
3134
|
-
throw error;
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
async function tryAcquireNamespaceMaintenanceLock(config, jobName, namespace) {
|
|
3138
|
-
const filePath = lockPath(config, jobName, namespace);
|
|
3139
|
-
await mkdir3(path3.dirname(filePath), { recursive: true });
|
|
3140
|
-
try {
|
|
3141
|
-
const lockId = randomUUID2();
|
|
3142
|
-
await mkdir3(filePath);
|
|
3143
|
-
const ownerPath = path3.join(filePath, `${lockId}.json`);
|
|
3144
|
-
let handle;
|
|
3145
|
-
try {
|
|
3146
|
-
handle = await namespaceMaintenanceFs.open(ownerPath, "wx");
|
|
3147
|
-
await handle.writeFile(
|
|
3148
|
-
`${JSON.stringify({
|
|
3149
|
-
lockId,
|
|
3150
|
-
pid: process.pid,
|
|
3151
|
-
jobName,
|
|
3152
|
-
namespace,
|
|
3153
|
-
acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3154
|
-
})}
|
|
3155
|
-
`,
|
|
3156
|
-
"utf8"
|
|
3157
|
-
);
|
|
3158
|
-
await handle.close();
|
|
3159
|
-
} catch (setupError) {
|
|
3160
|
-
await handle?.close().catch(() => void 0);
|
|
3161
|
-
await namespaceMaintenanceFs.rm(ownerPath, { force: true }).catch(() => void 0);
|
|
3162
|
-
await rmdir(filePath).catch(() => void 0);
|
|
3163
|
-
throw setupError;
|
|
3164
|
-
}
|
|
3165
|
-
return {
|
|
3166
|
-
path: filePath,
|
|
3167
|
-
async touch() {
|
|
3168
|
-
try {
|
|
3169
|
-
const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
|
|
3170
|
-
if (parsed.lockId === lockId) {
|
|
3171
|
-
const now = /* @__PURE__ */ new Date();
|
|
3172
|
-
await utimes2(ownerPath, now, now);
|
|
3173
|
-
await utimes2(filePath, now, now);
|
|
3174
|
-
}
|
|
3175
|
-
} catch {
|
|
3176
|
-
}
|
|
3177
|
-
},
|
|
3178
|
-
async release() {
|
|
3179
|
-
try {
|
|
3180
|
-
const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
|
|
3181
|
-
if (parsed.lockId === lockId) {
|
|
3182
|
-
await namespaceMaintenanceFs.rm(ownerPath, { force: true });
|
|
3183
|
-
await rmdir(filePath).catch(() => void 0);
|
|
3184
|
-
}
|
|
3185
|
-
} catch {
|
|
3186
|
-
}
|
|
1416
|
+
async transcriptMemories(options = {}) {
|
|
1417
|
+
if (options.date !== void 0 && !isValidTranscriptDate(options.date)) {
|
|
1418
|
+
throw new WearablesInputError(`invalid date '${options.date}' \u2014 expected YYYY-MM-DD`);
|
|
1419
|
+
}
|
|
1420
|
+
if (options.source !== void 0) assertValidSourceId(options.source);
|
|
1421
|
+
const limit = clampLimit(
|
|
1422
|
+
options.limit,
|
|
1423
|
+
MEMORY_LIST_DEFAULT_LIMIT,
|
|
1424
|
+
MEMORY_LIST_MAX_LIMIT,
|
|
1425
|
+
"limit"
|
|
1426
|
+
);
|
|
1427
|
+
const storage = await this.deps.getStorage();
|
|
1428
|
+
const memories = await storage.readAllMemories();
|
|
1429
|
+
const results = [];
|
|
1430
|
+
for (const memory of memories) {
|
|
1431
|
+
const source = memory.frontmatter.source;
|
|
1432
|
+
if (typeof source !== "string" || !source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`)) {
|
|
1433
|
+
continue;
|
|
3187
1434
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
const s = await lstat2(filePath);
|
|
3194
|
-
if (s.isSymbolicLink()) {
|
|
3195
|
-
return null;
|
|
3196
|
-
}
|
|
3197
|
-
if ((s.isFile() || s.isDirectory()) && Date.now() - s.mtimeMs > staleMs) {
|
|
3198
|
-
try {
|
|
3199
|
-
if (s.isDirectory()) {
|
|
3200
|
-
await removeStaleLockDirectory(filePath);
|
|
3201
|
-
} else {
|
|
3202
|
-
await namespaceMaintenanceFs.rm(filePath, { force: true });
|
|
3203
|
-
}
|
|
3204
|
-
} catch (removeError) {
|
|
3205
|
-
if (errorCode(removeError) === "ENOENT") {
|
|
3206
|
-
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
3207
|
-
}
|
|
3208
|
-
if (errorCode(removeError) === "ENOTEMPTY") {
|
|
3209
|
-
return null;
|
|
3210
|
-
}
|
|
3211
|
-
throw removeError;
|
|
3212
|
-
}
|
|
3213
|
-
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
3214
|
-
}
|
|
3215
|
-
} catch (statError) {
|
|
3216
|
-
if (errorCode(statError) === "ENOENT") {
|
|
3217
|
-
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
1435
|
+
const attrs = memory.frontmatter.structuredAttributes ?? {};
|
|
1436
|
+
const sourceId = attrs.wearableSource;
|
|
1437
|
+
if (options.source !== void 0) {
|
|
1438
|
+
if (sourceId !== options.source && source !== wearableSourceLabel(options.source) && source !== `${wearableSourceLabel(options.source)}:native`) {
|
|
1439
|
+
continue;
|
|
3218
1440
|
}
|
|
3219
|
-
throw statError;
|
|
3220
1441
|
}
|
|
3221
|
-
|
|
1442
|
+
if (options.date !== void 0 && attrs.wearableDate !== options.date) {
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
results.push({
|
|
1446
|
+
id: memory.frontmatter.id,
|
|
1447
|
+
source: sourceId ?? source,
|
|
1448
|
+
date: attrs.wearableDate,
|
|
1449
|
+
conversationId: attrs.wearableConversationId,
|
|
1450
|
+
status: memory.frontmatter.status,
|
|
1451
|
+
content: memory.content,
|
|
1452
|
+
created: memory.frontmatter.created
|
|
1453
|
+
});
|
|
3222
1454
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
}
|
|
3232
|
-
function lastRanStatusPath(config, jobName, namespace) {
|
|
3233
|
-
return path3.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.last-ran.json`);
|
|
3234
|
-
}
|
|
3235
|
-
function parseStatus(value) {
|
|
3236
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3237
|
-
const v = value;
|
|
3238
|
-
if (typeof v.namespace === "string" && typeof v.jobName === "string" && (v.state === "ran" || v.state === "skipped" || v.state === "failed") && typeof v.startedAt === "string" && typeof v.completedAt === "string") {
|
|
3239
|
-
return v;
|
|
3240
|
-
}
|
|
3241
|
-
return null;
|
|
3242
|
-
}
|
|
3243
|
-
async function readLatestStatusAtByNamespace(config, jobName) {
|
|
3244
|
-
const latest = /* @__PURE__ */ new Map();
|
|
3245
|
-
const latestMs = /* @__PURE__ */ new Map();
|
|
3246
|
-
for (const status of [...await readStatusFiles(config), ...await readLastRanStatusFiles(config)]) {
|
|
3247
|
-
if (status.state !== "ran") continue;
|
|
3248
|
-
if (status.jobName !== jobName) continue;
|
|
3249
|
-
const completedAtMs = Date.parse(status.completedAt);
|
|
3250
|
-
if (!Number.isFinite(completedAtMs)) continue;
|
|
3251
|
-
const previousMs = latestMs.get(status.namespace);
|
|
3252
|
-
if (previousMs !== void 0 && previousMs >= completedAtMs) continue;
|
|
3253
|
-
latestMs.set(status.namespace, completedAtMs);
|
|
3254
|
-
latest.set(status.namespace, status.completedAt);
|
|
3255
|
-
}
|
|
3256
|
-
return latest;
|
|
3257
|
-
}
|
|
3258
|
-
async function readStatusFile(filePath) {
|
|
3259
|
-
try {
|
|
3260
|
-
const parsed = JSON.parse(await readFile3(filePath, "utf8"));
|
|
3261
|
-
return parseStatus(parsed);
|
|
3262
|
-
} catch {
|
|
3263
|
-
return null;
|
|
3264
|
-
}
|
|
3265
|
-
}
|
|
3266
|
-
async function readStatusFiles(config) {
|
|
3267
|
-
if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
|
|
3268
|
-
return [];
|
|
1455
|
+
results.sort((a, b) => {
|
|
1456
|
+
if (a.created > b.created) return -1;
|
|
1457
|
+
if (a.created < b.created) return 1;
|
|
1458
|
+
if (a.id < b.id) return -1;
|
|
1459
|
+
if (a.id > b.id) return 1;
|
|
1460
|
+
return 0;
|
|
1461
|
+
});
|
|
1462
|
+
return results.slice(0, limit);
|
|
3269
1463
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
jobDirs = await readdir2(root, { withFileTypes: true });
|
|
3275
|
-
} catch {
|
|
3276
|
-
return statuses;
|
|
1464
|
+
// -- speakers -------------------------------------------------------------
|
|
1465
|
+
async listSpeakers() {
|
|
1466
|
+
const storage = await this.deps.getStorage();
|
|
1467
|
+
return loadSpeakerRegistry(storage.dir);
|
|
3277
1468
|
}
|
|
3278
|
-
|
|
3279
|
-
if (
|
|
3280
|
-
|
|
3281
|
-
try {
|
|
3282
|
-
files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
|
|
3283
|
-
} catch {
|
|
3284
|
-
continue;
|
|
1469
|
+
async setSpeaker(sourceId, speakerKey, name, opts = {}) {
|
|
1470
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
1471
|
+
throw new WearablesInputError("speaker name must be a non-empty string");
|
|
3285
1472
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
if (file.name.endsWith(".last-ran.json")) continue;
|
|
3289
|
-
const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
|
|
3290
|
-
if (status) statuses.push(status);
|
|
1473
|
+
if (typeof speakerKey !== "string" || speakerKey.trim().length === 0) {
|
|
1474
|
+
throw new WearablesInputError("speaker key must be a non-empty string");
|
|
3291
1475
|
}
|
|
1476
|
+
const storage = await this.deps.getStorage();
|
|
1477
|
+
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1478
|
+
registry.speakers[speakerRegistryKey(sourceId, speakerKey.trim())] = {
|
|
1479
|
+
name: name.trim(),
|
|
1480
|
+
...opts.isSelf === true ? { isSelf: true } : {},
|
|
1481
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1482
|
+
};
|
|
1483
|
+
await saveSpeakerRegistry(storage.dir, registry);
|
|
1484
|
+
return registry;
|
|
3292
1485
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
|
|
3297
|
-
return [];
|
|
3298
|
-
}
|
|
3299
|
-
const root = statusBasePath(config);
|
|
3300
|
-
const statuses = [];
|
|
3301
|
-
let jobDirs;
|
|
3302
|
-
try {
|
|
3303
|
-
jobDirs = await readdir2(root, { withFileTypes: true });
|
|
3304
|
-
} catch {
|
|
3305
|
-
return statuses;
|
|
3306
|
-
}
|
|
3307
|
-
for (const jobDir of jobDirs) {
|
|
3308
|
-
if (!jobDir.isDirectory()) continue;
|
|
3309
|
-
let files;
|
|
3310
|
-
try {
|
|
3311
|
-
files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
|
|
3312
|
-
} catch {
|
|
3313
|
-
continue;
|
|
3314
|
-
}
|
|
3315
|
-
for (const file of files) {
|
|
3316
|
-
if (!file.isFile() || !file.name.endsWith(".last-ran.json")) continue;
|
|
3317
|
-
const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
|
|
3318
|
-
if (status) statuses.push(status);
|
|
1486
|
+
async setSelfName(name) {
|
|
1487
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
1488
|
+
throw new WearablesInputError("self name must be a non-empty string");
|
|
3319
1489
|
}
|
|
1490
|
+
const storage = await this.deps.getStorage();
|
|
1491
|
+
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1492
|
+
registry.selfName = name.trim();
|
|
1493
|
+
await saveSpeakerRegistry(storage.dir, registry);
|
|
1494
|
+
return registry;
|
|
3320
1495
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
const payload = {
|
|
3328
|
-
version: 1,
|
|
3329
|
-
...status
|
|
3330
|
-
};
|
|
3331
|
-
await writeFile3(temp, `${JSON.stringify(payload, null, 2)}
|
|
3332
|
-
`, "utf8");
|
|
3333
|
-
await rename2(temp, target);
|
|
3334
|
-
}
|
|
3335
|
-
async function writeStatusFile(config, status) {
|
|
3336
|
-
await writeStatusPayload(statusPath(config, status.jobName, status.namespace), status);
|
|
3337
|
-
if (status.state === "ran") {
|
|
3338
|
-
await writeStatusPayload(lastRanStatusPath(config, status.jobName, status.namespace), status);
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
async function recordNamespaceMaintenanceStatusSafely(config, status) {
|
|
3342
|
-
try {
|
|
3343
|
-
await writeStatusFile(config, status);
|
|
3344
|
-
} catch {
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
|
-
function maintenanceErrorDetail(error) {
|
|
3348
|
-
return displayErrorDetail(error) || "Error";
|
|
3349
|
-
}
|
|
3350
|
-
async function runNamespaceMaintenanceBatchPlan(config, plan, runner, catalog, options = {}) {
|
|
3351
|
-
const statuses = [];
|
|
3352
|
-
for (const skipped of plan.skipped) {
|
|
3353
|
-
if (skipped.namespace === "*") continue;
|
|
3354
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3355
|
-
const status = {
|
|
3356
|
-
namespace: skipped.namespace,
|
|
3357
|
-
jobName: plan.jobName,
|
|
3358
|
-
state: "skipped",
|
|
3359
|
-
reason: skipped.reason,
|
|
3360
|
-
startedAt: now,
|
|
3361
|
-
completedAt: now
|
|
3362
|
-
};
|
|
3363
|
-
statuses.push(status);
|
|
3364
|
-
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3365
|
-
}
|
|
3366
|
-
const acquired = [];
|
|
3367
|
-
try {
|
|
3368
|
-
for (const candidate of plan.namespaces) {
|
|
3369
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3370
|
-
const lock = await tryAcquireNamespaceMaintenanceLock(config, plan.jobName, candidate.namespace);
|
|
3371
|
-
if (!lock) {
|
|
3372
|
-
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3373
|
-
const status = {
|
|
3374
|
-
namespace: candidate.namespace,
|
|
3375
|
-
jobName: plan.jobName,
|
|
3376
|
-
state: "skipped",
|
|
3377
|
-
reason: "lock_held",
|
|
3378
|
-
startedAt,
|
|
3379
|
-
completedAt
|
|
3380
|
-
};
|
|
3381
|
-
statuses.push(status);
|
|
3382
|
-
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3383
|
-
continue;
|
|
3384
|
-
}
|
|
3385
|
-
acquired.push({ candidate, lock, startedAt });
|
|
3386
|
-
}
|
|
3387
|
-
} catch (error) {
|
|
3388
|
-
await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
|
|
3389
|
-
throw error;
|
|
3390
|
-
}
|
|
3391
|
-
if (options.requireAllLocks && acquired.length > 0 && acquired.length < plan.namespaces.length) {
|
|
3392
|
-
for (const { candidate, startedAt } of acquired) {
|
|
3393
|
-
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3394
|
-
const status = {
|
|
3395
|
-
namespace: candidate.namespace,
|
|
3396
|
-
jobName: plan.jobName,
|
|
3397
|
-
state: "skipped",
|
|
3398
|
-
reason: "batch_lock_incomplete",
|
|
3399
|
-
startedAt,
|
|
3400
|
-
completedAt
|
|
3401
|
-
};
|
|
3402
|
-
statuses.push(status);
|
|
3403
|
-
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
1496
|
+
async removeSpeaker(sourceId, speakerKey) {
|
|
1497
|
+
const storage = await this.deps.getStorage();
|
|
1498
|
+
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1499
|
+
const key = speakerRegistryKey(sourceId, speakerKey.trim());
|
|
1500
|
+
if (!(key in registry.speakers)) {
|
|
1501
|
+
throw new WearablesInputError(`no speaker override stored for '${key}'`);
|
|
3404
1502
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3409
|
-
ran: statuses.filter((s) => s.state === "ran").length,
|
|
3410
|
-
skipped: statuses.filter((s) => s.state === "skipped").length,
|
|
3411
|
-
failed: statuses.filter((s) => s.state === "failed").length,
|
|
3412
|
-
statuses
|
|
3413
|
-
};
|
|
1503
|
+
delete registry.speakers[key];
|
|
1504
|
+
await saveSpeakerRegistry(storage.dir, registry);
|
|
1505
|
+
return registry;
|
|
3414
1506
|
}
|
|
3415
|
-
|
|
1507
|
+
// -- corrections ----------------------------------------------------------
|
|
1508
|
+
async listCorrections() {
|
|
1509
|
+
const storage = await this.deps.getStorage();
|
|
3416
1510
|
return {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
skipped: statuses.filter((s) => s.state === "skipped").length,
|
|
3421
|
-
failed: statuses.filter((s) => s.state === "failed").length,
|
|
3422
|
-
statuses
|
|
1511
|
+
fromConfig: this.deps.config.corrections,
|
|
1512
|
+
fromState: await loadCorrectionsFile(storage.dir),
|
|
1513
|
+
stateFilePath: correctionsFilePath(storage.dir)
|
|
3423
1514
|
};
|
|
3424
1515
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
1516
|
+
async addCorrection(rule) {
|
|
1517
|
+
compileCorrectionRule(rule, "correction");
|
|
1518
|
+
const storage = await this.deps.getStorage();
|
|
1519
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1520
|
+
const duplicate = rules.some(
|
|
1521
|
+
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
3430
1522
|
);
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
jobName: plan.jobName,
|
|
3436
|
-
state: "ran",
|
|
3437
|
-
startedAt,
|
|
3438
|
-
completedAt,
|
|
3439
|
-
itemCount: itemCountForNamespace(result, candidate.namespace)
|
|
3440
|
-
};
|
|
3441
|
-
statuses.push(status);
|
|
3442
|
-
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3443
|
-
try {
|
|
3444
|
-
await catalog?.markMaintenance(candidate.namespace, plan.jobName, new Date(completedAt));
|
|
3445
|
-
} catch {
|
|
3446
|
-
}
|
|
1523
|
+
if (duplicate) {
|
|
1524
|
+
throw new WearablesInputError(
|
|
1525
|
+
`an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
|
|
1526
|
+
);
|
|
3447
1527
|
}
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
jobName: plan.jobName,
|
|
3455
|
-
state: "skipped",
|
|
3456
|
-
reason: skipReason,
|
|
3457
|
-
startedAt,
|
|
3458
|
-
completedAt
|
|
3459
|
-
} : {
|
|
3460
|
-
namespace: candidate.namespace,
|
|
3461
|
-
jobName: plan.jobName,
|
|
3462
|
-
state: "failed",
|
|
3463
|
-
reason: "job_failed",
|
|
3464
|
-
startedAt,
|
|
3465
|
-
completedAt,
|
|
3466
|
-
error: maintenanceErrorDetail(error)
|
|
3467
|
-
};
|
|
3468
|
-
statuses.push(status);
|
|
3469
|
-
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
1528
|
+
rules.push(rule);
|
|
1529
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1530
|
+
}
|
|
1531
|
+
async removeCorrection(index) {
|
|
1532
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
1533
|
+
throw new WearablesInputError(`invalid correction index '${index}'`);
|
|
3470
1534
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
1535
|
+
const storage = await this.deps.getStorage();
|
|
1536
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1537
|
+
if (index >= rules.length) {
|
|
1538
|
+
throw new WearablesInputError(
|
|
1539
|
+
`correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
const [removed] = rules.splice(index, 1);
|
|
1543
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1544
|
+
return removed;
|
|
3473
1545
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
};
|
|
3482
|
-
}
|
|
3483
|
-
function itemCountForNamespace(result, namespace) {
|
|
3484
|
-
const itemCounts = result?.itemCounts;
|
|
3485
|
-
if (itemCounts instanceof Map) return itemCounts.get(namespace);
|
|
3486
|
-
if (itemCounts && Object.prototype.hasOwnProperty.call(itemCounts, namespace)) {
|
|
3487
|
-
return itemCounts[namespace];
|
|
1546
|
+
};
|
|
1547
|
+
function clampLimit(value, fallback, max, label) {
|
|
1548
|
+
if (value === void 0) return fallback;
|
|
1549
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
|
|
1550
|
+
throw new WearablesInputError(
|
|
1551
|
+
`invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
|
|
1552
|
+
);
|
|
3488
1553
|
}
|
|
3489
|
-
return
|
|
1554
|
+
return value;
|
|
1555
|
+
}
|
|
1556
|
+
function locateTranscriptPath(hitPath) {
|
|
1557
|
+
const normalized = hitPath.replace(/\\/g, "/");
|
|
1558
|
+
const match = normalized.match(
|
|
1559
|
+
/(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
|
|
1560
|
+
);
|
|
1561
|
+
if (!match) return null;
|
|
1562
|
+
if (!isValidTranscriptDate(match[2])) return null;
|
|
1563
|
+
return { source: match[1], date: match[2] };
|
|
1564
|
+
}
|
|
1565
|
+
function extractSnippet(body, index, matchLength) {
|
|
1566
|
+
const start = Math.max(0, index - 80);
|
|
1567
|
+
const end = Math.min(body.length, index + matchLength + 80);
|
|
1568
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
1569
|
+
const suffix = end < body.length ? "\u2026" : "";
|
|
1570
|
+
return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
3490
1571
|
}
|
|
3491
1572
|
|
|
3492
1573
|
// src/scopes/scope-plan.ts
|
|
@@ -3625,7 +1706,7 @@ function flattenStructuredSectionEvidence(sections) {
|
|
|
3625
1706
|
);
|
|
3626
1707
|
}
|
|
3627
1708
|
function fingerprintEntitySynthesisEvidence(entity) {
|
|
3628
|
-
const fingerprint =
|
|
1709
|
+
const fingerprint = createHash("sha256");
|
|
3629
1710
|
const timelineEntries = entity.timeline.map((entry) => [
|
|
3630
1711
|
entry.timestamp,
|
|
3631
1712
|
entry.source ?? "",
|
|
@@ -3675,7 +1756,7 @@ async function raceRecallAbort(promise, signal, message = "recall aborted") {
|
|
|
3675
1756
|
}
|
|
3676
1757
|
}
|
|
3677
1758
|
function qmdCollectionPathParts(resultPath) {
|
|
3678
|
-
if (!resultPath ||
|
|
1759
|
+
if (!resultPath || path2.isAbsolute(resultPath)) return null;
|
|
3679
1760
|
const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3680
1761
|
const slashIndex = normalized.indexOf("/");
|
|
3681
1762
|
if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
|
|
@@ -3688,9 +1769,9 @@ function qmdCollectionPathParts(resultPath) {
|
|
|
3688
1769
|
}
|
|
3689
1770
|
function qmdResultPathCandidates(storageDir, resultPath) {
|
|
3690
1771
|
const candidates = /* @__PURE__ */ new Set();
|
|
3691
|
-
const storageRoot =
|
|
1772
|
+
const storageRoot = path2.resolve(storageDir);
|
|
3692
1773
|
const addCandidate = (candidate) => {
|
|
3693
|
-
const resolved =
|
|
1774
|
+
const resolved = path2.resolve(candidate);
|
|
3694
1775
|
if (isPathInsideStorageRoot(storageRoot, resolved)) {
|
|
3695
1776
|
candidates.add(resolved);
|
|
3696
1777
|
}
|
|
@@ -3698,12 +1779,12 @@ function qmdResultPathCandidates(storageDir, resultPath) {
|
|
|
3698
1779
|
const addRelativeCandidates = (relativePath) => {
|
|
3699
1780
|
const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3700
1781
|
if (!normalized) return;
|
|
3701
|
-
addCandidate(
|
|
1782
|
+
addCandidate(path2.join(storageRoot, normalized));
|
|
3702
1783
|
if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
|
|
3703
|
-
addCandidate(
|
|
1784
|
+
addCandidate(path2.join(storageRoot, "facts", normalized));
|
|
3704
1785
|
}
|
|
3705
1786
|
};
|
|
3706
|
-
if (
|
|
1787
|
+
if (path2.isAbsolute(resultPath)) {
|
|
3707
1788
|
addCandidate(resultPath);
|
|
3708
1789
|
} else {
|
|
3709
1790
|
addRelativeCandidates(resultPath);
|
|
@@ -3820,11 +1901,11 @@ async function qmdStartupCollectionCheckWithTimeout(promise, controller, label)
|
|
|
3820
1901
|
return await Promise.race([checkedPromise, timeoutPromise]);
|
|
3821
1902
|
}
|
|
3822
1903
|
function defaultWorkspaceDir() {
|
|
3823
|
-
return
|
|
1904
|
+
return path2.join(os.homedir(), ".openclaw", "workspace");
|
|
3824
1905
|
}
|
|
3825
1906
|
function sanitizeSessionKeyForFilename(sessionKey) {
|
|
3826
1907
|
const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3827
|
-
const hash =
|
|
1908
|
+
const hash = createHash("sha256").update(sessionKey).digest("hex").slice(0, 12);
|
|
3828
1909
|
return `${readable}-${hash}`;
|
|
3829
1910
|
}
|
|
3830
1911
|
function latestSourceValidAtFromTurns(turns) {
|
|
@@ -4127,11 +2208,11 @@ function mergeGraphExpandedResults(primary, expanded) {
|
|
|
4127
2208
|
return Array.from(mergedByPath.values());
|
|
4128
2209
|
}
|
|
4129
2210
|
function graphPathRelativeToStorage(storageDir, candidatePath) {
|
|
4130
|
-
const absolutePath =
|
|
4131
|
-
const rel =
|
|
2211
|
+
const absolutePath = path2.isAbsolute(candidatePath) ? candidatePath : path2.resolve(storageDir, candidatePath);
|
|
2212
|
+
const rel = path2.relative(storageDir, absolutePath);
|
|
4132
2213
|
if (!rel || rel === ".") return null;
|
|
4133
2214
|
if (rel.startsWith("..")) return null;
|
|
4134
|
-
return rel.split(
|
|
2215
|
+
return rel.split(path2.sep).join("/");
|
|
4135
2216
|
}
|
|
4136
2217
|
function normalizeGraphActivationScore(score) {
|
|
4137
2218
|
const bounded = Number.isFinite(score) && score > 0 ? score : 0;
|
|
@@ -4273,7 +2354,7 @@ function buildMemoryPathById(allMemsForGraph, storageDir) {
|
|
|
4273
2354
|
for (const mem of allMemsForGraph ?? []) {
|
|
4274
2355
|
const id = mem.frontmatter.id;
|
|
4275
2356
|
if (!id) continue;
|
|
4276
|
-
pathById.set(id,
|
|
2357
|
+
pathById.set(id, path2.relative(storageDir, mem.path));
|
|
4277
2358
|
}
|
|
4278
2359
|
return pathById;
|
|
4279
2360
|
}
|
|
@@ -4281,7 +2362,7 @@ function appendMemoryToGraphContext(options) {
|
|
|
4281
2362
|
if (!Array.isArray(options.allMemsForGraph)) return;
|
|
4282
2363
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4283
2364
|
options.allMemsForGraph.push({
|
|
4284
|
-
path:
|
|
2365
|
+
path: path2.join(options.storageDir, options.memoryRelPath),
|
|
4285
2366
|
content: options.content,
|
|
4286
2367
|
frontmatter: {
|
|
4287
2368
|
id: options.memoryId,
|
|
@@ -4301,16 +2382,16 @@ function resolvePersistedMemoryRelativePath(options) {
|
|
|
4301
2382
|
const persisted = options.pathById.get(options.memoryId);
|
|
4302
2383
|
if (persisted) return persisted;
|
|
4303
2384
|
if (options.category === "correction") {
|
|
4304
|
-
return
|
|
2385
|
+
return path2.join("corrections", `${options.memoryId}.md`);
|
|
4305
2386
|
}
|
|
4306
2387
|
const subtree = categoryDirName(options.category);
|
|
4307
2388
|
const idParts = options.memoryId.split("-");
|
|
4308
2389
|
const maybeTimestamp = Number(idParts[1]);
|
|
4309
2390
|
if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
|
|
4310
2391
|
const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
|
|
4311
|
-
return
|
|
2392
|
+
return path2.join(subtree, day, `${options.memoryId}.md`);
|
|
4312
2393
|
}
|
|
4313
|
-
return
|
|
2394
|
+
return path2.join(subtree, `${options.memoryId}.md`);
|
|
4314
2395
|
}
|
|
4315
2396
|
function qmdMaintenanceSkipReasonForError(error) {
|
|
4316
2397
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4745,7 +2826,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
4745
2826
|
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4746
2827
|
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
|
|
4747
2828
|
if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
|
|
4748
|
-
const resolvedStorageDir =
|
|
2829
|
+
const resolvedStorageDir = path2.resolve(storageDir);
|
|
4749
2830
|
let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
4750
2831
|
if (!hints) {
|
|
4751
2832
|
hints = /* @__PURE__ */ new Set();
|
|
@@ -4756,21 +2837,21 @@ var Orchestrator = class _Orchestrator {
|
|
|
4756
2837
|
storageDirMatchesNamespaceHint(namespace, storageDir) {
|
|
4757
2838
|
const ns = normalizeNamespaceIdentity(namespace);
|
|
4758
2839
|
if (!ns) return false;
|
|
4759
|
-
const resolvedStorageDir =
|
|
4760
|
-
const resolvedMemoryDir =
|
|
2840
|
+
const resolvedStorageDir = path2.resolve(storageDir);
|
|
2841
|
+
const resolvedMemoryDir = path2.resolve(this.config.memoryDir);
|
|
4761
2842
|
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4762
2843
|
if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
|
|
4763
|
-
const resolvedNamespacesDir =
|
|
2844
|
+
const resolvedNamespacesDir = path2.join(resolvedMemoryDir, "namespaces");
|
|
4764
2845
|
if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
|
|
4765
|
-
const rawRoot =
|
|
4766
|
-
const tokenRoot =
|
|
2846
|
+
const rawRoot = path2.resolve(resolvedNamespacesDir, ns);
|
|
2847
|
+
const tokenRoot = path2.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
|
|
4767
2848
|
return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
|
|
4768
2849
|
}
|
|
4769
2850
|
namespaceStorageDirHintOwnershipRank(record, resolvedStorageDir, configured) {
|
|
4770
|
-
if (resolvedStorageDir ===
|
|
2851
|
+
if (resolvedStorageDir === path2.resolve(this.config.memoryDir)) {
|
|
4771
2852
|
return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace) ? 0 : 3;
|
|
4772
2853
|
}
|
|
4773
|
-
const leaf =
|
|
2854
|
+
const leaf = path2.basename(resolvedStorageDir);
|
|
4774
2855
|
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
4775
2856
|
if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
|
|
4776
2857
|
if (record.namespace === leaf) return 1;
|
|
@@ -4798,7 +2879,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
4798
2879
|
loadNamespaceStorageDirHintsFromCatalog() {
|
|
4799
2880
|
if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
|
|
4800
2881
|
this.namespaceStorageDirHintsLoaded = true;
|
|
4801
|
-
const catalogPath =
|
|
2882
|
+
const catalogPath = path2.join(this.config.memoryDir, "state", "namespaces.jsonl");
|
|
4802
2883
|
if (!existsSync(catalogPath)) return;
|
|
4803
2884
|
let body;
|
|
4804
2885
|
try {
|
|
@@ -4835,7 +2916,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
4835
2916
|
if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
|
|
4836
2917
|
continue;
|
|
4837
2918
|
}
|
|
4838
|
-
const resolvedStorageDir =
|
|
2919
|
+
const resolvedStorageDir = path2.resolve(record.storageDir);
|
|
4839
2920
|
const current = preferredByStorageDir.get(resolvedStorageDir);
|
|
4840
2921
|
preferredByStorageDir.set(
|
|
4841
2922
|
resolvedStorageDir,
|
|
@@ -4874,6 +2955,36 @@ var Orchestrator = class _Orchestrator {
|
|
|
4874
2955
|
});
|
|
4875
2956
|
return plan.namespaces.map((candidate) => candidate.namespace);
|
|
4876
2957
|
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Fan out a maintenance job across all maintained namespaces (issue #1500).
|
|
2960
|
+
*
|
|
2961
|
+
* Delegates to the namespace-maintenance-fanout coordinator, which plans
|
|
2962
|
+
* namespace discovery (configured + catalog), applies the cycle budget,
|
|
2963
|
+
* acquires per-job+namespace locks, records status files, and touches the
|
|
2964
|
+
* catalog's `lastMaintenanceAt`. The runner receives a per-namespace
|
|
2965
|
+
* candidate and a storage resolver wired to `this.getStorage(namespace)`.
|
|
2966
|
+
*
|
|
2967
|
+
* When namespaces are disabled the planner returns only the default
|
|
2968
|
+
* namespace, so the job runs exactly once — preserving single-user behavior.
|
|
2969
|
+
*/
|
|
2970
|
+
async runNamespaceMaintenanceFanoutForJob(jobName, runner, options = {}) {
|
|
2971
|
+
return runNamespaceMaintenanceFanout({
|
|
2972
|
+
config: this.config,
|
|
2973
|
+
catalog: this.namespaceCatalog,
|
|
2974
|
+
jobName,
|
|
2975
|
+
runner,
|
|
2976
|
+
resolveStorage: (namespace) => this.getStorage(namespace),
|
|
2977
|
+
enabled: options.enabled
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* Read-only namespace maintenance health summary for doctor / dashboard /
|
|
2982
|
+
* CLI (issue #1500). Aggregates all per-namespace maintenance status files
|
|
2983
|
+
* into one report without running any maintenance.
|
|
2984
|
+
*/
|
|
2985
|
+
async readNamespaceMaintenanceHealth() {
|
|
2986
|
+
return summarizeNamespaceMaintenanceHealth(this.config);
|
|
2987
|
+
}
|
|
4877
2988
|
buildConfiguredQmdSearchOptions(queryText) {
|
|
4878
2989
|
const intentHint = this.config.qmdIntentHintsEnabled ? buildQmdIntentHint(inferIntentFromText(queryText)) : void 0;
|
|
4879
2990
|
const explain = this.config.qmdExplainEnabled === true;
|
|
@@ -5002,7 +3113,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5002
3113
|
this.config = config;
|
|
5003
3114
|
this.profiler = new ProfilingCollector({
|
|
5004
3115
|
enabled: config.profilingEnabled,
|
|
5005
|
-
storageDir: config.profilingStorageDir ||
|
|
3116
|
+
storageDir: config.profilingStorageDir || path2.join(config.memoryDir, "profiling"),
|
|
5006
3117
|
maxTraces: config.profilingMaxTraces
|
|
5007
3118
|
});
|
|
5008
3119
|
this.namespaceCatalog = new NamespaceCatalog(config);
|
|
@@ -5048,7 +3159,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5048
3159
|
this.compounding = config.compoundingEnabled ? new CompoundingEngine(config, this.storage) : void 0;
|
|
5049
3160
|
this.buffer = new SmartBuffer(config, this.storage);
|
|
5050
3161
|
this.transcript = new TranscriptManager(config);
|
|
5051
|
-
this.conversationIndexDir =
|
|
3162
|
+
this.conversationIndexDir = path2.join(
|
|
5052
3163
|
config.memoryDir,
|
|
5053
3164
|
"conversation-index",
|
|
5054
3165
|
"chunks"
|
|
@@ -5105,7 +3216,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5105
3216
|
this.modelRegistry
|
|
5106
3217
|
);
|
|
5107
3218
|
this.threading = new ThreadingManager(
|
|
5108
|
-
|
|
3219
|
+
path2.join(config.memoryDir, "threads"),
|
|
5109
3220
|
config.threadingGapMinutes
|
|
5110
3221
|
);
|
|
5111
3222
|
this.tmtBuilder = new TmtBuilder(config.memoryDir, {
|
|
@@ -5184,7 +3295,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5184
3295
|
utilityPromoteThresholdDelta: this.utilityRuntimeValues?.promoteThresholdDelta ?? 0,
|
|
5185
3296
|
utilityDemoteThresholdDelta: this.utilityRuntimeValues?.demoteThresholdDelta ?? 0
|
|
5186
3297
|
};
|
|
5187
|
-
return
|
|
3298
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 12);
|
|
5188
3299
|
}
|
|
5189
3300
|
effectiveLifecycleThresholds() {
|
|
5190
3301
|
const archiveDecayThreshold = this.config.lifecycleArchiveDecayThreshold;
|
|
@@ -5404,13 +3515,13 @@ var Orchestrator = class _Orchestrator {
|
|
|
5404
3515
|
if (this.config.compactionResetEnabled) {
|
|
5405
3516
|
try {
|
|
5406
3517
|
const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
|
|
5407
|
-
const files = await
|
|
3518
|
+
const files = await readdir(wsDir).catch(() => []);
|
|
5408
3519
|
for (const f of files) {
|
|
5409
3520
|
if (!f.startsWith(".compaction-reset-signal-")) continue;
|
|
5410
|
-
const fp =
|
|
5411
|
-
const s = await
|
|
3521
|
+
const fp = path2.join(wsDir, f);
|
|
3522
|
+
const s = await stat(fp).catch(() => null);
|
|
5412
3523
|
if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
|
|
5413
|
-
await
|
|
3524
|
+
await unlink(fp).catch(() => {
|
|
5414
3525
|
});
|
|
5415
3526
|
log.debug(`initialize: removed stale compaction signal ${f}`);
|
|
5416
3527
|
}
|
|
@@ -5665,7 +3776,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5665
3776
|
if (signal.aborted) return;
|
|
5666
3777
|
if (!this.wearablesAutoSyncHandle && this.config.wearables.enabled && this.config.wearables.autoSyncEnabled && Object.values(this.config.wearables.sources).some((source) => source.enabled)) {
|
|
5667
3778
|
try {
|
|
5668
|
-
const { startWearablesAutoSync } = await import("./auto-sync-
|
|
3779
|
+
const { startWearablesAutoSync } = await import("./auto-sync-RWXT6P5M.js");
|
|
5669
3780
|
if (signal.aborted) return;
|
|
5670
3781
|
this.wearablesAutoSyncHandle = startWearablesAutoSync(
|
|
5671
3782
|
{
|
|
@@ -5686,9 +3797,9 @@ var Orchestrator = class _Orchestrator {
|
|
|
5686
3797
|
`wearables auto-sync started: every ${this.config.wearables.autoSyncIntervalMinutes}m over ${this.config.wearables.autoSyncDays}d (deep ${this.config.wearables.autoSyncDeepDays}d daily)`
|
|
5687
3798
|
);
|
|
5688
3799
|
} catch (err) {
|
|
5689
|
-
const { displayErrorDetail
|
|
3800
|
+
const { displayErrorDetail } = await import("./runtime/better-sqlite.js");
|
|
5690
3801
|
log.warn(
|
|
5691
|
-
`wearables auto-sync failed to start (non-fatal): ${
|
|
3802
|
+
`wearables auto-sync failed to start (non-fatal): ${displayErrorDetail(err)}`
|
|
5692
3803
|
);
|
|
5693
3804
|
}
|
|
5694
3805
|
}
|
|
@@ -5801,7 +3912,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5801
3912
|
*/
|
|
5802
3913
|
async autoRegisterDaySummaryCron() {
|
|
5803
3914
|
const home = resolveHomeDir();
|
|
5804
|
-
const jobsPath =
|
|
3915
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5805
3916
|
try {
|
|
5806
3917
|
if (!existsSync(jobsPath)) {
|
|
5807
3918
|
log.debug(
|
|
@@ -5854,7 +3965,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5854
3965
|
}
|
|
5855
3966
|
async autoRegisterNightlyGovernanceCron() {
|
|
5856
3967
|
const home = resolveHomeDir();
|
|
5857
|
-
const jobsPath =
|
|
3968
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5858
3969
|
try {
|
|
5859
3970
|
if (!existsSync(jobsPath)) {
|
|
5860
3971
|
log.debug("nightly governance cron: jobs.json not found, skipping auto-register");
|
|
@@ -5876,7 +3987,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5876
3987
|
}
|
|
5877
3988
|
async autoRegisterProceduralMiningCron() {
|
|
5878
3989
|
const home = resolveHomeDir();
|
|
5879
|
-
const jobsPath =
|
|
3990
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5880
3991
|
try {
|
|
5881
3992
|
if (!existsSync(jobsPath)) {
|
|
5882
3993
|
log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
|
|
@@ -5896,7 +4007,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5896
4007
|
}
|
|
5897
4008
|
async autoRegisterContradictionScanCron() {
|
|
5898
4009
|
const home = resolveHomeDir();
|
|
5899
|
-
const jobsPath =
|
|
4010
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5900
4011
|
try {
|
|
5901
4012
|
if (!existsSync(jobsPath)) {
|
|
5902
4013
|
log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
|
|
@@ -5916,7 +4027,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
5916
4027
|
}
|
|
5917
4028
|
async autoRegisterPatternReinforcementCron() {
|
|
5918
4029
|
const home = resolveHomeDir();
|
|
5919
|
-
const jobsPath =
|
|
4030
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5920
4031
|
try {
|
|
5921
4032
|
if (!existsSync(jobsPath)) {
|
|
5922
4033
|
log.debug("pattern reinforcement cron: jobs.json not found, skipping auto-register");
|
|
@@ -5976,9 +4087,92 @@ var Orchestrator = class _Orchestrator {
|
|
|
5976
4087
|
);
|
|
5977
4088
|
return { ran: true, result, namespace: cadenceKey };
|
|
5978
4089
|
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Fan out pattern reinforcement across all maintained namespaces (issue #1500).
|
|
4092
|
+
* Delegates per-namespace execution to {@link runPatternReinforcement} while
|
|
4093
|
+
* the planner handles discovery, budgeting, locking, and status recording.
|
|
4094
|
+
* When namespaces are disabled, runs once against default storage.
|
|
4095
|
+
*/
|
|
4096
|
+
async runPatternReinforcementFanout(options = {}) {
|
|
4097
|
+
return this.runNamespaceMaintenanceFanoutForJob(
|
|
4098
|
+
"pattern-reinforcement",
|
|
4099
|
+
async (ctx) => {
|
|
4100
|
+
const result = await this.runPatternReinforcement({
|
|
4101
|
+
namespace: ctx.candidate.namespace,
|
|
4102
|
+
force: options.force
|
|
4103
|
+
});
|
|
4104
|
+
if (!result.ran) {
|
|
4105
|
+
return {
|
|
4106
|
+
skipped: true,
|
|
4107
|
+
skipReason: result.skippedReason ?? "throttled"
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
return result.result ? { itemCount: result.result.clustersFound } : { itemCount: 0 };
|
|
4111
|
+
},
|
|
4112
|
+
{ enabled: this.config.patternReinforcementEnabled }
|
|
4113
|
+
);
|
|
4114
|
+
}
|
|
4115
|
+
/**
|
|
4116
|
+
* Fan out lifecycle/governance policy across all maintained namespaces
|
|
4117
|
+
* (issue #1500). Each namespace gets its own lifecycle pass against its
|
|
4118
|
+
* namespace-scoped storage. When namespaces are disabled, runs once against
|
|
4119
|
+
* default storage.
|
|
4120
|
+
*/
|
|
4121
|
+
async runLifecyclePolicyFanout() {
|
|
4122
|
+
return this.runNamespaceMaintenanceFanoutForJob(
|
|
4123
|
+
"lifecycle",
|
|
4124
|
+
async (ctx) => {
|
|
4125
|
+
const storage = await this.getStorage(ctx.candidate.namespace);
|
|
4126
|
+
const corpus = await storage.readAllMemories();
|
|
4127
|
+
const assessed = await this.runLifecyclePolicyPass(corpus, storage);
|
|
4128
|
+
return { itemCount: assessed };
|
|
4129
|
+
},
|
|
4130
|
+
{ enabled: this.config.lifecyclePolicyEnabled }
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Fan out semantic consolidation across all maintained namespaces (issue #1500).
|
|
4135
|
+
* Each namespace gets its own consolidation pass against its namespace-scoped
|
|
4136
|
+
* storage. When namespaces are disabled, runs once against default storage.
|
|
4137
|
+
*/
|
|
4138
|
+
async runSemanticConsolidationFanout(options = {}) {
|
|
4139
|
+
return this.runNamespaceMaintenanceFanoutForJob(
|
|
4140
|
+
"semantic-consolidation",
|
|
4141
|
+
async (ctx) => {
|
|
4142
|
+
const storage = await this.getStorage(ctx.candidate.namespace);
|
|
4143
|
+
const result = await this.runSemanticConsolidation({
|
|
4144
|
+
dryRun: options.dryRun,
|
|
4145
|
+
thresholdOverride: void 0,
|
|
4146
|
+
force: true,
|
|
4147
|
+
storage
|
|
4148
|
+
});
|
|
4149
|
+
return { itemCount: result.clustersFound };
|
|
4150
|
+
},
|
|
4151
|
+
{ enabled: this.config.semanticConsolidationEnabled }
|
|
4152
|
+
);
|
|
4153
|
+
}
|
|
4154
|
+
/**
|
|
4155
|
+
* Fan out deep-sleep governance across all maintained namespaces (issue #1500).
|
|
4156
|
+
* Each namespace gets its own governance scan against its namespace-scoped
|
|
4157
|
+
* storage. When namespaces are disabled, runs once against default storage.
|
|
4158
|
+
*/
|
|
4159
|
+
async runDeepSleepGovernanceFanout(options = {}) {
|
|
4160
|
+
return this.runNamespaceMaintenanceFanoutForJob(
|
|
4161
|
+
"governance",
|
|
4162
|
+
async (ctx) => {
|
|
4163
|
+
const storage = await this.getStorage(ctx.candidate.namespace);
|
|
4164
|
+
const result = await this.runDeepSleepGovernanceNow({
|
|
4165
|
+
dryRun: options.dryRun,
|
|
4166
|
+
storage
|
|
4167
|
+
});
|
|
4168
|
+
return { itemCount: result.scannedMemories };
|
|
4169
|
+
},
|
|
4170
|
+
{ enabled: this.config.dreamsPhases.deepSleep.enabled }
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
5979
4173
|
async autoRegisterGraphEdgeDecayCron() {
|
|
5980
4174
|
const home = resolveHomeDir();
|
|
5981
|
-
const jobsPath =
|
|
4175
|
+
const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
|
|
5982
4176
|
try {
|
|
5983
4177
|
if (!existsSync(jobsPath)) {
|
|
5984
4178
|
log.debug("graph edge decay cron: jobs.json not found, skipping auto-register");
|
|
@@ -6035,15 +4229,15 @@ ${doc.content}` : doc.content,
|
|
|
6035
4229
|
this.lastFileHygieneRunAtMs = now;
|
|
6036
4230
|
if (hygiene.rotateEnabled) {
|
|
6037
4231
|
for (const rel of hygiene.rotatePaths) {
|
|
6038
|
-
const abs =
|
|
4232
|
+
const abs = path2.isAbsolute(rel) ? rel : path2.join(this.config.workspaceDir, rel);
|
|
6039
4233
|
try {
|
|
6040
|
-
const raw = await
|
|
4234
|
+
const raw = await readFile2(abs, "utf-8");
|
|
6041
4235
|
if (raw.length > hygiene.rotateMaxBytes) {
|
|
6042
|
-
const archiveDir =
|
|
4236
|
+
const archiveDir = path2.join(
|
|
6043
4237
|
this.config.workspaceDir,
|
|
6044
4238
|
hygiene.archiveDir
|
|
6045
4239
|
);
|
|
6046
|
-
const base =
|
|
4240
|
+
const base = path2.basename(abs);
|
|
6047
4241
|
const prefix = base.toUpperCase().replace(/\.MD$/i, "").replace(/[^A-Z0-9]+/g, "-") || "FILE";
|
|
6048
4242
|
const { newContent } = await rotateMarkdownFileToArchive({
|
|
6049
4243
|
filePath: abs,
|
|
@@ -6051,7 +4245,7 @@ ${doc.content}` : doc.content,
|
|
|
6051
4245
|
archivePrefix: prefix,
|
|
6052
4246
|
keepTailChars: hygiene.rotateKeepTailChars
|
|
6053
4247
|
});
|
|
6054
|
-
await
|
|
4248
|
+
await writeFile2(abs, newContent, "utf-8");
|
|
6055
4249
|
}
|
|
6056
4250
|
} catch {
|
|
6057
4251
|
}
|
|
@@ -6068,8 +4262,8 @@ ${doc.content}` : doc.content,
|
|
|
6068
4262
|
log.warn(w.message);
|
|
6069
4263
|
}
|
|
6070
4264
|
if (hygiene.warningsLogEnabled && warnings.length > 0) {
|
|
6071
|
-
const fp =
|
|
6072
|
-
await
|
|
4265
|
+
const fp = path2.join(this.config.memoryDir, hygiene.warningsLogPath);
|
|
4266
|
+
await mkdir2(path2.dirname(fp), { recursive: true });
|
|
6073
4267
|
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6074
4268
|
const block = `
|
|
6075
4269
|
|
|
@@ -6078,11 +4272,11 @@ ${doc.content}` : doc.content,
|
|
|
6078
4272
|
` + warnings.map((w) => `- ${w.message}`).join("\n") + "\n";
|
|
6079
4273
|
let existing = "";
|
|
6080
4274
|
try {
|
|
6081
|
-
existing = await
|
|
4275
|
+
existing = await readFile2(fp, "utf-8");
|
|
6082
4276
|
} catch {
|
|
6083
4277
|
existing = "# Engram File Hygiene Warnings\n";
|
|
6084
4278
|
}
|
|
6085
|
-
await
|
|
4279
|
+
await writeFile2(fp, existing + block, "utf-8");
|
|
6086
4280
|
}
|
|
6087
4281
|
}
|
|
6088
4282
|
}
|
|
@@ -6547,26 +4741,26 @@ ${evidenceText}`
|
|
|
6547
4741
|
const facts = [];
|
|
6548
4742
|
let memoryRootReal = null;
|
|
6549
4743
|
try {
|
|
6550
|
-
memoryRootReal = await
|
|
4744
|
+
memoryRootReal = await realpath(storage.dir);
|
|
6551
4745
|
} catch {
|
|
6552
4746
|
memoryRootReal = null;
|
|
6553
4747
|
}
|
|
6554
4748
|
for (const categoryDir of RECALL_FALLBACK_DIRS) {
|
|
6555
4749
|
if (memoryRootReal === null) break;
|
|
6556
4750
|
for (const date of datesToScan) {
|
|
6557
|
-
const dateDir =
|
|
4751
|
+
const dateDir = path2.join(storage.dir, categoryDir, date);
|
|
6558
4752
|
try {
|
|
6559
|
-
const dirStat = await
|
|
4753
|
+
const dirStat = await lstat(dateDir);
|
|
6560
4754
|
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
6561
|
-
assertPathInsideRoot(memoryRootReal, await
|
|
6562
|
-
const entries = await
|
|
4755
|
+
assertPathInsideRoot(memoryRootReal, await realpath(dateDir), dateDir);
|
|
4756
|
+
const entries = await readdir(dateDir, { withFileTypes: true });
|
|
6563
4757
|
for (const entry of entries) {
|
|
6564
4758
|
if (entry.isSymbolicLink()) continue;
|
|
6565
4759
|
if (!entry.name.endsWith(".md")) continue;
|
|
6566
|
-
const fullPath =
|
|
4760
|
+
const fullPath = path2.join(dateDir, entry.name);
|
|
6567
4761
|
try {
|
|
6568
|
-
assertPathInsideRoot(memoryRootReal, await
|
|
6569
|
-
const raw = await
|
|
4762
|
+
assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
|
|
4763
|
+
const raw = await readFile2(fullPath, "utf-8");
|
|
6570
4764
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
6571
4765
|
if (!fmMatch) continue;
|
|
6572
4766
|
const fmBlock = fmMatch[1];
|
|
@@ -6585,7 +4779,7 @@ ${evidenceText}`
|
|
|
6585
4779
|
facts.push({
|
|
6586
4780
|
path: fullPath,
|
|
6587
4781
|
frontmatter: {
|
|
6588
|
-
id: fm.id ||
|
|
4782
|
+
id: fm.id || path2.basename(entry.name, ".md"),
|
|
6589
4783
|
category: fm.category || "fact",
|
|
6590
4784
|
created,
|
|
6591
4785
|
updated: fm.updated || created,
|
|
@@ -6608,15 +4802,15 @@ ${evidenceText}`
|
|
|
6608
4802
|
return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
|
|
6609
4803
|
});
|
|
6610
4804
|
const hourlySummaries = [];
|
|
6611
|
-
const hourlyBaseDir =
|
|
4805
|
+
const hourlyBaseDir = path2.join(storage.dir, "summaries", "hourly");
|
|
6612
4806
|
try {
|
|
6613
|
-
const sessionKeys = await
|
|
4807
|
+
const sessionKeys = await readdir(hourlyBaseDir, { withFileTypes: true });
|
|
6614
4808
|
for (const sk of sessionKeys) {
|
|
6615
4809
|
if (!sk.isDirectory()) continue;
|
|
6616
4810
|
for (const date of datesToScan) {
|
|
6617
|
-
const summaryFile =
|
|
4811
|
+
const summaryFile = path2.join(hourlyBaseDir, sk.name, `${date}.md`);
|
|
6618
4812
|
try {
|
|
6619
|
-
const raw = await
|
|
4813
|
+
const raw = await readFile2(summaryFile, "utf-8");
|
|
6620
4814
|
const filtered = filterHourlySummaryMarkdownForLocalDay(
|
|
6621
4815
|
raw,
|
|
6622
4816
|
date,
|
|
@@ -6718,13 +4912,13 @@ ${evidenceText}`
|
|
|
6718
4912
|
}
|
|
6719
4913
|
async getLastGraphRecallSnapshot(namespace) {
|
|
6720
4914
|
const storage = await this.getStorage(namespace);
|
|
6721
|
-
const snapshotPath =
|
|
4915
|
+
const snapshotPath = path2.join(
|
|
6722
4916
|
storage.dir,
|
|
6723
4917
|
"state",
|
|
6724
4918
|
"last_graph_recall.json"
|
|
6725
4919
|
);
|
|
6726
4920
|
try {
|
|
6727
|
-
const raw = await
|
|
4921
|
+
const raw = await readFile2(snapshotPath, "utf-8");
|
|
6728
4922
|
const parsed = JSON.parse(raw);
|
|
6729
4923
|
if (!parsed || typeof parsed !== "object") return null;
|
|
6730
4924
|
return {
|
|
@@ -6757,9 +4951,9 @@ ${evidenceText}`
|
|
|
6757
4951
|
}
|
|
6758
4952
|
async getLastIntentSnapshot(namespace) {
|
|
6759
4953
|
const storage = await this.getStorage(namespace);
|
|
6760
|
-
const snapshotPath =
|
|
4954
|
+
const snapshotPath = path2.join(storage.dir, "state", "last_intent.json");
|
|
6761
4955
|
try {
|
|
6762
|
-
const raw = await
|
|
4956
|
+
const raw = await readFile2(snapshotPath, "utf-8");
|
|
6763
4957
|
const parsed = JSON.parse(raw);
|
|
6764
4958
|
if (!parsed || typeof parsed !== "object") return null;
|
|
6765
4959
|
const graphDecision = parsed.graphDecision && typeof parsed.graphDecision === "object" ? parsed.graphDecision : void 0;
|
|
@@ -6790,13 +4984,13 @@ ${evidenceText}`
|
|
|
6790
4984
|
}
|
|
6791
4985
|
async getLastQmdRecallSnapshot(namespace) {
|
|
6792
4986
|
const storage = await this.getStorage(namespace);
|
|
6793
|
-
const snapshotPath =
|
|
4987
|
+
const snapshotPath = path2.join(
|
|
6794
4988
|
storage.dir,
|
|
6795
4989
|
"state",
|
|
6796
4990
|
"last_qmd_recall.json"
|
|
6797
4991
|
);
|
|
6798
4992
|
try {
|
|
6799
|
-
const raw = await
|
|
4993
|
+
const raw = await readFile2(snapshotPath, "utf-8");
|
|
6800
4994
|
const parsed = JSON.parse(raw);
|
|
6801
4995
|
if (!parsed || typeof parsed !== "object") return null;
|
|
6802
4996
|
return {
|
|
@@ -6940,10 +5134,10 @@ ${r.snippet.trim()}
|
|
|
6940
5134
|
}
|
|
6941
5135
|
async countConversationChunkDocs(dir) {
|
|
6942
5136
|
try {
|
|
6943
|
-
const entries = await
|
|
5137
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
6944
5138
|
let total = 0;
|
|
6945
5139
|
for (const entry of entries) {
|
|
6946
|
-
const fullPath =
|
|
5140
|
+
const fullPath = path2.join(dir, entry.name);
|
|
6947
5141
|
if (entry.isDirectory()) {
|
|
6948
5142
|
total += await this.countConversationChunkDocs(fullPath);
|
|
6949
5143
|
continue;
|
|
@@ -7667,7 +5861,7 @@ ${r.snippet.trim()}
|
|
|
7667
5861
|
if (!options.onDebugSnapshot) return;
|
|
7668
5862
|
await options.onDebugSnapshot({
|
|
7669
5863
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7670
|
-
queryHash:
|
|
5864
|
+
queryHash: createHash("sha256").update(prompt).digest("hex"),
|
|
7671
5865
|
queryLength: prompt.length,
|
|
7672
5866
|
collection: options.collection,
|
|
7673
5867
|
namespaces: options.recallNamespaces,
|
|
@@ -7849,7 +6043,7 @@ ${r.snippet.trim()}
|
|
|
7849
6043
|
resolvedPath = resolvedCold.result.path;
|
|
7850
6044
|
resolvedResult = resolvedCold.result;
|
|
7851
6045
|
}
|
|
7852
|
-
if (!
|
|
6046
|
+
if (!path2.isAbsolute(resolvedPath)) {
|
|
7853
6047
|
resolvedAmbiguousSeeds.set(result.path, null);
|
|
7854
6048
|
return null;
|
|
7855
6049
|
}
|
|
@@ -7874,7 +6068,7 @@ ${r.snippet.trim()}
|
|
|
7874
6068
|
}
|
|
7875
6069
|
continue;
|
|
7876
6070
|
}
|
|
7877
|
-
if (
|
|
6071
|
+
if (path2.isAbsolute(result.path)) {
|
|
7878
6072
|
const resolved = await resolveAmbiguousSeedOwner(result, null);
|
|
7879
6073
|
if (resolved) {
|
|
7880
6074
|
addResultForNamespace(resolved.namespace, resolved.result);
|
|
@@ -7917,7 +6111,7 @@ ${r.snippet.trim()}
|
|
|
7917
6111
|
0
|
|
7918
6112
|
);
|
|
7919
6113
|
seedPaths.push(
|
|
7920
|
-
...seedRelativePaths.map((rel) =>
|
|
6114
|
+
...seedRelativePaths.map((rel) => path2.join(storage.dir, rel))
|
|
7921
6115
|
);
|
|
7922
6116
|
const seedSet = new Set(seedRelativePaths);
|
|
7923
6117
|
const expanded = await this.graphIndexFor(storage).spreadingActivation(
|
|
@@ -7933,7 +6127,7 @@ ${r.snippet.trim()}
|
|
|
7933
6127
|
for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
|
|
7934
6128
|
if (deadlineExpired()) break;
|
|
7935
6129
|
if (seedSet.has(candidate.path)) continue;
|
|
7936
|
-
const memoryPath =
|
|
6130
|
+
const memoryPath = path2.resolve(storage.dir, candidate.path);
|
|
7937
6131
|
const memory = await storage.readMemoryByPath(memoryPath);
|
|
7938
6132
|
if (deadlineExpired()) break;
|
|
7939
6133
|
if (!memory) continue;
|
|
@@ -7958,7 +6152,7 @@ ${r.snippet.trim()}
|
|
|
7958
6152
|
path: memory.path,
|
|
7959
6153
|
score,
|
|
7960
6154
|
namespace,
|
|
7961
|
-
seed:
|
|
6155
|
+
seed: path2.resolve(storage.dir, candidate.seed),
|
|
7962
6156
|
hopDepth: candidate.hopDepth,
|
|
7963
6157
|
decayedWeight: candidate.decayedWeight,
|
|
7964
6158
|
graphType: candidate.graphType,
|
|
@@ -7979,12 +6173,12 @@ ${r.snippet.trim()}
|
|
|
7979
6173
|
}
|
|
7980
6174
|
async recordLastGraphRecallSnapshot(options) {
|
|
7981
6175
|
try {
|
|
7982
|
-
const snapshotPath =
|
|
6176
|
+
const snapshotPath = path2.join(
|
|
7983
6177
|
options.storage.dir,
|
|
7984
6178
|
"state",
|
|
7985
6179
|
"last_graph_recall.json"
|
|
7986
6180
|
);
|
|
7987
|
-
await
|
|
6181
|
+
await mkdir2(path2.dirname(snapshotPath), { recursive: true });
|
|
7988
6182
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7989
6183
|
const totalSeedCount = options.seedPaths.length;
|
|
7990
6184
|
const totalExpandedCount = options.expandedPaths.length;
|
|
@@ -7996,7 +6190,7 @@ ${r.snippet.trim()}
|
|
|
7996
6190
|
const payload = {
|
|
7997
6191
|
recordedAt: now,
|
|
7998
6192
|
mode: options.recallMode,
|
|
7999
|
-
queryHash:
|
|
6193
|
+
queryHash: createHash("sha256").update(options.prompt).digest("hex"),
|
|
8000
6194
|
queryLength: options.prompt.length,
|
|
8001
6195
|
namespaces: options.recallNamespaces,
|
|
8002
6196
|
seedCount: totalSeedCount,
|
|
@@ -8011,20 +6205,20 @@ ${r.snippet.trim()}
|
|
|
8011
6205
|
finalResults: (options.finalResults ?? []).slice(0, 64),
|
|
8012
6206
|
shadowComparison: options.shadowComparison
|
|
8013
6207
|
};
|
|
8014
|
-
await
|
|
6208
|
+
await writeFile2(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
8015
6209
|
} catch (err) {
|
|
8016
6210
|
log.debug(`last graph recall write failed: ${err}`);
|
|
8017
6211
|
}
|
|
8018
6212
|
}
|
|
8019
6213
|
async recordLastIntentSnapshot(options) {
|
|
8020
6214
|
try {
|
|
8021
|
-
const snapshotPath =
|
|
6215
|
+
const snapshotPath = path2.join(
|
|
8022
6216
|
options.storage.dir,
|
|
8023
6217
|
"state",
|
|
8024
6218
|
"last_intent.json"
|
|
8025
6219
|
);
|
|
8026
|
-
await
|
|
8027
|
-
await
|
|
6220
|
+
await mkdir2(path2.dirname(snapshotPath), { recursive: true });
|
|
6221
|
+
await writeFile2(
|
|
8028
6222
|
snapshotPath,
|
|
8029
6223
|
JSON.stringify(options.snapshot, null, 2),
|
|
8030
6224
|
"utf-8"
|
|
@@ -8035,13 +6229,13 @@ ${r.snippet.trim()}
|
|
|
8035
6229
|
}
|
|
8036
6230
|
async recordLastQmdRecallSnapshot(options) {
|
|
8037
6231
|
try {
|
|
8038
|
-
const snapshotPath =
|
|
6232
|
+
const snapshotPath = path2.join(
|
|
8039
6233
|
options.storage.dir,
|
|
8040
6234
|
"state",
|
|
8041
6235
|
"last_qmd_recall.json"
|
|
8042
6236
|
);
|
|
8043
|
-
await
|
|
8044
|
-
await
|
|
6237
|
+
await mkdir2(path2.dirname(snapshotPath), { recursive: true });
|
|
6238
|
+
await writeFile2(
|
|
8045
6239
|
snapshotPath,
|
|
8046
6240
|
JSON.stringify(options.snapshot, null, 2),
|
|
8047
6241
|
"utf-8"
|
|
@@ -8055,9 +6249,9 @@ ${r.snippet.trim()}
|
|
|
8055
6249
|
const stateDir = await this.resolveStateDirForNamespace(
|
|
8056
6250
|
options.namespace
|
|
8057
6251
|
);
|
|
8058
|
-
const snapshotPath =
|
|
8059
|
-
await
|
|
8060
|
-
await
|
|
6252
|
+
const snapshotPath = path2.join(stateDir, "last_intent.json");
|
|
6253
|
+
await mkdir2(path2.dirname(snapshotPath), { recursive: true });
|
|
6254
|
+
await writeFile2(
|
|
8061
6255
|
snapshotPath,
|
|
8062
6256
|
JSON.stringify(options.snapshot, null, 2),
|
|
8063
6257
|
"utf-8"
|
|
@@ -8068,24 +6262,24 @@ ${r.snippet.trim()}
|
|
|
8068
6262
|
}
|
|
8069
6263
|
async resolveStateDirForNamespace(namespace) {
|
|
8070
6264
|
if (!this.config.namespacesEnabled) {
|
|
8071
|
-
return
|
|
6265
|
+
return path2.join(this.config.memoryDir, "state");
|
|
8072
6266
|
}
|
|
8073
6267
|
if (namespace !== this.config.defaultNamespace) {
|
|
8074
|
-
return
|
|
6268
|
+
return path2.join(this.config.memoryDir, "namespaces", namespace, "state");
|
|
8075
6269
|
}
|
|
8076
|
-
const candidate =
|
|
6270
|
+
const candidate = path2.join(
|
|
8077
6271
|
this.config.memoryDir,
|
|
8078
6272
|
"namespaces",
|
|
8079
6273
|
this.config.defaultNamespace
|
|
8080
6274
|
);
|
|
8081
6275
|
try {
|
|
8082
|
-
const candidateStat = await
|
|
6276
|
+
const candidateStat = await stat(candidate);
|
|
8083
6277
|
if (candidateStat.isDirectory()) {
|
|
8084
|
-
return
|
|
6278
|
+
return path2.join(candidate, "state");
|
|
8085
6279
|
}
|
|
8086
6280
|
} catch {
|
|
8087
6281
|
}
|
|
8088
|
-
return
|
|
6282
|
+
return path2.join(this.config.memoryDir, "state");
|
|
8089
6283
|
}
|
|
8090
6284
|
buildGraphRecallRankedResults(results, sourceLabelResolver, limit = 64) {
|
|
8091
6285
|
return results.slice(0, limit).map((result) => ({
|
|
@@ -8338,8 +6532,8 @@ ${r.snippet.trim()}
|
|
|
8338
6532
|
timings,
|
|
8339
6533
|
logger: log
|
|
8340
6534
|
});
|
|
8341
|
-
const promptHash =
|
|
8342
|
-
const traceId =
|
|
6535
|
+
const promptHash = createHash("sha256").update(prompt).digest("hex");
|
|
6536
|
+
const traceId = createHash("sha256").update(`${sessionKey ?? "default"}:${recallStart}:${promptHash}`).digest("hex").slice(0, 16);
|
|
8343
6537
|
const sectionBuckets = /* @__PURE__ */ new Map();
|
|
8344
6538
|
const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
|
|
8345
6539
|
cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
|
|
@@ -8348,7 +6542,7 @@ ${r.snippet.trim()}
|
|
|
8348
6542
|
cronConversationRecallMode: this.config.cronConversationRecallMode
|
|
8349
6543
|
});
|
|
8350
6544
|
const retrievalQuery = queryPolicy.retrievalQuery || prompt;
|
|
8351
|
-
const retrievalQueryHash =
|
|
6545
|
+
const retrievalQueryHash = createHash("sha256").update(retrievalQuery).digest("hex");
|
|
8352
6546
|
const policyVersion = this.currentPolicyVersion();
|
|
8353
6547
|
let impressionRecorded = false;
|
|
8354
6548
|
let recallSource = "none";
|
|
@@ -8483,7 +6677,7 @@ ${r.snippet.trim()}
|
|
|
8483
6677
|
const graphExpandedResultPaths = /* @__PURE__ */ new Set();
|
|
8484
6678
|
const graphSourceLabelsForPath = (resultPath) => {
|
|
8485
6679
|
const labels = [];
|
|
8486
|
-
const normalizedPath = resultPath.split(
|
|
6680
|
+
const normalizedPath = resultPath.split(path2.sep).join("/");
|
|
8487
6681
|
const isEntityPath = normalizedPath.startsWith("entities/") || normalizedPath.includes("/entities/");
|
|
8488
6682
|
if (graphBaselinePaths.has(resultPath)) labels.push("baseline");
|
|
8489
6683
|
if (graphExpandedResultPaths.has(resultPath))
|
|
@@ -8633,7 +6827,7 @@ ${r.snippet.trim()}
|
|
|
8633
6827
|
profileStorageNamespaces.map((namespace) => this.storageRouter.storageFor(namespace))
|
|
8634
6828
|
);
|
|
8635
6829
|
const emptyProfileStorage = new Proxy(
|
|
8636
|
-
{ dir:
|
|
6830
|
+
{ dir: path2.join(this.config.memoryDir, ".empty-scope-profile") },
|
|
8637
6831
|
{
|
|
8638
6832
|
get(target, prop) {
|
|
8639
6833
|
if (prop in target) return target[prop];
|
|
@@ -10109,16 +8303,16 @@ ${formatted}`;
|
|
|
10109
8303
|
if (!this.config.compactionResetEnabled) return null;
|
|
10110
8304
|
const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
|
|
10111
8305
|
const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
|
|
10112
|
-
const signalPath =
|
|
8306
|
+
const signalPath = path2.join(
|
|
10113
8307
|
workspaceDir,
|
|
10114
8308
|
`.compaction-reset-signal-${safeSessionKey}`
|
|
10115
8309
|
);
|
|
10116
|
-
const bootPath =
|
|
8310
|
+
const bootPath = path2.join(workspaceDir, "BOOT.md");
|
|
10117
8311
|
try {
|
|
10118
|
-
const signalStat = await
|
|
8312
|
+
const signalStat = await stat(signalPath).catch(() => null);
|
|
10119
8313
|
if (!signalStat) return null;
|
|
10120
8314
|
const signalAge = Date.now() - signalStat.mtimeMs;
|
|
10121
|
-
const signalData = JSON.parse(await
|
|
8315
|
+
const signalData = JSON.parse(await readFile2(signalPath, "utf-8"));
|
|
10122
8316
|
if (signalData.sessionKey !== effectiveSessionKey) {
|
|
10123
8317
|
log.debug(
|
|
10124
8318
|
`recall: compaction signal is for ${signalData.sessionKey}, not ${effectiveSessionKey} \u2014 skipping`
|
|
@@ -10129,7 +8323,7 @@ ${formatted}`;
|
|
|
10129
8323
|
log.debug(
|
|
10130
8324
|
`recall: stale compaction signal (${Math.round(signalAge / 1e3)}s old), skipping`
|
|
10131
8325
|
);
|
|
10132
|
-
await
|
|
8326
|
+
await unlink(signalPath).catch(() => {
|
|
10133
8327
|
});
|
|
10134
8328
|
return null;
|
|
10135
8329
|
}
|
|
@@ -10138,7 +8332,7 @@ ${formatted}`;
|
|
|
10138
8332
|
|
|
10139
8333
|
`;
|
|
10140
8334
|
try {
|
|
10141
|
-
const bootContent = await
|
|
8335
|
+
const bootContent = await readFile2(bootPath, "utf-8");
|
|
10142
8336
|
section += "### BOOT.md (working state before compaction)\n\n";
|
|
10143
8337
|
section += bootContent + "\n";
|
|
10144
8338
|
} catch {
|
|
@@ -10149,12 +8343,12 @@ ${formatted}`;
|
|
|
10149
8343
|
log.info(
|
|
10150
8344
|
`recall: injected compaction reset context for ${effectiveSessionKey}`
|
|
10151
8345
|
);
|
|
10152
|
-
await
|
|
8346
|
+
await unlink(signalPath).catch(() => {
|
|
10153
8347
|
});
|
|
10154
8348
|
return section;
|
|
10155
8349
|
} catch (err) {
|
|
10156
8350
|
log.debug("recall: compaction signal check failed:", err);
|
|
10157
|
-
await
|
|
8351
|
+
await unlink(signalPath).catch(() => {
|
|
10158
8352
|
});
|
|
10159
8353
|
return null;
|
|
10160
8354
|
}
|
|
@@ -12293,7 +10487,7 @@ _Context: ${topQuestion.context}_`
|
|
|
12293
10487
|
const shouldUseStableBatchKey = turns.some(
|
|
12294
10488
|
(turn) => turn.persistProcessedFingerprint === true || typeof turn.turnFingerprint === "string" && turn.turnFingerprint.length > 0
|
|
12295
10489
|
);
|
|
12296
|
-
const stableBatchFingerprint = shouldUseStableBatchKey ?
|
|
10490
|
+
const stableBatchFingerprint = shouldUseStableBatchKey ? createHash("sha256").update(
|
|
12297
10491
|
turns.map(
|
|
12298
10492
|
(turn) => [
|
|
12299
10493
|
turn.role,
|
|
@@ -12550,7 +10744,7 @@ _Context: ${topQuestion.context}_`
|
|
|
12550
10744
|
buildExtractionFingerprint(turns, bufferKey) {
|
|
12551
10745
|
const normalized = this.normalizeExtractionFingerprintTurns(turns).join("\n");
|
|
12552
10746
|
if (!normalized) return null;
|
|
12553
|
-
return
|
|
10747
|
+
return createHash("sha256").update(`${bufferKey}
|
|
12554
10748
|
${normalized}`).digest("hex");
|
|
12555
10749
|
}
|
|
12556
10750
|
shouldQueueExtraction(turns, options = {}) {
|
|
@@ -13075,7 +11269,7 @@ ${normalized}`).digest("hex");
|
|
|
13075
11269
|
);
|
|
13076
11270
|
this.tierMigrationInFlight = true;
|
|
13077
11271
|
try {
|
|
13078
|
-
const coldStorage = new StorageManager(
|
|
11272
|
+
const coldStorage = new StorageManager(path2.join(storage.dir, "cold"));
|
|
13079
11273
|
const [hotMemories, coldMemories] = await Promise.all([
|
|
13080
11274
|
storage.readAllMemories(),
|
|
13081
11275
|
coldStorage.readAllMemories()
|
|
@@ -14491,7 +12685,7 @@ ${normalized}`).digest("hex");
|
|
|
14491
12685
|
const allMems = allMemsForGraph ?? [];
|
|
14492
12686
|
for (const m of allMems) {
|
|
14493
12687
|
if (m.frontmatter.entityRef === entityRef) {
|
|
14494
|
-
const rel =
|
|
12688
|
+
const rel = path2.relative(storage.dir, m.path);
|
|
14495
12689
|
if (rel !== memoryRelPath) entitySiblings.push(rel);
|
|
14496
12690
|
}
|
|
14497
12691
|
}
|
|
@@ -14813,14 +13007,14 @@ ${normalized}`).digest("hex");
|
|
|
14813
13007
|
}
|
|
14814
13008
|
if (this.config.semanticConsolidationEnabled) {
|
|
14815
13009
|
try {
|
|
14816
|
-
const stateFilePath =
|
|
13010
|
+
const stateFilePath = path2.join(
|
|
14817
13011
|
this.config.memoryDir,
|
|
14818
13012
|
"state",
|
|
14819
13013
|
"semantic-consolidation-last-run.json"
|
|
14820
13014
|
);
|
|
14821
13015
|
let shouldRun = true;
|
|
14822
13016
|
try {
|
|
14823
|
-
const stateRaw = await
|
|
13017
|
+
const stateRaw = await readFile2(stateFilePath, "utf-8");
|
|
14824
13018
|
const stateData = JSON.parse(stateRaw);
|
|
14825
13019
|
if (stateData.lastRunAt) {
|
|
14826
13020
|
const lastRunMs = new Date(stateData.lastRunAt).getTime();
|
|
@@ -14861,9 +13055,9 @@ ${normalized}`).digest("hex");
|
|
|
14861
13055
|
);
|
|
14862
13056
|
}
|
|
14863
13057
|
if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
|
|
14864
|
-
const stateDir =
|
|
14865
|
-
await
|
|
14866
|
-
await
|
|
13058
|
+
const stateDir = path2.join(this.config.memoryDir, "state");
|
|
13059
|
+
await mkdir2(stateDir, { recursive: true });
|
|
13060
|
+
await writeFile2(
|
|
14867
13061
|
stateFilePath,
|
|
14868
13062
|
JSON.stringify({ lastRunAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
14869
13063
|
"utf-8"
|
|
@@ -15059,7 +13253,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
15059
13253
|
}
|
|
15060
13254
|
});
|
|
15061
13255
|
const content = renderCompressionGuidelinesMarkdown(refinedCandidate);
|
|
15062
|
-
const contentHash =
|
|
13256
|
+
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
15063
13257
|
const semanticRefinementApplied = JSON.stringify(refinedCandidate.ruleUpdates) !== JSON.stringify(candidate.ruleUpdates);
|
|
15064
13258
|
const changedRules = refinedCandidate.ruleUpdates.filter(
|
|
15065
13259
|
(rule) => rule.delta !== 0
|
|
@@ -15361,13 +13555,13 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
15361
13555
|
protectedCategories: this.config.lifecycleProtectedCategories
|
|
15362
13556
|
}
|
|
15363
13557
|
};
|
|
15364
|
-
const metricsPath =
|
|
13558
|
+
const metricsPath = path2.join(
|
|
15365
13559
|
storage.dir,
|
|
15366
13560
|
"state",
|
|
15367
13561
|
"lifecycle-metrics.json"
|
|
15368
13562
|
);
|
|
15369
|
-
await
|
|
15370
|
-
await
|
|
13563
|
+
await mkdir2(path2.dirname(metricsPath), { recursive: true });
|
|
13564
|
+
await writeFile2(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
|
|
15371
13565
|
return updatedCount;
|
|
15372
13566
|
}
|
|
15373
13567
|
/**
|
|
@@ -15890,7 +14084,7 @@ ${lines.join("\n\n")}`;
|
|
|
15890
14084
|
const seenStorageDirs = /* @__PURE__ */ new Set();
|
|
15891
14085
|
const addStorage = (storage) => {
|
|
15892
14086
|
const storageDir = storageDirFor(storage);
|
|
15893
|
-
const storageKey = storageDir ?
|
|
14087
|
+
const storageKey = storageDir ? path2.resolve(storageDir) : `storage-without-dir-${storages.length}`;
|
|
15894
14088
|
if (seenStorageDirs.has(storageKey)) return;
|
|
15895
14089
|
seenStorageDirs.add(storageKey);
|
|
15896
14090
|
storages.push(storage);
|
|
@@ -15920,7 +14114,7 @@ ${lines.join("\n\n")}`;
|
|
|
15920
14114
|
continue;
|
|
15921
14115
|
}
|
|
15922
14116
|
try {
|
|
15923
|
-
const coldRoot =
|
|
14117
|
+
const coldRoot = path2.join(storageDir, "cold");
|
|
15924
14118
|
for (const candidate of qmdResultPathCandidates(
|
|
15925
14119
|
coldRoot,
|
|
15926
14120
|
parts.relativePath
|
|
@@ -15961,7 +14155,7 @@ ${lines.join("\n\n")}`;
|
|
|
15961
14155
|
return null;
|
|
15962
14156
|
}
|
|
15963
14157
|
}
|
|
15964
|
-
if (
|
|
14158
|
+
if (path2.isAbsolute(resultPath)) {
|
|
15965
14159
|
if (!fallbackStorageDir) {
|
|
15966
14160
|
return await fallbackStorage.readMemoryByPath(resultPath);
|
|
15967
14161
|
}
|
|
@@ -16000,7 +14194,7 @@ ${lines.join("\n\n")}`;
|
|
|
16000
14194
|
);
|
|
16001
14195
|
if (!memory) return null;
|
|
16002
14196
|
let ownerNamespace = null;
|
|
16003
|
-
if (
|
|
14197
|
+
if (path2.isAbsolute(memory.path)) {
|
|
16004
14198
|
const ownerStorage = await this.storageForAbsoluteQmdResultPath(
|
|
16005
14199
|
memory.path,
|
|
16006
14200
|
fallbackStorage,
|
|
@@ -16024,16 +14218,16 @@ ${lines.join("\n\n")}`;
|
|
|
16024
14218
|
};
|
|
16025
14219
|
}
|
|
16026
14220
|
async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
|
|
16027
|
-
const resolvedPath =
|
|
16028
|
-
const memoryRoot =
|
|
16029
|
-
const namespacesRoot =
|
|
14221
|
+
const resolvedPath = path2.resolve(resultPath);
|
|
14222
|
+
const memoryRoot = path2.resolve(this.config.memoryDir);
|
|
14223
|
+
const namespacesRoot = path2.join(memoryRoot, "namespaces");
|
|
16030
14224
|
const fallbackStorageDir = typeof fallbackStorage.dir === "string" && fallbackStorage.dir ? fallbackStorage.dir : null;
|
|
16031
14225
|
const matches = [];
|
|
16032
14226
|
const seenDirs = /* @__PURE__ */ new Set();
|
|
16033
14227
|
const maybeAddStorage = (storage, namespace) => {
|
|
16034
14228
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
16035
14229
|
if (!storageDir) return;
|
|
16036
|
-
const candidateRoot =
|
|
14230
|
+
const candidateRoot = path2.resolve(storageDir);
|
|
16037
14231
|
if (seenDirs.has(candidateRoot)) return;
|
|
16038
14232
|
if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
|
|
16039
14233
|
if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
@@ -16051,7 +14245,7 @@ ${lines.join("\n\n")}`;
|
|
|
16051
14245
|
candidateNamespaces.add(ns);
|
|
16052
14246
|
}
|
|
16053
14247
|
if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
16054
|
-
const relativeToNamespaces =
|
|
14248
|
+
const relativeToNamespaces = path2.relative(namespacesRoot, resolvedPath);
|
|
16055
14249
|
const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
|
|
16056
14250
|
if (namespaceSegment) {
|
|
16057
14251
|
candidateNamespaces.add(
|
|
@@ -16096,7 +14290,7 @@ ${lines.join("\n\n")}`;
|
|
|
16096
14290
|
nsMap = buildMemoryWorthCounterMap(memories);
|
|
16097
14291
|
this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
|
|
16098
14292
|
}
|
|
16099
|
-
for (const [
|
|
14293
|
+
for (const [path3, c] of nsMap) counters.set(path3, c);
|
|
16100
14294
|
} catch (err) {
|
|
16101
14295
|
log.debug("memory-worth: failed to read namespace, skipping", {
|
|
16102
14296
|
namespace: ns,
|
|
@@ -16267,12 +14461,12 @@ ${lines.join("\n\n")}`;
|
|
|
16267
14461
|
*/
|
|
16268
14462
|
semanticDedupScopeFor(targetStorage) {
|
|
16269
14463
|
if (!this.config.namespacesEnabled) return {};
|
|
16270
|
-
const memoryDir =
|
|
16271
|
-
const storageDir =
|
|
14464
|
+
const memoryDir = path2.resolve(this.config.memoryDir);
|
|
14465
|
+
const storageDir = path2.resolve(targetStorage.dir);
|
|
16272
14466
|
if (storageDir === memoryDir) {
|
|
16273
14467
|
return { pathExcludePrefixes: ["namespaces/"] };
|
|
16274
14468
|
}
|
|
16275
|
-
let rel =
|
|
14469
|
+
let rel = path2.relative(memoryDir, storageDir);
|
|
16276
14470
|
if (!rel || rel.startsWith("..")) {
|
|
16277
14471
|
log.debug(
|
|
16278
14472
|
`semantic dedup: target storage dir ${storageDir} is outside memoryDir ${memoryDir}; scoping lookup to absolute path prefix`
|
|
@@ -16291,7 +14485,7 @@ ${lines.join("\n\n")}`;
|
|
|
16291
14485
|
if (hits.length === 0) return [];
|
|
16292
14486
|
const results = [];
|
|
16293
14487
|
for (const hit of hits) {
|
|
16294
|
-
const fullPath =
|
|
14488
|
+
const fullPath = path2.isAbsolute(hit.path) ? hit.path : path2.join(this.config.memoryDir, hit.path);
|
|
16295
14489
|
const memory = await this.storage.readMemoryByPath(fullPath);
|
|
16296
14490
|
if (!memory) continue;
|
|
16297
14491
|
results.push({
|
|
@@ -16495,7 +14689,7 @@ ${lines.join("\n\n")}`;
|
|
|
16495
14689
|
const storage = await this.storageRouter.storageFor(namespace);
|
|
16496
14690
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
16497
14691
|
if (!storageDir) continue;
|
|
16498
|
-
const recallRoot =
|
|
14692
|
+
const recallRoot = path2.resolve(storageDir);
|
|
16499
14693
|
if (seenRecallRoots.has(recallRoot)) continue;
|
|
16500
14694
|
seenRecallRoots.add(recallRoot);
|
|
16501
14695
|
recallRoots.push(recallRoot);
|
|
@@ -16519,8 +14713,8 @@ ${lines.join("\n\n")}`;
|
|
|
16519
14713
|
if (resolvedCold) scopedResults.push(resolvedCold.result);
|
|
16520
14714
|
continue;
|
|
16521
14715
|
}
|
|
16522
|
-
if (
|
|
16523
|
-
const resolvedPath =
|
|
14716
|
+
if (path2.isAbsolute(result.path)) {
|
|
14717
|
+
const resolvedPath = path2.resolve(result.path);
|
|
16524
14718
|
if (recallRoots.some(
|
|
16525
14719
|
(recallRoot) => isPathInsideStorageRoot(recallRoot, resolvedPath)
|
|
16526
14720
|
)) {
|
|
@@ -16807,6 +15001,7 @@ ${lines.join("\n\n")}`;
|
|
|
16807
15001
|
filterSearchResultsByRecallSafety(results, memoryByPath, options) {
|
|
16808
15002
|
let lifecycleFilteredCount = 0;
|
|
16809
15003
|
let temporalSupersededFilteredCount = 0;
|
|
15004
|
+
let biTemporalExpiredFilteredCount = 0;
|
|
16810
15005
|
let dedicatedSurfaceFilteredCount = 0;
|
|
16811
15006
|
let forgottenFilteredCount = 0;
|
|
16812
15007
|
let blockedPathFilteredCount = 0;
|
|
@@ -16854,6 +15049,10 @@ ${lines.join("\n\n")}`;
|
|
|
16854
15049
|
temporalSupersededFilteredCount += 1;
|
|
16855
15050
|
continue;
|
|
16856
15051
|
}
|
|
15052
|
+
if (!asOfActive && this.config.temporalBiTemporal && !this.config.temporalExpiredInInjection && isValidityExpiredNow(memory.frontmatter, Date.now())) {
|
|
15053
|
+
biTemporalExpiredFilteredCount += 1;
|
|
15054
|
+
continue;
|
|
15055
|
+
}
|
|
16857
15056
|
if (options?.allowDedicatedSurface !== true && (memory.frontmatter.memoryKind === "dream" || memory.frontmatter.memoryKind === "procedural")) {
|
|
16858
15057
|
dedicatedSurfaceFilteredCount += 1;
|
|
16859
15058
|
continue;
|
|
@@ -16871,6 +15070,11 @@ ${lines.join("\n\n")}`;
|
|
|
16871
15070
|
`temporal supersession filter removed ${temporalSupersededFilteredCount} superseded candidates`
|
|
16872
15071
|
);
|
|
16873
15072
|
}
|
|
15073
|
+
if (biTemporalExpiredFilteredCount > 0) {
|
|
15074
|
+
log.debug(
|
|
15075
|
+
`bi-temporal validity filter removed ${biTemporalExpiredFilteredCount} expired-validity candidates from injection (temporal.biTemporal on)`
|
|
15076
|
+
);
|
|
15077
|
+
}
|
|
16874
15078
|
if (dedicatedSurfaceFilteredCount > 0) {
|
|
16875
15079
|
log.debug(
|
|
16876
15080
|
`dedicated surface filter removed ${dedicatedSurfaceFilteredCount} dream/procedural candidates from generic recall`
|
|
@@ -17262,8 +15466,8 @@ ${lines.join("\n\n")}`;
|
|
|
17262
15466
|
}
|
|
17263
15467
|
namespaceFromStorageDir(storageDir) {
|
|
17264
15468
|
if (!this.config.namespacesEnabled) return this.config.defaultNamespace;
|
|
17265
|
-
const resolvedStorageDir =
|
|
17266
|
-
const resolvedMemoryDir =
|
|
15469
|
+
const resolvedStorageDir = path2.resolve(storageDir);
|
|
15470
|
+
const resolvedMemoryDir = path2.resolve(this.config.memoryDir);
|
|
17267
15471
|
if (resolvedStorageDir === resolvedMemoryDir)
|
|
17268
15472
|
return this.config.defaultNamespace;
|
|
17269
15473
|
const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
|
|
@@ -17343,7 +15547,6 @@ export {
|
|
|
17343
15547
|
ensureBuiltInWearableConnectors,
|
|
17344
15548
|
WearablesService,
|
|
17345
15549
|
locateTranscriptPath,
|
|
17346
|
-
NamespaceCatalog,
|
|
17347
15550
|
BulkImportBatchPartialFailureError,
|
|
17348
15551
|
dedupeEntitySynthesisEvidenceEntries,
|
|
17349
15552
|
defaultWorkspaceDir,
|
|
@@ -17374,4 +15577,4 @@ export {
|
|
|
17374
15577
|
resolvePersistedMemoryRelativePath,
|
|
17375
15578
|
Orchestrator
|
|
17376
15579
|
};
|
|
17377
|
-
//# sourceMappingURL=chunk-
|
|
15580
|
+
//# sourceMappingURL=chunk-TJ5IIQIR.js.map
|