@remnic/core 9.3.653 → 9.3.655
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-cli.js +24 -24
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +17 -17
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +16 -16
- package/dist/access-schema.d.ts +12 -12
- package/dist/{access-service-CdJFd3_b.d.ts → access-service-BEJvriUt.d.ts} +11 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +15 -15
- 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/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-GI45G4BK.js → chunk-2RCGZ67B.js} +4 -4
- package/dist/{chunk-BEMWL2FZ.js → chunk-54LOUIBE.js} +2 -2
- package/dist/{chunk-E3J6O6N7.js → chunk-55ZMNKMQ.js} +20 -9
- package/dist/{chunk-E3J6O6N7.js.map → chunk-55ZMNKMQ.js.map} +1 -1
- package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
- package/dist/{chunk-SPMZZUEJ.js → chunk-5QD3QD76.js} +2684 -401
- package/dist/chunk-5QD3QD76.js.map +1 -0
- package/dist/{chunk-WLGE6KEO.js → chunk-67G4T7KI.js} +3 -3
- package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
- package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
- package/dist/chunk-6IMKOIZ6.js.map +1 -0
- package/dist/{chunk-KJDKZVF3.js → chunk-A3Y37UWI.js} +3 -3
- package/dist/{chunk-CFOCZPIQ.js → chunk-BGKXTVNG.js} +2 -2
- package/dist/{chunk-QQHIQ7JD.js → chunk-COVZLGMR.js} +87 -18
- package/dist/chunk-COVZLGMR.js.map +1 -0
- package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
- package/dist/chunk-EKQMQQ3U.js.map +1 -0
- package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
- package/dist/chunk-GKKAXVAJ.js.map +1 -0
- package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
- package/dist/{chunk-EHQLDFSH.js → chunk-IQ53ZSXV.js} +2 -2
- package/dist/{chunk-C63WC454.js → chunk-KOI765XP.js} +125 -1
- package/dist/chunk-KOI765XP.js.map +1 -0
- package/dist/{chunk-IVYSVAC6.js → chunk-KZZ4YAEC.js} +2 -2
- package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
- package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
- package/dist/{chunk-JF7SFXTG.js → chunk-NCSJKK23.js} +2 -2
- package/dist/{chunk-XMN6MMTU.js → chunk-NRBGRZW4.js} +2 -2
- package/dist/{chunk-NOBL7OUP.js → chunk-OKW6F5S5.js} +12 -5
- package/dist/{chunk-NOBL7OUP.js.map → chunk-OKW6F5S5.js.map} +1 -1
- package/dist/{chunk-BNFRL6QW.js → chunk-PTMJ2FH2.js} +2 -2
- package/dist/{chunk-KWM33SPU.js → chunk-PVE7KSQP.js} +2 -2
- package/dist/{chunk-EW52H5EM.js → chunk-QDVQ4AN2.js} +12 -5
- package/dist/chunk-QDVQ4AN2.js.map +1 -0
- package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
- package/dist/chunk-QRSKPI62.js.map +1 -0
- package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
- package/dist/{chunk-C43KEWEV.js → chunk-TDZSSJV4.js} +1 -1
- package/dist/chunk-TDZSSJV4.js.map +1 -0
- package/dist/{chunk-Y7NWBBHV.js → chunk-TEO46GMM.js} +2 -2
- package/dist/{chunk-AJE7FJVE.js → chunk-UCEABZZN.js} +2 -2
- package/dist/{chunk-IENGGY2C.js → chunk-UCEDY5M7.js} +2 -2
- package/dist/{chunk-PRQXUSQV.js → chunk-UYNFWZWG.js} +2 -2
- package/dist/{chunk-V4UDXYGG.js → chunk-WDTUYOLS.js} +2 -2
- package/dist/{chunk-RZOBQ23O.js → chunk-XOFXKASO.js} +2 -2
- package/dist/chunk-XRKQOQLY.js +212 -0
- package/dist/chunk-XRKQOQLY.js.map +1 -0
- package/dist/{chunk-WTI35CVJ.js → chunk-YYN3LIYA.js} +5 -5
- package/dist/{cli-DDo7Qgs-.d.ts → cli-BGahB_d3.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +29 -29
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- 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 +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +19 -1
- package/dist/contradiction/index.js +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/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 +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +37 -35
- 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/index.d.ts +1 -1
- 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 +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- 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 +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +15 -4
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +52 -3
- package/dist/namespaces/storage.js +9 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +14 -14
- package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-BgzZlWxH.d.ts} +500 -3
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +20 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +5 -1
- package/dist/qmd.js +2 -2
- 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-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/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.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 +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +6 -6
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +17 -1
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-Z8d_uMq8.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- 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 +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -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/types.d.ts +12 -12
- package/dist/{types-D8yUmSik.d.ts → types-2OPlQWJG.d.ts} +23 -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 +3 -3
- package/package.json +1 -1
- package/src/access-http.ts +7 -0
- package/src/access-mcp.ts +7 -0
- package/src/access-service.ts +12 -0
- package/src/cli.ts +104 -0
- package/src/config.test.ts +109 -0
- package/src/config.ts +164 -0
- package/src/contradiction/contradiction.test.ts +284 -0
- package/src/contradiction/resolution.ts +151 -4
- package/src/explicit-capture.ts +31 -10
- package/src/index.ts +10 -0
- package/src/maintenance/namespace-planner.test.ts +1120 -0
- package/src/maintenance/namespace-planner.ts +893 -0
- package/src/namespaces/catalog.test.ts +3356 -0
- package/src/namespaces/catalog.ts +2123 -0
- package/src/namespaces/search.test.ts +130 -2
- package/src/namespaces/search.ts +71 -10
- package/src/namespaces/storage.ts +210 -30
- package/src/orchestrator-flush.test.ts +720 -0
- package/src/orchestrator.ts +881 -239
- package/src/qmd-client.test.ts +59 -0
- package/src/qmd.ts +124 -84
- package/src/search/port.ts +16 -0
- package/src/types.ts +23 -0
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-EW52H5EM.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-JVRPJ7D4.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-PYWNNF2I.js.map +0 -1
- package/dist/chunk-QQHIQ7JD.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-SPMZZUEJ.js.map +0 -1
- /package/dist/{chunk-GI45G4BK.js.map → chunk-2RCGZ67B.js.map} +0 -0
- /package/dist/{chunk-BEMWL2FZ.js.map → chunk-54LOUIBE.js.map} +0 -0
- /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
- /package/dist/{chunk-WLGE6KEO.js.map → chunk-67G4T7KI.js.map} +0 -0
- /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
- /package/dist/{chunk-KJDKZVF3.js.map → chunk-A3Y37UWI.js.map} +0 -0
- /package/dist/{chunk-CFOCZPIQ.js.map → chunk-BGKXTVNG.js.map} +0 -0
- /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
- /package/dist/{chunk-EHQLDFSH.js.map → chunk-IQ53ZSXV.js.map} +0 -0
- /package/dist/{chunk-IVYSVAC6.js.map → chunk-KZZ4YAEC.js.map} +0 -0
- /package/dist/{chunk-JF7SFXTG.js.map → chunk-NCSJKK23.js.map} +0 -0
- /package/dist/{chunk-XMN6MMTU.js.map → chunk-NRBGRZW4.js.map} +0 -0
- /package/dist/{chunk-BNFRL6QW.js.map → chunk-PTMJ2FH2.js.map} +0 -0
- /package/dist/{chunk-KWM33SPU.js.map → chunk-PVE7KSQP.js.map} +0 -0
- /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
- /package/dist/{chunk-Y7NWBBHV.js.map → chunk-TEO46GMM.js.map} +0 -0
- /package/dist/{chunk-AJE7FJVE.js.map → chunk-UCEABZZN.js.map} +0 -0
- /package/dist/{chunk-IENGGY2C.js.map → chunk-UCEDY5M7.js.map} +0 -0
- /package/dist/{chunk-PRQXUSQV.js.map → chunk-UYNFWZWG.js.map} +0 -0
- /package/dist/{chunk-V4UDXYGG.js.map → chunk-WDTUYOLS.js.map} +0 -0
- /package/dist/{chunk-RZOBQ23O.js.map → chunk-XOFXKASO.js.map} +0 -0
- /package/dist/{chunk-WTI35CVJ.js.map → chunk-YYN3LIYA.js.map} +0 -0
- /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
CompoundingEngine,
|
|
31
31
|
defaultTierMigrationCycleBudget
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-UYNFWZWG.js";
|
|
33
33
|
import {
|
|
34
34
|
SharedContextManager
|
|
35
35
|
} from "./chunk-DRD2Q7HQ.js";
|
|
@@ -175,7 +175,7 @@ import {
|
|
|
175
175
|
buildEntityRecallSection,
|
|
176
176
|
entityRecentTranscriptLookbackHours,
|
|
177
177
|
readRecentEntityTranscriptEntries
|
|
178
|
-
} from "./chunk-
|
|
178
|
+
} from "./chunk-NRBGRZW4.js";
|
|
179
179
|
import {
|
|
180
180
|
buildEventOrderRecallSection,
|
|
181
181
|
shouldRecallEventOrderEvidence
|
|
@@ -210,7 +210,7 @@ import {
|
|
|
210
210
|
materializeAfterSemanticConsolidation,
|
|
211
211
|
parseConsolidationResponse,
|
|
212
212
|
parseOperatorAwareConsolidationResponse
|
|
213
|
-
} from "./chunk-
|
|
213
|
+
} from "./chunk-BGKXTVNG.js";
|
|
214
214
|
import {
|
|
215
215
|
normalizeReplaySessionKey
|
|
216
216
|
} from "./chunk-2PRQG7PV.js";
|
|
@@ -219,13 +219,13 @@ import {
|
|
|
219
219
|
} from "./chunk-X6IRLNOO.js";
|
|
220
220
|
import {
|
|
221
221
|
searchVerifiedEpisodes
|
|
222
|
-
} from "./chunk-
|
|
222
|
+
} from "./chunk-NCSJKK23.js";
|
|
223
223
|
import {
|
|
224
224
|
ThreadingManager
|
|
225
225
|
} from "./chunk-W4RVMTHR.js";
|
|
226
226
|
import {
|
|
227
227
|
searchVerifiedSemanticRules
|
|
228
|
-
} from "./chunk-
|
|
228
|
+
} from "./chunk-UCEABZZN.js";
|
|
229
229
|
import {
|
|
230
230
|
searchWorkProductLedgerEntries
|
|
231
231
|
} from "./chunk-ZRWB5D4H.js";
|
|
@@ -236,8 +236,10 @@ import {
|
|
|
236
236
|
PolicyRuntimeManager
|
|
237
237
|
} from "./chunk-33JBK2XP.js";
|
|
238
238
|
import {
|
|
239
|
-
NamespaceStorageRouter
|
|
240
|
-
|
|
239
|
+
NamespaceStorageRouter,
|
|
240
|
+
resolveDefaultNamespaceRoot,
|
|
241
|
+
resolveNamespaceStorageRoot
|
|
242
|
+
} from "./chunk-XRKQOQLY.js";
|
|
241
243
|
import {
|
|
242
244
|
isAboveImportanceThreshold,
|
|
243
245
|
scoreImportance
|
|
@@ -315,26 +317,28 @@ import {
|
|
|
315
317
|
} from "./chunk-FF4KLI5W.js";
|
|
316
318
|
import {
|
|
317
319
|
buildXraySnapshot
|
|
318
|
-
} from "./chunk-
|
|
320
|
+
} from "./chunk-54LOUIBE.js";
|
|
319
321
|
import {
|
|
320
322
|
NamespaceSearchRouter
|
|
321
|
-
} from "./chunk-
|
|
323
|
+
} from "./chunk-EKQMQQ3U.js";
|
|
322
324
|
import {
|
|
323
325
|
createConversationIndexRuntime,
|
|
324
326
|
createSearchBackend
|
|
325
|
-
} from "./chunk-
|
|
327
|
+
} from "./chunk-SSSXWIBP.js";
|
|
326
328
|
import {
|
|
327
329
|
NoopSearchBackend
|
|
328
330
|
} from "./chunk-CYEPCZN5.js";
|
|
329
331
|
import {
|
|
330
|
-
namespaceIdentityFromToken
|
|
332
|
+
namespaceIdentityFromToken,
|
|
333
|
+
namespaceIdentityToken,
|
|
334
|
+
normalizeNamespaceIdentity
|
|
331
335
|
} from "./chunk-ZFXCQPNO.js";
|
|
332
336
|
import {
|
|
333
337
|
writeConversationChunks
|
|
334
338
|
} from "./chunk-OIF36KGD.js";
|
|
335
339
|
import {
|
|
336
340
|
parseQmdExplain
|
|
337
|
-
} from "./chunk-
|
|
341
|
+
} from "./chunk-QRSKPI62.js";
|
|
338
342
|
import {
|
|
339
343
|
objectiveStateStoreOverrideForNamespace,
|
|
340
344
|
searchObjectiveStateSnapshots
|
|
@@ -347,14 +351,15 @@ import {
|
|
|
347
351
|
} from "./chunk-QDW3E4RD.js";
|
|
348
352
|
import {
|
|
349
353
|
shouldSkipImplicitExtraction
|
|
350
|
-
} from "./chunk-
|
|
354
|
+
} from "./chunk-GKKAXVAJ.js";
|
|
351
355
|
import {
|
|
352
356
|
GraphIndex
|
|
353
357
|
} from "./chunk-Y56J7CXW.js";
|
|
354
358
|
import {
|
|
355
359
|
buildChainFollowupGenerator
|
|
356
|
-
} from "./chunk-
|
|
360
|
+
} from "./chunk-PTMJ2FH2.js";
|
|
357
361
|
import {
|
|
362
|
+
ALL_CATEGORY_DIRS,
|
|
358
363
|
ContentHashIndex,
|
|
359
364
|
StorageManager,
|
|
360
365
|
compareEntityTimestamps,
|
|
@@ -363,7 +368,7 @@ import {
|
|
|
363
368
|
normalizeEntityName,
|
|
364
369
|
parseEntityFile,
|
|
365
370
|
stripAttributesSuffix
|
|
366
|
-
} from "./chunk-
|
|
371
|
+
} from "./chunk-TEO46GMM.js";
|
|
367
372
|
import {
|
|
368
373
|
isValidTranscriptDate,
|
|
369
374
|
loadSpeakerRegistry,
|
|
@@ -378,11 +383,14 @@ import {
|
|
|
378
383
|
} from "./chunk-J6A3CX5N.js";
|
|
379
384
|
import {
|
|
380
385
|
confidenceTier
|
|
381
|
-
} from "./chunk-
|
|
386
|
+
} from "./chunk-TDZSSJV4.js";
|
|
382
387
|
import {
|
|
383
388
|
inferMemoryStatus,
|
|
384
389
|
isActiveMemoryStatus
|
|
385
390
|
} from "./chunk-RULE4VG5.js";
|
|
391
|
+
import {
|
|
392
|
+
displayErrorDetail
|
|
393
|
+
} from "./chunk-6KYMPV2O.js";
|
|
386
394
|
import {
|
|
387
395
|
lintWorkspaceFiles,
|
|
388
396
|
rotateMarkdownFileToArchive
|
|
@@ -400,6 +408,7 @@ import {
|
|
|
400
408
|
resolvePrincipal
|
|
401
409
|
} from "./chunk-UZYLX7M6.js";
|
|
402
410
|
import {
|
|
411
|
+
isSafeRouteNamespace,
|
|
403
412
|
selectRouteRule
|
|
404
413
|
} from "./chunk-U3PN77QT.js";
|
|
405
414
|
import {
|
|
@@ -432,17 +441,17 @@ import {
|
|
|
432
441
|
} from "./chunk-AC5LO7IU.js";
|
|
433
442
|
|
|
434
443
|
// src/orchestrator.ts
|
|
435
|
-
import
|
|
444
|
+
import path4 from "path";
|
|
436
445
|
import os from "os";
|
|
437
|
-
import { createHash, randomBytes } from "crypto";
|
|
438
|
-
import { existsSync } from "fs";
|
|
446
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
447
|
+
import { existsSync, readFileSync } from "fs";
|
|
439
448
|
import {
|
|
440
|
-
mkdir as
|
|
441
|
-
readdir,
|
|
442
|
-
readFile as
|
|
443
|
-
stat,
|
|
444
|
-
unlink,
|
|
445
|
-
writeFile as
|
|
449
|
+
mkdir as mkdir4,
|
|
450
|
+
readdir as readdir3,
|
|
451
|
+
readFile as readFile4,
|
|
452
|
+
stat as stat3,
|
|
453
|
+
unlink as unlink2,
|
|
454
|
+
writeFile as writeFile4
|
|
446
455
|
} from "fs/promises";
|
|
447
456
|
|
|
448
457
|
// src/procedural/procedure-recall.ts
|
|
@@ -1464,81 +1473,2004 @@ Install it alongside Remnic:
|
|
|
1464
1473
|
await saveSpeakerRegistry(storage.dir, registry);
|
|
1465
1474
|
return registry;
|
|
1466
1475
|
}
|
|
1467
|
-
async removeSpeaker(sourceId, speakerKey) {
|
|
1468
|
-
const storage = await this.deps.getStorage();
|
|
1469
|
-
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1470
|
-
const key = speakerRegistryKey(sourceId, speakerKey.trim());
|
|
1471
|
-
if (!(key in registry.speakers)) {
|
|
1472
|
-
throw new WearablesInputError(`no speaker override stored for '${key}'`);
|
|
1476
|
+
async removeSpeaker(sourceId, speakerKey) {
|
|
1477
|
+
const storage = await this.deps.getStorage();
|
|
1478
|
+
const registry = await loadSpeakerRegistry(storage.dir);
|
|
1479
|
+
const key = speakerRegistryKey(sourceId, speakerKey.trim());
|
|
1480
|
+
if (!(key in registry.speakers)) {
|
|
1481
|
+
throw new WearablesInputError(`no speaker override stored for '${key}'`);
|
|
1482
|
+
}
|
|
1483
|
+
delete registry.speakers[key];
|
|
1484
|
+
await saveSpeakerRegistry(storage.dir, registry);
|
|
1485
|
+
return registry;
|
|
1486
|
+
}
|
|
1487
|
+
// -- corrections ----------------------------------------------------------
|
|
1488
|
+
async listCorrections() {
|
|
1489
|
+
const storage = await this.deps.getStorage();
|
|
1490
|
+
return {
|
|
1491
|
+
fromConfig: this.deps.config.corrections,
|
|
1492
|
+
fromState: await loadCorrectionsFile(storage.dir),
|
|
1493
|
+
stateFilePath: correctionsFilePath(storage.dir)
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
async addCorrection(rule) {
|
|
1497
|
+
compileCorrectionRule(rule, "correction");
|
|
1498
|
+
const storage = await this.deps.getStorage();
|
|
1499
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1500
|
+
const duplicate = rules.some(
|
|
1501
|
+
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
1502
|
+
);
|
|
1503
|
+
if (duplicate) {
|
|
1504
|
+
throw new WearablesInputError(
|
|
1505
|
+
`an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
rules.push(rule);
|
|
1509
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1510
|
+
}
|
|
1511
|
+
async removeCorrection(index) {
|
|
1512
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
1513
|
+
throw new WearablesInputError(`invalid correction index '${index}'`);
|
|
1514
|
+
}
|
|
1515
|
+
const storage = await this.deps.getStorage();
|
|
1516
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1517
|
+
if (index >= rules.length) {
|
|
1518
|
+
throw new WearablesInputError(
|
|
1519
|
+
`correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const [removed] = rules.splice(index, 1);
|
|
1523
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1524
|
+
return removed;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
function clampLimit(value, fallback, max, label) {
|
|
1528
|
+
if (value === void 0) return fallback;
|
|
1529
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
|
|
1530
|
+
throw new WearablesInputError(
|
|
1531
|
+
`invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
return value;
|
|
1535
|
+
}
|
|
1536
|
+
function locateTranscriptPath(hitPath) {
|
|
1537
|
+
const normalized = hitPath.replace(/\\/g, "/");
|
|
1538
|
+
const match = normalized.match(
|
|
1539
|
+
/(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
|
|
1540
|
+
);
|
|
1541
|
+
if (!match) return null;
|
|
1542
|
+
if (!isValidTranscriptDate(match[2])) return null;
|
|
1543
|
+
return { source: match[1], date: match[2] };
|
|
1544
|
+
}
|
|
1545
|
+
function extractSnippet(body, index, matchLength) {
|
|
1546
|
+
const start = Math.max(0, index - 80);
|
|
1547
|
+
const end = Math.min(body.length, index + matchLength + 80);
|
|
1548
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
1549
|
+
const suffix = end < body.length ? "\u2026" : "";
|
|
1550
|
+
return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/namespaces/catalog.ts
|
|
1554
|
+
import path2 from "path";
|
|
1555
|
+
import { randomUUID } from "crypto";
|
|
1556
|
+
import {
|
|
1557
|
+
appendFile,
|
|
1558
|
+
lstat,
|
|
1559
|
+
mkdir as mkdir2,
|
|
1560
|
+
open,
|
|
1561
|
+
readdir,
|
|
1562
|
+
readFile as readFile2,
|
|
1563
|
+
realpath,
|
|
1564
|
+
rename,
|
|
1565
|
+
stat,
|
|
1566
|
+
unlink,
|
|
1567
|
+
utimes,
|
|
1568
|
+
writeFile as writeFile2
|
|
1569
|
+
} from "fs/promises";
|
|
1570
|
+
var NAMESPACE_KINDS = [
|
|
1571
|
+
"default",
|
|
1572
|
+
"self",
|
|
1573
|
+
"shared",
|
|
1574
|
+
"project",
|
|
1575
|
+
"branch",
|
|
1576
|
+
"team-project",
|
|
1577
|
+
"explicit",
|
|
1578
|
+
"legacy"
|
|
1579
|
+
];
|
|
1580
|
+
var NAMESPACE_DISCOVERY_SOURCES = [
|
|
1581
|
+
"config",
|
|
1582
|
+
"write",
|
|
1583
|
+
"read",
|
|
1584
|
+
"scan",
|
|
1585
|
+
"migration"
|
|
1586
|
+
];
|
|
1587
|
+
var CATALOG_FILE = "namespaces.jsonl";
|
|
1588
|
+
var STATE_DIR = "state";
|
|
1589
|
+
var REBUILD_LOCK_FILE = "namespaces.rebuild.lock";
|
|
1590
|
+
var REBUILD_LOCK_STALE_MS = 3e4;
|
|
1591
|
+
var REBUILD_LOCK_MAX_WAIT_MS = 5e3;
|
|
1592
|
+
var REBUILD_LOCK_POLL_MS = 50;
|
|
1593
|
+
var REBUILD_LOCK_HEARTBEAT_MS = 1e4;
|
|
1594
|
+
var MEMORY_DATA_CHILDREN = [
|
|
1595
|
+
...ALL_CATEGORY_DIRS,
|
|
1596
|
+
"entities",
|
|
1597
|
+
"artifacts",
|
|
1598
|
+
"identity",
|
|
1599
|
+
"config",
|
|
1600
|
+
"summaries",
|
|
1601
|
+
"profile.md",
|
|
1602
|
+
"state"
|
|
1603
|
+
];
|
|
1604
|
+
function isCatalogEnabled(config) {
|
|
1605
|
+
if (config.namespacesEnabled !== true) return false;
|
|
1606
|
+
return config.namespaceCatalogEnabled !== false;
|
|
1607
|
+
}
|
|
1608
|
+
var FILE_MEMORY_DATA_CHILDREN = /* @__PURE__ */ new Set(["profile.md"]);
|
|
1609
|
+
function isNotFoundError(err) {
|
|
1610
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1611
|
+
}
|
|
1612
|
+
async function inspectMemoryDataMarker(rootDir, child) {
|
|
1613
|
+
const childPath = path2.join(rootDir, child);
|
|
1614
|
+
let entry;
|
|
1615
|
+
try {
|
|
1616
|
+
entry = await lstat(childPath);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
return isNotFoundError(err) ? { state: "absent" } : { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1619
|
+
}
|
|
1620
|
+
if (entry.isSymbolicLink()) return { state: "invalid", detail: `${child}: symlink` };
|
|
1621
|
+
if (FILE_MEMORY_DATA_CHILDREN.has(child)) {
|
|
1622
|
+
return entry.isFile() ? { state: "valid" } : { state: "invalid", detail: `${child}: expected file` };
|
|
1623
|
+
}
|
|
1624
|
+
if (!entry.isDirectory()) return { state: "invalid", detail: `${child}: expected directory` };
|
|
1625
|
+
try {
|
|
1626
|
+
const rootReal = await realpath(rootDir);
|
|
1627
|
+
const childReal = await realpath(childPath);
|
|
1628
|
+
return isPathInside(rootReal, childReal) ? { state: "valid" } : { state: "invalid", detail: `${child}: escapes namespace root` };
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
return { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async function inspectMemoryDataRoot(rootDir) {
|
|
1634
|
+
let hasData = false;
|
|
1635
|
+
for (const child of MEMORY_DATA_CHILDREN) {
|
|
1636
|
+
const marker = await inspectMemoryDataMarker(rootDir, child);
|
|
1637
|
+
if (marker.state === "invalid") {
|
|
1638
|
+
return { hasData: false, invalidMarker: marker.detail };
|
|
1639
|
+
}
|
|
1640
|
+
if (marker.state === "valid") {
|
|
1641
|
+
hasData = true;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return { hasData };
|
|
1645
|
+
}
|
|
1646
|
+
async function hasMemoryData(rootDir) {
|
|
1647
|
+
return (await inspectMemoryDataRoot(rootDir)).hasData;
|
|
1648
|
+
}
|
|
1649
|
+
function isValidIsoTimestamp(value) {
|
|
1650
|
+
const ms = Date.parse(value);
|
|
1651
|
+
return Number.isFinite(ms);
|
|
1652
|
+
}
|
|
1653
|
+
function isNamespaceKind(value) {
|
|
1654
|
+
return typeof value === "string" && NAMESPACE_KINDS.includes(value);
|
|
1655
|
+
}
|
|
1656
|
+
function isNamespaceDiscoverySource(value) {
|
|
1657
|
+
return typeof value === "string" && NAMESPACE_DISCOVERY_SOURCES.includes(value);
|
|
1658
|
+
}
|
|
1659
|
+
function coerceRecord(value) {
|
|
1660
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1661
|
+
const v = value;
|
|
1662
|
+
if (typeof v.namespace !== "string") return null;
|
|
1663
|
+
const namespace = normalizeNamespaceIdentity(v.namespace);
|
|
1664
|
+
if (namespace.length === 0) return null;
|
|
1665
|
+
if (typeof v.identityToken !== "string" || v.identityToken.length === 0) return null;
|
|
1666
|
+
const expectedIdentityToken = namespaceIdentityToken(namespace);
|
|
1667
|
+
if (v.identityToken !== expectedIdentityToken) return null;
|
|
1668
|
+
if (typeof v.storageDir !== "string" || v.storageDir.length === 0) return null;
|
|
1669
|
+
if (typeof v.createdAt !== "string" || v.createdAt.length === 0) return null;
|
|
1670
|
+
if (!isValidIsoTimestamp(v.createdAt)) return null;
|
|
1671
|
+
const kind = v.kind === void 0 ? "explicit" : isNamespaceKind(v.kind) ? v.kind : null;
|
|
1672
|
+
if (!kind) return null;
|
|
1673
|
+
const discoveredBy = v.discoveredBy === void 0 ? "scan" : isNamespaceDiscoverySource(v.discoveredBy) ? v.discoveredBy : null;
|
|
1674
|
+
if (!discoveredBy) return null;
|
|
1675
|
+
const record = {
|
|
1676
|
+
namespace,
|
|
1677
|
+
identityToken: expectedIdentityToken,
|
|
1678
|
+
kind,
|
|
1679
|
+
createdAt: v.createdAt,
|
|
1680
|
+
storageDir: v.storageDir,
|
|
1681
|
+
discoveredBy
|
|
1682
|
+
};
|
|
1683
|
+
if (typeof v.principal === "string") record.principal = v.principal;
|
|
1684
|
+
if (typeof v.projectId === "string") record.projectId = v.projectId;
|
|
1685
|
+
if (typeof v.branch === "string") record.branch = v.branch;
|
|
1686
|
+
if (typeof v.parentNamespace === "string") record.parentNamespace = v.parentNamespace;
|
|
1687
|
+
if (typeof v.lastReadAt === "string" && isValidIsoTimestamp(v.lastReadAt)) {
|
|
1688
|
+
record.lastReadAt = v.lastReadAt;
|
|
1689
|
+
}
|
|
1690
|
+
if (typeof v.lastWriteAt === "string" && isValidIsoTimestamp(v.lastWriteAt)) {
|
|
1691
|
+
record.lastWriteAt = v.lastWriteAt;
|
|
1692
|
+
}
|
|
1693
|
+
if (v.lastMaintenanceAt && typeof v.lastMaintenanceAt === "object") {
|
|
1694
|
+
const out = {};
|
|
1695
|
+
for (const [k, val] of Object.entries(v.lastMaintenanceAt)) {
|
|
1696
|
+
if (typeof val === "string" && isValidIsoTimestamp(val)) out[k] = val;
|
|
1697
|
+
}
|
|
1698
|
+
if (Object.keys(out).length > 0) record.lastMaintenanceAt = out;
|
|
1699
|
+
}
|
|
1700
|
+
return record;
|
|
1701
|
+
}
|
|
1702
|
+
function laterIso(a, b) {
|
|
1703
|
+
if (!a) return b;
|
|
1704
|
+
if (!b) return a;
|
|
1705
|
+
const am = Date.parse(a);
|
|
1706
|
+
const bm = Date.parse(b);
|
|
1707
|
+
if (!Number.isFinite(am)) return b;
|
|
1708
|
+
if (!Number.isFinite(bm)) return a;
|
|
1709
|
+
return bm > am ? b : a;
|
|
1710
|
+
}
|
|
1711
|
+
function mergeNewerTouchFields(base, fresh) {
|
|
1712
|
+
const merged = { ...base };
|
|
1713
|
+
const lr = laterIso(base.lastReadAt, fresh.lastReadAt);
|
|
1714
|
+
if (lr) merged.lastReadAt = lr;
|
|
1715
|
+
const lw = laterIso(base.lastWriteAt, fresh.lastWriteAt);
|
|
1716
|
+
if (lw) merged.lastWriteAt = lw;
|
|
1717
|
+
if (base.lastMaintenanceAt || fresh.lastMaintenanceAt) {
|
|
1718
|
+
const jobs = { ...base.lastMaintenanceAt ?? {} };
|
|
1719
|
+
for (const [job, ts] of Object.entries(fresh.lastMaintenanceAt ?? {})) {
|
|
1720
|
+
const latest = laterIso(jobs[job], ts);
|
|
1721
|
+
if (latest) jobs[job] = latest;
|
|
1722
|
+
}
|
|
1723
|
+
if (Object.keys(jobs).length > 0) merged.lastMaintenanceAt = jobs;
|
|
1724
|
+
}
|
|
1725
|
+
return merged;
|
|
1726
|
+
}
|
|
1727
|
+
function serializeRecord(record) {
|
|
1728
|
+
const ordered = {};
|
|
1729
|
+
const source = record;
|
|
1730
|
+
for (const key of Object.keys(source).sort()) {
|
|
1731
|
+
const value = source[key];
|
|
1732
|
+
if (value === void 0) continue;
|
|
1733
|
+
if (key === "lastMaintenanceAt" && value && typeof value === "object") {
|
|
1734
|
+
const sortedJobs = {};
|
|
1735
|
+
for (const jobKey of Object.keys(value).sort()) {
|
|
1736
|
+
sortedJobs[jobKey] = value[jobKey];
|
|
1737
|
+
}
|
|
1738
|
+
ordered[key] = sortedJobs;
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
ordered[key] = value;
|
|
1742
|
+
}
|
|
1743
|
+
return JSON.stringify(ordered);
|
|
1744
|
+
}
|
|
1745
|
+
function inferKind(namespace, config) {
|
|
1746
|
+
if (namespace === normalizeNamespaceIdentity(config.defaultNamespace)) return "default";
|
|
1747
|
+
if (namespace === normalizeNamespaceIdentity(config.sharedNamespace)) return "shared";
|
|
1748
|
+
if (config.namespacePolicies.some((p) => normalizeNamespaceIdentity(p.name) === namespace)) {
|
|
1749
|
+
return "explicit";
|
|
1750
|
+
}
|
|
1751
|
+
if (/-branch-|^project-[^-]+-branch-/.test(namespace) || namespace.includes("-branch-")) {
|
|
1752
|
+
return "branch";
|
|
1753
|
+
}
|
|
1754
|
+
if (/^team-.*-project-/.test(namespace) || /^team-.*project-/.test(namespace)) {
|
|
1755
|
+
return "team-project";
|
|
1756
|
+
}
|
|
1757
|
+
if (/^project-/.test(namespace) || /-project-/.test(namespace)) {
|
|
1758
|
+
return "project";
|
|
1759
|
+
}
|
|
1760
|
+
return "explicit";
|
|
1761
|
+
}
|
|
1762
|
+
var NamespaceCatalog = class {
|
|
1763
|
+
constructor(config) {
|
|
1764
|
+
this.config = config;
|
|
1765
|
+
this.memoryDir = config.memoryDir;
|
|
1766
|
+
this.stateDir = path2.join(this.memoryDir, STATE_DIR);
|
|
1767
|
+
this.catalogPath = path2.join(this.stateDir, CATALOG_FILE);
|
|
1768
|
+
this.rebuildLockPath = path2.join(this.stateDir, REBUILD_LOCK_FILE);
|
|
1769
|
+
this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
1770
|
+
}
|
|
1771
|
+
config;
|
|
1772
|
+
memoryDir;
|
|
1773
|
+
stateDir;
|
|
1774
|
+
catalogPath;
|
|
1775
|
+
rebuildLockPath;
|
|
1776
|
+
// Per-INSTANCE lock owner id (round 6, codex P2 — NBsGP). The rebuild lock
|
|
1777
|
+
// file records this id, not just `process.pid`, so two NamespaceCatalog
|
|
1778
|
+
// instances in the SAME process sharing a memoryDir are NOT mistaken for each
|
|
1779
|
+
// other: a touch on instance B must still wait for instance A's rebuild lock
|
|
1780
|
+
// (different owner id, same PID) instead of skipping as "self-held".
|
|
1781
|
+
lockOwnerId = randomUUID();
|
|
1782
|
+
// Serialized write chain that recovers from rejection (CLAUDE.md rule #40)
|
|
1783
|
+
// so a single failed append cannot permanently poison subsequent writes.
|
|
1784
|
+
writeChain = Promise.resolve();
|
|
1785
|
+
// Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
|
|
1786
|
+
// section, after the lock is acquired but BEFORE the read→merge→append. A
|
|
1787
|
+
// deterministic concurrency test installs a hook here to widen the (otherwise
|
|
1788
|
+
// microscopic) window and prove that a cross-process rebuild CANNOT run its
|
|
1789
|
+
// load→rename while a touch holds the lock. Never set in production code.
|
|
1790
|
+
onTouchCriticalSectionForTest;
|
|
1791
|
+
// Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock
|
|
1792
|
+
// critical section, after the final cross-process re-merge `loadCompacted()` and
|
|
1793
|
+
// BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-
|
|
1794
|
+
// append touch (the old bug) would clobber its append. A deterministic test
|
|
1795
|
+
// installs a hook here to attempt a cross-instance touch in this window and
|
|
1796
|
+
// assert the held mutex blocks it. Never set in production code.
|
|
1797
|
+
onRebuildBeforeRenameForTest;
|
|
1798
|
+
// Test-only seam (NFgCT, codex P2): fires AFTER the lockless disk scan but
|
|
1799
|
+
// BEFORE the rebuild acquires the cross-process file lock for its final
|
|
1800
|
+
// load→merge→rename window. A deterministic test installs a hook here to attempt
|
|
1801
|
+
// a cross-instance touch DURING the scan window and assert it is NOT blocked or
|
|
1802
|
+
// dropped — proving the scan no longer holds the mutex. Never set in production.
|
|
1803
|
+
onRebuildAfterScanForTest;
|
|
1804
|
+
// Test-only seam (NG7Bg, codex P2): fires inside `breakStaleRebuildLock` AFTER it
|
|
1805
|
+
// has judged the lock stale and captured its identity, but BEFORE the final
|
|
1806
|
+
// re-validation+unlink. A deterministic test installs a hook here to REPLACE the
|
|
1807
|
+
// lock file (a fresh holder created a new lock in the race window) and assert the
|
|
1808
|
+
// break is skipped — the replacement's active lock is not deleted. Never set in
|
|
1809
|
+
// production.
|
|
1810
|
+
onBeforeBreakStaleUnlinkForTest;
|
|
1811
|
+
// Normalized (trimmed) default namespace identity (NH-FH, cursor Medium).
|
|
1812
|
+
// Catalog records key namespaces by their NORMALIZED identity
|
|
1813
|
+
// (`normalizeNamespaceIdentity`), but several default-namespace exemptions and
|
|
1814
|
+
// memoryDir-ownership checks compared against the RAW `config.defaultNamespace`.
|
|
1815
|
+
// If the configured default name carries surrounding whitespace the record key
|
|
1816
|
+
// is trimmed while the comparison string is not, so the default row is
|
|
1817
|
+
// misclassified, dropped at read time, or given the wrong storage root. Compare
|
|
1818
|
+
// against this normalized form everywhere instead.
|
|
1819
|
+
defaultNamespaceIdentity;
|
|
1820
|
+
/** Whether the catalog is active (namespaces enabled and catalog not opted out). */
|
|
1821
|
+
get enabled() {
|
|
1822
|
+
return isCatalogEnabled(this.config);
|
|
1823
|
+
}
|
|
1824
|
+
// ── Public enumeration API ──────────────────────────────────────────────
|
|
1825
|
+
/**
|
|
1826
|
+
* Sanitize a record at the enumeration boundary (round 5, cursor Medium + codex
|
|
1827
|
+
* P2; round 6 — NDXHe). Reads return whatever is in `namespaces.jsonl` after
|
|
1828
|
+
* schema checks only, so a tampered or pre-fix row could surface unsafe data to
|
|
1829
|
+
* maintenance/QMD until a rewrite occurs. Two distinct defenses:
|
|
1830
|
+
*
|
|
1831
|
+
* 1. UNSAFE NAMESPACE NAME (NGZqr, codex P2): an unsafe non-default namespace
|
|
1832
|
+
* (e.g. `../evil`, a name with separators, or >64 chars) is REJECTED outright
|
|
1833
|
+
* — return `null` so the caller drops it. The disk SCAN and the hot touch
|
|
1834
|
+
* path both reject such names with the SAME default-exempt `isSafeRouteNamespace`
|
|
1835
|
+
* gate, so the read boundary MUST agree, or `listNamespaces()`/`getNamespaceRecord()`
|
|
1836
|
+
* would expose a namespace those paths reject (note `isStorageDirForNamespace`
|
|
1837
|
+
* can still build a tokenized root even for `../evil`, so storageDir sanitation
|
|
1838
|
+
* alone does not catch it). The default namespace is exempt (it may be a
|
|
1839
|
+
* non-route literal), matching every other validation site.
|
|
1840
|
+
*
|
|
1841
|
+
* 2. UNSAFE storageDir: for an otherwise-valid namespace, apply the SAME contract
|
|
1842
|
+
* as the write path — full containment (`isContainedStorageDir`: lexical +
|
|
1843
|
+
* symlink/realpath) AND namespace ownership (`isStorageDirForNamespace`). When
|
|
1844
|
+
* a record fails EITHER check we substitute the trusted resolved-and-safe root
|
|
1845
|
+
* for that namespace (rule 42: read and write stay symmetric).
|
|
1846
|
+
*/
|
|
1847
|
+
async sanitizeRecordForRead(record) {
|
|
1848
|
+
if (record.namespace !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(record.namespace)) {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
if (await this.isContainedStorageDir(record.storageDir) && await this.isStorageDirForNamespace(record.namespace, record.storageDir)) {
|
|
1852
|
+
return record;
|
|
1853
|
+
}
|
|
1854
|
+
const safe = await this.resolveSafeStorageDir(record.namespace);
|
|
1855
|
+
return { ...record, storageDir: safe };
|
|
1856
|
+
}
|
|
1857
|
+
storageRootOwnershipRank(record, resolvedStorageDir, configured) {
|
|
1858
|
+
if (resolvedStorageDir === path2.resolve(this.memoryDir)) {
|
|
1859
|
+
return record.namespace === this.defaultNamespaceIdentity ? 0 : 3;
|
|
1860
|
+
}
|
|
1861
|
+
const leaf = path2.basename(resolvedStorageDir);
|
|
1862
|
+
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
1863
|
+
if (tokenOwnsRoot && configured.has(record.namespace)) {
|
|
1864
|
+
return 0;
|
|
1865
|
+
}
|
|
1866
|
+
if (record.namespace === leaf) return 1;
|
|
1867
|
+
if (tokenOwnsRoot) return 2;
|
|
1868
|
+
return 3;
|
|
1869
|
+
}
|
|
1870
|
+
configuredNamespaceIdentities() {
|
|
1871
|
+
return new Set(
|
|
1872
|
+
[
|
|
1873
|
+
this.config.defaultNamespace,
|
|
1874
|
+
this.config.sharedNamespace,
|
|
1875
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
1876
|
+
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
preferStorageRootOwner(current, candidate, resolvedStorageDir, configured) {
|
|
1880
|
+
const currentRank = this.storageRootOwnershipRank(current, resolvedStorageDir, configured);
|
|
1881
|
+
const candidateRank = this.storageRootOwnershipRank(candidate, resolvedStorageDir, configured);
|
|
1882
|
+
if (candidateRank < currentRank) return candidate;
|
|
1883
|
+
if (candidateRank > currentRank) return current;
|
|
1884
|
+
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
1885
|
+
if (byName < 0) return candidate;
|
|
1886
|
+
if (byName > 0) return current;
|
|
1887
|
+
return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
|
|
1888
|
+
}
|
|
1889
|
+
dropDuplicateStorageRootAliases(records) {
|
|
1890
|
+
const byStorageDir = /* @__PURE__ */ new Map();
|
|
1891
|
+
const configured = this.configuredNamespaceIdentities();
|
|
1892
|
+
for (const record of records) {
|
|
1893
|
+
const resolvedStorageDir = path2.resolve(record.storageDir);
|
|
1894
|
+
const current = byStorageDir.get(resolvedStorageDir);
|
|
1895
|
+
if (!current) {
|
|
1896
|
+
byStorageDir.set(resolvedStorageDir, record);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
const owner = this.preferStorageRootOwner(current, record, resolvedStorageDir, configured);
|
|
1900
|
+
const alias = owner === current ? record : current;
|
|
1901
|
+
byStorageDir.set(resolvedStorageDir, mergeNewerTouchFields(owner, alias));
|
|
1902
|
+
}
|
|
1903
|
+
return [...byStorageDir.values()];
|
|
1904
|
+
}
|
|
1905
|
+
async loadSanitizedRecords() {
|
|
1906
|
+
const records = await this.loadCompacted();
|
|
1907
|
+
const sanitized = await Promise.all(
|
|
1908
|
+
[...records.values()].map((r) => this.sanitizeRecordForRead(r))
|
|
1909
|
+
);
|
|
1910
|
+
return this.dropDuplicateStorageRootAliases(
|
|
1911
|
+
sanitized.filter((r) => r !== null)
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
async listNamespaces(filter) {
|
|
1915
|
+
if (!this.enabled) return [];
|
|
1916
|
+
let out = await this.loadSanitizedRecords();
|
|
1917
|
+
if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);
|
|
1918
|
+
if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);
|
|
1919
|
+
if (filter?.writtenSince) {
|
|
1920
|
+
const sinceMs = filter.writtenSince.getTime();
|
|
1921
|
+
out = out.filter((r) => {
|
|
1922
|
+
if (!r.lastWriteAt) return false;
|
|
1923
|
+
const ms = Date.parse(r.lastWriteAt);
|
|
1924
|
+
return Number.isFinite(ms) && ms >= sinceMs;
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
return out.sort((a, b) => {
|
|
1928
|
+
const byName = a.namespace.localeCompare(b.namespace);
|
|
1929
|
+
if (byName !== 0) return byName;
|
|
1930
|
+
return a.identityToken.localeCompare(b.identityToken);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
async getNamespaceRecord(namespace) {
|
|
1934
|
+
if (!this.enabled) return null;
|
|
1935
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
1936
|
+
return (await this.loadSanitizedRecords()).find((record) => record.namespace === ns) ?? null;
|
|
1937
|
+
}
|
|
1938
|
+
// ── Touch API (cheap, failure-tolerant) ─────────────────────────────────
|
|
1939
|
+
async markRead(namespace, metadata) {
|
|
1940
|
+
await this.touch(namespace, "read", metadata);
|
|
1941
|
+
}
|
|
1942
|
+
async markWrite(namespace, metadata) {
|
|
1943
|
+
await this.touch(namespace, "write", metadata);
|
|
1944
|
+
}
|
|
1945
|
+
async markMaintenance(namespace, jobName, at) {
|
|
1946
|
+
if (typeof jobName !== "string" || jobName.trim().length === 0) {
|
|
1947
|
+
throw new Error("markMaintenance requires a non-empty jobName");
|
|
1948
|
+
}
|
|
1949
|
+
await this.touch(namespace, "maintenance", { at }, jobName.trim());
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Register namespaces known purely from config (default, shared, explicit
|
|
1953
|
+
* policies). Source `config`. Cheap and idempotent.
|
|
1954
|
+
*/
|
|
1955
|
+
async registerConfiguredNamespaces() {
|
|
1956
|
+
if (!this.enabled) return;
|
|
1957
|
+
const names = /* @__PURE__ */ new Set([
|
|
1958
|
+
this.config.defaultNamespace,
|
|
1959
|
+
this.config.sharedNamespace,
|
|
1960
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
1961
|
+
]);
|
|
1962
|
+
for (const ns of names) {
|
|
1963
|
+
if (!ns) continue;
|
|
1964
|
+
if (normalizeNamespaceIdentity(ns) !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
await this.register(ns, { discoveredBy: "config" });
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Register a namespace whose storage was just resolved by the router. Used as
|
|
1975
|
+
* the router's integration hook (`discoveredBy: config`). Storage dir is
|
|
1976
|
+
* provided so we do not re-resolve it. Failure-tolerant. Returns whether the
|
|
1977
|
+
* registration actually APPENDED (round 6, codex P2 — NEFoX), so the router's
|
|
1978
|
+
* resolve-hook dedup only marks a namespace notified when it truly persisted —
|
|
1979
|
+
* a dropped append (disabled catalog or rebuild-lock-timeout drop) returns
|
|
1980
|
+
* `false` and is retried on the next resolve.
|
|
1981
|
+
*/
|
|
1982
|
+
async registerResolved(namespace, storageDir) {
|
|
1983
|
+
if (!this.enabled) return false;
|
|
1984
|
+
return this.register(namespace, { discoveredBy: "config", storageDir });
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Generic register/touch without changing read/write timestamps unless the
|
|
1988
|
+
* source implies it. Validates the namespace and resolves a storage dir.
|
|
1989
|
+
* Returns whether the touch actually appended.
|
|
1990
|
+
*/
|
|
1991
|
+
async register(namespace, metadata) {
|
|
1992
|
+
return this.touch(namespace, "register", metadata);
|
|
1993
|
+
}
|
|
1994
|
+
validateNamespace(namespace) {
|
|
1995
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
1996
|
+
if (ns.length === 0) throw new Error("empty namespace");
|
|
1997
|
+
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
1998
|
+
throw new Error(`unsafe namespace: ${ns}`);
|
|
1999
|
+
}
|
|
2000
|
+
return ns;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Resolve the on-disk storage dir for a namespace WITHOUT trusting caller
|
|
2004
|
+
* input. The default namespace may use the legacy memoryDir root; everything
|
|
2005
|
+
* else lives under `<memoryDir>/namespaces/<token>`. Containment is enforced
|
|
2006
|
+
* by rejecting separators/parent-refs in the token.
|
|
2007
|
+
*/
|
|
2008
|
+
resolveStorageDir(namespace) {
|
|
2009
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2010
|
+
return this.memoryDir;
|
|
2011
|
+
}
|
|
2012
|
+
const token = namespaceIdentityToken(namespace);
|
|
2013
|
+
return this.namespaceTokenDir(token);
|
|
2014
|
+
}
|
|
2015
|
+
namespaceTokenDir(token) {
|
|
2016
|
+
if (token.length === 0 || token.includes("/") || token.includes("\\") || token.includes("..") || path2.isAbsolute(token)) {
|
|
2017
|
+
throw new Error(`unsafe namespace token: ${token}`);
|
|
2018
|
+
}
|
|
2019
|
+
return path2.join(this.memoryDir, "namespaces", token);
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Whether a candidate storage dir is LEXICALLY contained: it is either the
|
|
2023
|
+
* legacy default root (`memoryDir`) or a strict descendant of
|
|
2024
|
+
* `<memoryDir>/namespaces/`. The router legitimately resolves a namespace to
|
|
2025
|
+
* EITHER the tokenized dir or a legacy raw-name dir under `namespaces/`, so we
|
|
2026
|
+
* accept any contained child rather than a single exact token path. This is a
|
|
2027
|
+
* pure string check — symlink escape is checked separately via realpath.
|
|
2028
|
+
*/
|
|
2029
|
+
isLexicallyContained(candidate) {
|
|
2030
|
+
const resolved = path2.resolve(candidate);
|
|
2031
|
+
if (resolved === path2.resolve(this.memoryDir)) return true;
|
|
2032
|
+
const nsBase = path2.resolve(path2.join(this.memoryDir, "namespaces"));
|
|
2033
|
+
const rel = path2.relative(nsBase, resolved);
|
|
2034
|
+
return rel.length > 0 && !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Whether a candidate storage dir satisfies the catalog containment contract,
|
|
2038
|
+
* including SYMLINK-escape rejection (round 5, codex P2). A lexically-contained
|
|
2039
|
+
* path that is actually a symlink to an outside directory would let maintenance
|
|
2040
|
+
* or QMD follow it outside `memoryDir`. We mirror `rebuildFromDisk`'s posture:
|
|
2041
|
+
* the path must be lexically contained AND, if it exists on disk, neither the
|
|
2042
|
+
* path itself a symlink nor its realpath escaping the memory root. Non-existent
|
|
2043
|
+
* paths pass the realpath stage (nothing to follow yet) but still must be
|
|
2044
|
+
* lexically contained.
|
|
2045
|
+
*/
|
|
2046
|
+
async isContainedStorageDir(candidate) {
|
|
2047
|
+
if (!this.isLexicallyContained(candidate)) return false;
|
|
2048
|
+
if (path2.resolve(candidate) === path2.resolve(this.memoryDir)) return true;
|
|
2049
|
+
let memoryReal;
|
|
2050
|
+
try {
|
|
2051
|
+
memoryReal = await realpath(this.memoryDir);
|
|
2052
|
+
} catch {
|
|
2053
|
+
memoryReal = path2.resolve(this.memoryDir);
|
|
2054
|
+
}
|
|
2055
|
+
if (await this.hasSymlinkedAncestor(candidate)) return false;
|
|
2056
|
+
try {
|
|
2057
|
+
const stat4 = await lstat(candidate);
|
|
2058
|
+
if (stat4.isSymbolicLink()) return false;
|
|
2059
|
+
if (!stat4.isDirectory()) return false;
|
|
2060
|
+
} catch {
|
|
2061
|
+
return this.isNearestExistingAncestorContained(candidate, memoryReal);
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
const real = await realpath(candidate);
|
|
2065
|
+
return isPathInside(memoryReal, real);
|
|
2066
|
+
} catch {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Reject a candidate whose path crosses a SYMLINKED ancestor strictly between
|
|
2072
|
+
* memoryDir and the leaf (codex NVuq5). `realpath`-based containment accepts a
|
|
2073
|
+
* symlinked `<memoryDir>/namespaces` that currently resolves back inside
|
|
2074
|
+
* memoryDir, but the disk scanner rejects such a root and a later retarget would
|
|
2075
|
+
* escape the memory tree — so refuse it here too. The leaf itself is
|
|
2076
|
+
* symlink-checked by the caller; this walks only the intermediate ancestors.
|
|
2077
|
+
*/
|
|
2078
|
+
async hasSymlinkedAncestor(candidate) {
|
|
2079
|
+
const stopAt = path2.resolve(this.memoryDir);
|
|
2080
|
+
let dir = path2.dirname(path2.resolve(candidate));
|
|
2081
|
+
const root = path2.parse(dir).root;
|
|
2082
|
+
while (dir !== stopAt && dir !== root && dir !== path2.dirname(dir)) {
|
|
2083
|
+
try {
|
|
2084
|
+
if ((await lstat(dir)).isSymbolicLink()) return true;
|
|
2085
|
+
} catch {
|
|
2086
|
+
}
|
|
2087
|
+
dir = path2.dirname(dir);
|
|
2088
|
+
}
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Walk up from a not-yet-existing candidate to the nearest ancestor that exists
|
|
2093
|
+
* on disk and verify its realpath stays inside `memoryReal` (round 6, codex P2
|
|
2094
|
+
* — NDo79). Rejects a non-existent leaf whose existing parent chain escapes
|
|
2095
|
+
* memoryDir via a symlink. Stops at memoryDir's resolved root.
|
|
2096
|
+
*
|
|
2097
|
+
* The nearest existing ancestor must also be a DIRECTORY (NHIdt, codex P2): if
|
|
2098
|
+
* an existing parent such as `<memoryDir>/namespaces` is a regular FILE (or
|
|
2099
|
+
* socket/fifo), `realpath(parent)` still succeeds and resolves inside memoryDir,
|
|
2100
|
+
* so a containment-only check would ACCEPT a leaf that can never be created — you
|
|
2101
|
+
* cannot mkdir a child under a file. We `lstat` the nearest existing ancestor and
|
|
2102
|
+
* reject when it is not a directory, mirroring the leaf non-directory rejection
|
|
2103
|
+
* (NF21i) and the disk scan, so every containment consumer agrees.
|
|
2104
|
+
*/
|
|
2105
|
+
async isNearestExistingAncestorContained(candidate, memoryReal) {
|
|
2106
|
+
let dir = path2.resolve(candidate);
|
|
2107
|
+
const root = path2.parse(dir).root;
|
|
2108
|
+
for (; ; ) {
|
|
2109
|
+
const parent = path2.dirname(dir);
|
|
2110
|
+
if (parent === dir || dir === root) return false;
|
|
2111
|
+
let real;
|
|
2112
|
+
try {
|
|
2113
|
+
real = await realpath(parent);
|
|
2114
|
+
} catch {
|
|
2115
|
+
dir = parent;
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
if (!(isPathInside(memoryReal, real) || real === memoryReal)) return false;
|
|
2119
|
+
try {
|
|
2120
|
+
const stat4 = await lstat(real);
|
|
2121
|
+
return stat4.isDirectory();
|
|
2122
|
+
} catch {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Resolve the storage dir to persist for a touch, validating any caller-
|
|
2129
|
+
* provided `metadata.storageDir` against the catalog containment contract
|
|
2130
|
+
* (round 4 + round 5, codex P2). `markWrite`/`registerResolved` accept an
|
|
2131
|
+
* explicit storageDir, but persisting it verbatim would let a bad hook or
|
|
2132
|
+
* external consumer write an arbitrary path — including one outside `memoryDir`
|
|
2133
|
+
* or a symlink that escapes it — into the catalog, handing maintenance/QMD an
|
|
2134
|
+
* unsafe root. We accept an explicit (or previously-stored) dir ONLY when it
|
|
2135
|
+
* stays contained under memoryDir (lexically AND via realpath); otherwise we
|
|
2136
|
+
* drop it and fall back to the trusted resolved dir.
|
|
2137
|
+
*/
|
|
2138
|
+
async resolveTouchStorageDir(namespace, explicit, existingDir) {
|
|
2139
|
+
if (explicit !== void 0 && await this.isContainedStorageDir(explicit) && await this.isStorageDirForNamespace(namespace, explicit)) {
|
|
2140
|
+
return explicit;
|
|
2141
|
+
}
|
|
2142
|
+
if (existingDir !== void 0 && await this.isContainedStorageDir(existingDir) && await this.isStorageDirForNamespace(namespace, existingDir)) {
|
|
2143
|
+
return existingDir;
|
|
2144
|
+
}
|
|
2145
|
+
return this.resolveSafeStorageDir(namespace);
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Whether `candidate` is a legitimate storage root FOR `namespace` (round 6,
|
|
2149
|
+
* codex P2 — NDATT). Accepts the namespace's router-resolved root, its canonical
|
|
2150
|
+
* lexical tokenized dir, and (for the default namespace only) memoryDir. This
|
|
2151
|
+
* prevents a contained-but-CROSS-NAMESPACE path — another namespace's tree, or
|
|
2152
|
+
* memoryDir for a non-default namespace — from being persisted as this
|
|
2153
|
+
* namespace's root. Compared on resolved (absolute) paths.
|
|
2154
|
+
*/
|
|
2155
|
+
async isStorageDirForNamespace(namespace, candidate) {
|
|
2156
|
+
const resolvedCandidate = path2.resolve(candidate);
|
|
2157
|
+
const valid = /* @__PURE__ */ new Set();
|
|
2158
|
+
try {
|
|
2159
|
+
valid.add(path2.resolve(this.namespaceTokenDir(namespaceIdentityToken(namespace))));
|
|
2160
|
+
} catch {
|
|
2161
|
+
}
|
|
2162
|
+
try {
|
|
2163
|
+
valid.add(path2.resolve(this.namespaceTokenDir(namespace)));
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
valid.add(path2.resolve(await resolveNamespaceStorageRoot(this.config, namespace)));
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2171
|
+
valid.add(path2.resolve(this.memoryDir));
|
|
2172
|
+
try {
|
|
2173
|
+
valid.add(path2.resolve(await resolveDefaultNamespaceRoot(this.config)));
|
|
2174
|
+
} catch {
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return valid.has(resolvedCandidate);
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Resolve the canonical storage dir for a namespace as the LIVE ROUTER would,
|
|
2181
|
+
* but NEVER return a path that escapes the memory root.
|
|
2182
|
+
*
|
|
2183
|
+
* Router alignment (round 4, cursor Medium): a read/register touch with no
|
|
2184
|
+
* explicit storageDir previously used the lexical `resolveStorageDir`, which
|
|
2185
|
+
* always picks `<memoryDir>/namespaces/<token>` (or `memoryDir` for the
|
|
2186
|
+
* default). That diverges from `NamespaceStorageRouter`, which can route to a
|
|
2187
|
+
* legacy raw-name dir or a migrated default root — so a recall touch could
|
|
2188
|
+
* record a contained-but-WRONG root that maintenance/rebuild then targets. We
|
|
2189
|
+
* now delegate to the shared `resolveNamespaceStorageRoot` (the very helper the
|
|
2190
|
+
* router uses) so the catalog records the same on-disk root the router serves.
|
|
2191
|
+
*
|
|
2192
|
+
* Containment (round 5, codex P2): the resolved path can still be a symlink
|
|
2193
|
+
* escaping memoryDir, so we run the full (lexical + realpath) containment
|
|
2194
|
+
* contract. When it FAILS we fall back to a NAMESPACE-SPECIFIC safe root, NOT
|
|
2195
|
+
* a blanket `memoryDir`. Recording `memoryDir` for a non-default namespace
|
|
2196
|
+
* would point enumeration/maintenance at the DEFAULT namespace's tree (round 5,
|
|
2197
|
+
* cursor/codex Medium/P2) — a cross-namespace fanout error. The correct safe
|
|
2198
|
+
* root is the namespace's own lexical tokenized dir
|
|
2199
|
+
* (`<memoryDir>/namespaces/<token>`), which is always contained and is that
|
|
2200
|
+
* namespace's canonical location (we record the lexical PATH as metadata; we do
|
|
2201
|
+
* not follow the escaping symlink). Only the default namespace — or a token so
|
|
2202
|
+
* unsafe even the lexical dir cannot be built — falls back to `memoryDir`.
|
|
2203
|
+
*/
|
|
2204
|
+
async resolveSafeStorageDir(namespace) {
|
|
2205
|
+
let resolved;
|
|
2206
|
+
try {
|
|
2207
|
+
resolved = await resolveNamespaceStorageRoot(this.config, namespace);
|
|
2208
|
+
} catch {
|
|
2209
|
+
return this.safeFallbackStorageDir(namespace);
|
|
2210
|
+
}
|
|
2211
|
+
if (await this.isContainedStorageDir(resolved)) return resolved;
|
|
2212
|
+
return this.safeFallbackStorageDir(namespace);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* The namespace-specific contained fallback root, used when the router-resolved
|
|
2216
|
+
* root fails containment (round 5, cursor/codex Medium/P2).
|
|
2217
|
+
*
|
|
2218
|
+
* Preference order:
|
|
2219
|
+
* 1. The namespace's OWN lexical tokenized dir (`namespaces/<token>`) — so a
|
|
2220
|
+
* non-default namespace is NOT pointed at the DEFAULT namespace's `memoryDir`
|
|
2221
|
+
* tree (which would misdirect maintenance fanout). Returned only when the
|
|
2222
|
+
* token dir itself stays CONTAINED (it is not a symlink, and its realpath
|
|
2223
|
+
* does not escape memoryDir — e.g. via a symlinked `namespaces/` parent).
|
|
2224
|
+
* 2. `memoryDir` as a LAST resort — for the default namespace, an unsafe token
|
|
2225
|
+
* that cannot build a contained path, OR the irreparable case where the
|
|
2226
|
+
* token dir's realpath escapes the root (so even its lexical path resolves
|
|
2227
|
+
* outside). NF21m note (codex P2): we deliberately do NOT record the lexical
|
|
2228
|
+
* token dir in that irreparable case — its realpath escapes memoryDir, and
|
|
2229
|
+
* the NDo79 contract REQUIRES that an escaping path is never persisted (a
|
|
2230
|
+
* later mkdir/maintenance/QMD op would follow it outside the root). Since no
|
|
2231
|
+
* contained namespace-specific path exists, containment wins: `memoryDir` is
|
|
2232
|
+
* the only safe root left. A namespace whose token dir's realpath escapes is
|
|
2233
|
+
* an irreparable on-disk state; recording the contained default root is
|
|
2234
|
+
* strictly safer than persisting an escaping one. The common case where the
|
|
2235
|
+
* token dir IS contained is handled by branch 1, so a healthy non-default
|
|
2236
|
+
* namespace never reaches `memoryDir`.
|
|
2237
|
+
*/
|
|
2238
|
+
async safeFallbackStorageDir(namespace) {
|
|
2239
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) return this.memoryDir;
|
|
2240
|
+
let tokenDir;
|
|
2241
|
+
try {
|
|
2242
|
+
tokenDir = this.namespaceTokenDir(namespaceIdentityToken(namespace));
|
|
2243
|
+
} catch {
|
|
2244
|
+
return this.memoryDir;
|
|
2245
|
+
}
|
|
2246
|
+
if (await this.isContainedStorageDir(tokenDir)) return tokenDir;
|
|
2247
|
+
return this.memoryDir;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Re-check, NOW, whether a namespace's storage root currently EXISTS on disk
|
|
2251
|
+
* with the SAME safety the directory scan uses (NFJV8, codex P2).
|
|
2252
|
+
*
|
|
2253
|
+
* The rebuild's final re-merge runs under the held lock and folds the freshly
|
|
2254
|
+
* re-read log (`latest`) into the scanned `rebuilt` set. A namespace present in
|
|
2255
|
+
* `latest` (a live touch row) but ABSENT from `rebuilt` is normally PURGED as
|
|
2256
|
+
* deleted (the NATqU "disk scan is authoritative" rule). But there is a TOCTOU
|
|
2257
|
+
* window: a dynamic namespace can be CREATED on disk AFTER `rebuildFromDisk()`
|
|
2258
|
+
* already enumerated `namespaces/` but BEFORE this re-merge. The scan snapshot
|
|
2259
|
+
* missed its new root, yet a gateway `markWrite` already appended a row for it.
|
|
2260
|
+
* Blindly purging that row would rewrite the catalog WITHOUT a live namespace
|
|
2261
|
+
* that now has data on disk, so `writtenSince`/maintenance/QMD consumers miss
|
|
2262
|
+
* it until another touch or rebuild.
|
|
2263
|
+
*
|
|
2264
|
+
* So before purging, we re-resolve the namespace's safe storage root (the same
|
|
2265
|
+
* router-aligned, containment-checked path the scan would have catalogued) and
|
|
2266
|
+
* confirm it is a real, contained, non-symlink directory that actually holds
|
|
2267
|
+
* memory data RIGHT NOW. If so the namespace was created-after-scan and is LIVE
|
|
2268
|
+
* — KEEP its row. This is the precise inverse of NATqU and does NOT reintroduce
|
|
2269
|
+
* it: a touch on a REMOVED root re-checks as ABSENT (no data on disk) and is
|
|
2270
|
+
* still purged; only a root that EXISTS on a fresh re-check is kept.
|
|
2271
|
+
*
|
|
2272
|
+
* Mirrors the per-entry scan checks (symlink rejection + realpath containment +
|
|
2273
|
+
* `hasMemoryData`) so a symlinked/escaping root is never resurrected.
|
|
2274
|
+
*/
|
|
2275
|
+
async liveStorageRootExistsForRebuild(namespace, memoryReal) {
|
|
2276
|
+
let root;
|
|
2277
|
+
try {
|
|
2278
|
+
root = await this.resolveSafeStorageDir(namespace);
|
|
2279
|
+
} catch {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
if (normalizeNamespaceIdentity(namespace) !== this.defaultNamespaceIdentity && path2.resolve(root) === path2.resolve(this.memoryDir)) {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
let stat4;
|
|
2286
|
+
try {
|
|
2287
|
+
stat4 = await lstat(root);
|
|
2288
|
+
} catch {
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
if (stat4.isSymbolicLink()) return false;
|
|
2292
|
+
if (!stat4.isDirectory()) return false;
|
|
2293
|
+
try {
|
|
2294
|
+
const real = await realpath(root);
|
|
2295
|
+
if (memoryReal && !isPathInside(memoryReal, real)) return false;
|
|
2296
|
+
} catch {
|
|
2297
|
+
return false;
|
|
2298
|
+
}
|
|
2299
|
+
return hasMemoryData(root);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Record a namespace touch. Returns whether the touch actually APPENDED to the
|
|
2303
|
+
* log (round 6, codex P2 — NEFoX): a disabled catalog or a dropped append (the
|
|
2304
|
+
* NAUf7 rebuild-lock-timeout drop) returns `false`, so callers (e.g. the router
|
|
2305
|
+
* resolve-hook dedup) can avoid marking a dropped registration as completed and
|
|
2306
|
+
* suppressing its retry.
|
|
2307
|
+
*/
|
|
2308
|
+
async touch(namespace, kind, metadata, jobName) {
|
|
2309
|
+
if (!this.enabled) return false;
|
|
2310
|
+
const ns = this.validateNamespace(namespace);
|
|
2311
|
+
const nowIso = (metadata?.at ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2312
|
+
return this.queueCritical(
|
|
2313
|
+
async () => this.withHeldCatalogLock(async (acquired) => {
|
|
2314
|
+
if (!acquired) return false;
|
|
2315
|
+
if (this.onTouchCriticalSectionForTest) {
|
|
2316
|
+
await this.onTouchCriticalSectionForTest();
|
|
2317
|
+
}
|
|
2318
|
+
const records = await this.loadCompacted();
|
|
2319
|
+
const existing = records.get(ns);
|
|
2320
|
+
const storageDir = await this.resolveTouchStorageDir(
|
|
2321
|
+
ns,
|
|
2322
|
+
metadata?.storageDir,
|
|
2323
|
+
existing?.storageDir
|
|
2324
|
+
);
|
|
2325
|
+
const record = existing ? { ...existing } : {
|
|
2326
|
+
namespace: ns,
|
|
2327
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2328
|
+
kind: metadata?.kind ?? inferKind(ns, this.config),
|
|
2329
|
+
createdAt: nowIso,
|
|
2330
|
+
storageDir,
|
|
2331
|
+
discoveredBy: metadata?.discoveredBy ?? (kind === "register" ? "config" : kind === "maintenance" ? "scan" : kind)
|
|
2332
|
+
};
|
|
2333
|
+
record.storageDir = storageDir;
|
|
2334
|
+
if (metadata?.kind) record.kind = metadata.kind;
|
|
2335
|
+
if (metadata?.principal !== void 0) record.principal = metadata.principal;
|
|
2336
|
+
if (metadata?.projectId !== void 0) record.projectId = metadata.projectId;
|
|
2337
|
+
if (metadata?.branch !== void 0) record.branch = metadata.branch;
|
|
2338
|
+
if (metadata?.parentNamespace !== void 0)
|
|
2339
|
+
record.parentNamespace = metadata.parentNamespace;
|
|
2340
|
+
if (kind === "write" && existing && record.discoveredBy === "config") {
|
|
2341
|
+
record.discoveredBy = "write";
|
|
2342
|
+
}
|
|
2343
|
+
if (kind === "read") record.lastReadAt = nowIso;
|
|
2344
|
+
if (kind === "write") record.lastWriteAt = nowIso;
|
|
2345
|
+
if (kind === "maintenance" && jobName) {
|
|
2346
|
+
record.lastMaintenanceAt = { ...record.lastMaintenanceAt ?? {}, [jobName]: nowIso };
|
|
2347
|
+
}
|
|
2348
|
+
await this.appendUnchained(record);
|
|
2349
|
+
return true;
|
|
2350
|
+
})
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
// ── Rebuild from disk ────────────────────────────────────────────────────
|
|
2354
|
+
async rebuildFromDisk(options) {
|
|
2355
|
+
const dryRun = options?.dryRun === true;
|
|
2356
|
+
if (!this.enabled) {
|
|
2357
|
+
return { dryRun, records: [], skipped: [], applied: false };
|
|
2358
|
+
}
|
|
2359
|
+
if (dryRun) {
|
|
2360
|
+
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, false));
|
|
2361
|
+
}
|
|
2362
|
+
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, true));
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Body of `rebuildFromDisk`, run inside a single `queueCritical` turn. MUST
|
|
2366
|
+
* only be invoked from within the serialized chain so the load and the
|
|
2367
|
+
* rewrite are atomic with respect to concurrent touches (in-process).
|
|
2368
|
+
*
|
|
2369
|
+
* `wantMutate` is true for an `--apply` (the caller intends to rewrite). The
|
|
2370
|
+
* cross-process file lock is acquired LATE — only around the final
|
|
2371
|
+
* load→merge→rename window (NFgCT, codex P2) — never across the disk scan, so a
|
|
2372
|
+
* long scan does not force concurrent gateway touches to wait (and drop their
|
|
2373
|
+
* append). Whether the rewrite actually happened is reported via the result's
|
|
2374
|
+
* `applied`: true only when `wantMutate` AND the lock was acquired.
|
|
2375
|
+
*/
|
|
2376
|
+
async rebuildInsideChain(dryRun, wantMutate) {
|
|
2377
|
+
const existing = await this.loadCompacted();
|
|
2378
|
+
const skipped = [];
|
|
2379
|
+
const rebuilt = /* @__PURE__ */ new Map();
|
|
2380
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2381
|
+
let memoryReal = null;
|
|
2382
|
+
try {
|
|
2383
|
+
memoryReal = await realpath(this.memoryDir);
|
|
2384
|
+
} catch {
|
|
2385
|
+
memoryReal = this.memoryDir;
|
|
2386
|
+
}
|
|
2387
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
2388
|
+
const configured = new Set(
|
|
2389
|
+
[
|
|
2390
|
+
this.config.defaultNamespace,
|
|
2391
|
+
this.config.sharedNamespace,
|
|
2392
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
2393
|
+
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
2394
|
+
);
|
|
2395
|
+
const resolvedDefaultRoot = await resolveDefaultNamespaceRoot(this.config);
|
|
2396
|
+
const defaultStorageDir = await this.isContainedStorageDir(resolvedDefaultRoot) ? resolvedDefaultRoot : this.memoryDir;
|
|
2397
|
+
const legacyDefaultHasData = defaultStorageDir === this.memoryDir;
|
|
2398
|
+
for (const ns of configured) {
|
|
2399
|
+
if (!ns) continue;
|
|
2400
|
+
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) {
|
|
2401
|
+
let token;
|
|
2402
|
+
try {
|
|
2403
|
+
token = namespaceIdentityToken(ns);
|
|
2404
|
+
} catch {
|
|
2405
|
+
token = ns;
|
|
2406
|
+
}
|
|
2407
|
+
skipped.push({ token, reason: "unsafe", detail: ns });
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
let storageDir;
|
|
2411
|
+
if (ns === defaultNs) {
|
|
2412
|
+
storageDir = defaultStorageDir;
|
|
2413
|
+
} else {
|
|
2414
|
+
try {
|
|
2415
|
+
storageDir = await resolveNamespaceStorageRoot(this.config, ns);
|
|
2416
|
+
} catch {
|
|
2417
|
+
storageDir = this.namespaceTokenDir(namespaceIdentityToken(ns));
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (!await this.isContainedStorageDir(storageDir)) {
|
|
2421
|
+
if (ns === defaultNs) {
|
|
2422
|
+
storageDir = this.memoryDir;
|
|
2423
|
+
} else {
|
|
2424
|
+
skipped.push({ token: namespaceIdentityToken(ns), reason: "escape", detail: storageDir });
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
rebuilt.set(
|
|
2429
|
+
ns,
|
|
2430
|
+
this.mergeForRebuild(existing.get(ns), {
|
|
2431
|
+
namespace: ns,
|
|
2432
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2433
|
+
kind: inferKind(ns, this.config),
|
|
2434
|
+
createdAt: existing.get(ns)?.createdAt ?? nowIso,
|
|
2435
|
+
storageDir,
|
|
2436
|
+
discoveredBy: "config"
|
|
2437
|
+
})
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
const namespacesDir = path2.join(this.memoryDir, "namespaces");
|
|
2441
|
+
let entries = [];
|
|
2442
|
+
let namespacesDirSafe = true;
|
|
2443
|
+
try {
|
|
2444
|
+
const rootStat = await lstat(namespacesDir);
|
|
2445
|
+
if (rootStat.isSymbolicLink()) {
|
|
2446
|
+
namespacesDirSafe = false;
|
|
2447
|
+
} else {
|
|
2448
|
+
const realNamespacesDir = await realpath(namespacesDir);
|
|
2449
|
+
if (memoryReal && !isPathInside(memoryReal, realNamespacesDir)) {
|
|
2450
|
+
namespacesDirSafe = false;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} catch {
|
|
2454
|
+
namespacesDirSafe = true;
|
|
2455
|
+
}
|
|
2456
|
+
if (!namespacesDirSafe) {
|
|
2457
|
+
skipped.push({ token: "namespaces", reason: "symlink", detail: namespacesDir });
|
|
2458
|
+
} else {
|
|
2459
|
+
try {
|
|
2460
|
+
entries = await readdir(namespacesDir, { withFileTypes: true });
|
|
2461
|
+
} catch {
|
|
2462
|
+
entries = [];
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
const scannedFromTokenized = /* @__PURE__ */ new Set();
|
|
2466
|
+
for (const entry of entries) {
|
|
2467
|
+
const token = entry.name;
|
|
2468
|
+
const fullPath = path2.join(namespacesDir, token);
|
|
2469
|
+
let stat4;
|
|
2470
|
+
try {
|
|
2471
|
+
stat4 = await lstat(fullPath);
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
if (stat4.isSymbolicLink()) {
|
|
2477
|
+
skipped.push({ token, reason: "symlink", detail: fullPath });
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
if (!stat4.isDirectory()) continue;
|
|
2481
|
+
try {
|
|
2482
|
+
const real = await realpath(fullPath);
|
|
2483
|
+
if (memoryReal && !isPathInside(memoryReal, real)) {
|
|
2484
|
+
skipped.push({ token, reason: "escape", detail: real });
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const literalRecord = existing.get(token);
|
|
2492
|
+
const literalOwnsRoot = configured.has(token) || literalRecord !== void 0 && path2.resolve(literalRecord.storageDir) === path2.resolve(fullPath);
|
|
2493
|
+
const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);
|
|
2494
|
+
const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;
|
|
2495
|
+
const decoded = normalizeNamespaceIdentity(rawDecoded);
|
|
2496
|
+
if (decoded.length === 0 || rawDecoded !== decoded) {
|
|
2497
|
+
skipped.push({ token, reason: "unsafe", detail: rawDecoded });
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
if (decoded !== defaultNs && !isSafeRouteNamespace(decoded)) {
|
|
2501
|
+
skipped.push({ token, reason: "unsafe", detail: decoded });
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
const memoryData = await inspectMemoryDataRoot(fullPath);
|
|
2505
|
+
if (memoryData.invalidMarker) {
|
|
2506
|
+
skipped.push({
|
|
2507
|
+
token,
|
|
2508
|
+
reason: "unsafe",
|
|
2509
|
+
detail: `invalid memory marker: ${memoryData.invalidMarker}`
|
|
2510
|
+
});
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (!memoryData.hasData) continue;
|
|
2514
|
+
if (decoded === defaultNs) {
|
|
2515
|
+
const def = rebuilt.get(defaultNs);
|
|
2516
|
+
if (def) {
|
|
2517
|
+
def.storageDir = defaultStorageDir;
|
|
2518
|
+
def.kind = "default";
|
|
2519
|
+
}
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
const isTokenizedEntry = token === namespaceIdentityToken(decoded);
|
|
2523
|
+
if (rebuilt.has(decoded) && scannedFromTokenized.has(decoded) && !isTokenizedEntry) {
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
if (isTokenizedEntry) scannedFromTokenized.add(decoded);
|
|
2527
|
+
const prior = existing.get(decoded);
|
|
2528
|
+
rebuilt.set(
|
|
2529
|
+
decoded,
|
|
2530
|
+
this.mergeForRebuild(prior, {
|
|
2531
|
+
namespace: decoded,
|
|
2532
|
+
identityToken: namespaceIdentityToken(decoded),
|
|
2533
|
+
kind: inferKind(decoded, this.config),
|
|
2534
|
+
createdAt: prior?.createdAt ?? nowIso,
|
|
2535
|
+
storageDir: fullPath,
|
|
2536
|
+
// Configured-and-present namespaces keep config provenance; purely
|
|
2537
|
+
// discovered ones are scan.
|
|
2538
|
+
discoveredBy: configured.has(decoded) ? "config" : prior?.discoveredBy ?? "scan"
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
if (legacyDefaultHasData && defaultStorageDir === this.memoryDir) {
|
|
2543
|
+
const def = rebuilt.get(defaultNs);
|
|
2544
|
+
if (def) def.kind = "default";
|
|
2545
|
+
}
|
|
2546
|
+
if (!wantMutate) {
|
|
2547
|
+
return this.finishRebuild(rebuilt, skipped, dryRun, false, memoryReal, nowIso);
|
|
2548
|
+
}
|
|
2549
|
+
if (this.onRebuildAfterScanForTest) {
|
|
2550
|
+
await this.onRebuildAfterScanForTest();
|
|
2551
|
+
}
|
|
2552
|
+
return this.withHeldCatalogLock(
|
|
2553
|
+
(acquired) => this.finishRebuild(rebuilt, skipped, dryRun, acquired, memoryReal, nowIso)
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Final load→merge→rename window of a rebuild, factored out so the caller can
|
|
2558
|
+
* run it WITHIN the cross-process file lock (NFgCT, codex P2) without holding
|
|
2559
|
+
* that lock across the preceding disk scan. Re-reads the latest on-disk state,
|
|
2560
|
+
* folds concurrent touches, then (when `canMutate`) atomically rewrites the log.
|
|
2561
|
+
*
|
|
2562
|
+
* `canMutate` records that the cross-process lock was actually held. The
|
|
2563
|
+
* re-merge + rewrite run only when it is true — a dry-run, or an unlocked apply
|
|
2564
|
+
* (lock-acquisition timeout), computes records but does NOT rename, so it can
|
|
2565
|
+
* never clobber a concurrent lock holder's window. `applied` mirrors `canMutate`.
|
|
2566
|
+
*/
|
|
2567
|
+
async finishRebuild(rebuilt, skipped, dryRun, canMutate, memoryReal, nowIso) {
|
|
2568
|
+
if (canMutate) {
|
|
2569
|
+
const latest = await this.loadCompacted();
|
|
2570
|
+
for (const [ns, fresh] of latest) {
|
|
2571
|
+
const current = rebuilt.get(ns);
|
|
2572
|
+
if (!current) {
|
|
2573
|
+
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
if (await this.liveStorageRootExistsForRebuild(ns, memoryReal)) {
|
|
2577
|
+
const safeDir = await this.resolveSafeStorageDir(ns);
|
|
2578
|
+
const resolvedSafe = path2.resolve(safeDir);
|
|
2579
|
+
let owningNamespace = null;
|
|
2580
|
+
for (const [otherNs, otherRec] of rebuilt) {
|
|
2581
|
+
if (otherNs !== ns && path2.resolve(otherRec.storageDir) === resolvedSafe) {
|
|
2582
|
+
owningNamespace = otherNs;
|
|
2583
|
+
break;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (owningNamespace) {
|
|
2587
|
+
const owner = rebuilt.get(owningNamespace);
|
|
2588
|
+
if (owner) rebuilt.set(owningNamespace, mergeNewerTouchFields(owner, fresh));
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
rebuilt.set(ns, {
|
|
2592
|
+
...fresh,
|
|
2593
|
+
storageDir: safeDir,
|
|
2594
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2595
|
+
kind: fresh.kind ?? inferKind(ns, this.config),
|
|
2596
|
+
createdAt: fresh.createdAt ?? nowIso
|
|
2597
|
+
});
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
rebuilt.set(ns, mergeNewerTouchFields(current, fresh));
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const records = [...rebuilt.values()].sort((a, b) => {
|
|
2606
|
+
const byName = a.namespace.localeCompare(b.namespace);
|
|
2607
|
+
if (byName !== 0) return byName;
|
|
2608
|
+
return a.identityToken.localeCompare(b.identityToken);
|
|
2609
|
+
});
|
|
2610
|
+
if (canMutate) {
|
|
2611
|
+
if (this.onRebuildBeforeRenameForTest) {
|
|
2612
|
+
await this.onRebuildBeforeRenameForTest();
|
|
2613
|
+
}
|
|
2614
|
+
await this.rewriteUnchained(records);
|
|
2615
|
+
}
|
|
2616
|
+
return { dryRun, records, skipped, applied: canMutate };
|
|
2617
|
+
}
|
|
2618
|
+
// ── Cross-process catalog write lock (held mutex) ────────────────────────
|
|
2619
|
+
/**
|
|
2620
|
+
* Run `fn` while HOLDING the shared cross-process advisory lock (round 5, codex
|
|
2621
|
+
* P2; generalized round 7 — NEZkA). This is the SINGLE mutex shared by BOTH the
|
|
2622
|
+
* touch read→merge→append window AND the rebuild final load→merge→rename window,
|
|
2623
|
+
* so a touch and a rebuild in different processes are mutually exclusive over
|
|
2624
|
+
* their respective critical sections — closing the check-then-append gap where a
|
|
2625
|
+
* polled-only touch could append into a rebuild's load→rename window.
|
|
2626
|
+
*
|
|
2627
|
+
* Acquisition is atomic via `open(..., "wx")`. A lock older than
|
|
2628
|
+
* `REBUILD_LOCK_STALE_MS` is treated as a crashed holder and broken. After
|
|
2629
|
+
* `REBUILD_LOCK_MAX_WAIT_MS` of contention we proceed best-effort WITHOUT the
|
|
2630
|
+
* lock rather than block forever. The lock is always released in `finally`.
|
|
2631
|
+
*
|
|
2632
|
+
* IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the
|
|
2633
|
+
* per-process `queueCritical` chain, which serializes all catalog mutations in
|
|
2634
|
+
* THIS process. So within one process only one logical holder attempts OS-lock
|
|
2635
|
+
* acquisition at a time — the file lock is never self-contended in-process, and
|
|
2636
|
+
* the lock is acquired and released within a single in-process turn. The file
|
|
2637
|
+
* lock adds only the missing CROSS-process exclusion.
|
|
2638
|
+
*
|
|
2639
|
+
* HEARTBEAT (round 5, cursor/codex Medium/P2): while WE hold the lock a timer
|
|
2640
|
+
* refreshes its mtime every `REBUILD_LOCK_HEARTBEAT_MS`, so a legitimately long
|
|
2641
|
+
* holder (> `REBUILD_LOCK_STALE_MS`) is not treated as a crashed holder and
|
|
2642
|
+
* unlinked by another process — which would let overlapping windows lose
|
|
2643
|
+
* appends. Heartbeat failures are swallowed; the timer is always cleared in
|
|
2644
|
+
* `finally`.
|
|
2645
|
+
*
|
|
2646
|
+
* ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE
|
|
2647
|
+
* actually hold the lock. When acquisition TIMED OUT (another holder is active),
|
|
2648
|
+
* a MUTATING rebuild must NOT perform its load/rename window unlocked, and a
|
|
2649
|
+
* touch must NOT append unlocked — both would recreate the lost-append race. The
|
|
2650
|
+
* caller uses `acquired` to run compute-only (rebuild) or DROP the append
|
|
2651
|
+
* (touch) when unlocked.
|
|
2652
|
+
*/
|
|
2653
|
+
async withHeldCatalogLock(fn) {
|
|
2654
|
+
const acquired = await this.acquireRebuildLock();
|
|
2655
|
+
let heartbeat;
|
|
2656
|
+
if (acquired) {
|
|
2657
|
+
heartbeat = setInterval(() => {
|
|
2658
|
+
const now = /* @__PURE__ */ new Date();
|
|
2659
|
+
utimes(this.rebuildLockPath, now, now).catch(() => void 0);
|
|
2660
|
+
}, REBUILD_LOCK_HEARTBEAT_MS);
|
|
2661
|
+
heartbeat.unref?.();
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
return await fn(acquired);
|
|
2665
|
+
} finally {
|
|
2666
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2667
|
+
if (acquired) {
|
|
2668
|
+
try {
|
|
2669
|
+
if (await this.rebuildLockHeldBySelf()) {
|
|
2670
|
+
await unlink(this.rebuildLockPath);
|
|
2671
|
+
}
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
/** Try to acquire the rebuild lock; returns true if WE created it. */
|
|
2678
|
+
async acquireRebuildLock() {
|
|
2679
|
+
const deadline = Date.now() + REBUILD_LOCK_MAX_WAIT_MS;
|
|
2680
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2681
|
+
for (; ; ) {
|
|
2682
|
+
try {
|
|
2683
|
+
const handle = await open(this.rebuildLockPath, "wx");
|
|
2684
|
+
try {
|
|
2685
|
+
await handle.writeFile(
|
|
2686
|
+
`${process.pid} ${this.lockOwnerId} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2687
|
+
`,
|
|
2688
|
+
"utf8"
|
|
2689
|
+
);
|
|
2690
|
+
} catch {
|
|
2691
|
+
} finally {
|
|
2692
|
+
await handle.close();
|
|
2693
|
+
}
|
|
2694
|
+
return true;
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
if (err?.code !== "EEXIST") {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
await this.breakStaleRebuildLock();
|
|
2700
|
+
if (Date.now() >= deadline) return false;
|
|
2701
|
+
await new Promise((r) => setTimeout(r, REBUILD_LOCK_POLL_MS));
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* Remove the lock file if its mtime is older than the stale threshold.
|
|
2707
|
+
*
|
|
2708
|
+
* REPLACEMENT-SAFE (NG7Bg, codex P2): a plain `stat` → `unlink` has a TOCTOU
|
|
2709
|
+
* window — two processes can both observe the SAME stale lock; one removes it and
|
|
2710
|
+
* creates a FRESH lock, and the other's later `unlink` then deletes that fresh
|
|
2711
|
+
* holder's ACTIVE lock based on the stale identity it read earlier, leaving the
|
|
2712
|
+
* fresh holder running its critical section with no visible lock and reopening the
|
|
2713
|
+
* lost-update race the mutex prevents. We therefore capture the lock's IDENTITY
|
|
2714
|
+
* (its full content line: `<pid> <owner-uuid> <iso>`) when we judge it stale, then
|
|
2715
|
+
* RE-READ immediately before unlinking and only remove it when the content is
|
|
2716
|
+
* byte-identical AND still stale. A replacement lock has a different owner id /
|
|
2717
|
+
* timestamp, so its content differs and we leave it untouched. We never unlink a
|
|
2718
|
+
* lock whose mtime is now fresh (a heartbeat refreshed it) or whose identity
|
|
2719
|
+
* changed (a replacement was created). This is best-effort: any mismatch/vanish
|
|
2720
|
+
* simply skips the break and the caller polls again.
|
|
2721
|
+
*/
|
|
2722
|
+
async breakStaleRebuildLock() {
|
|
2723
|
+
let staleIdentity;
|
|
2724
|
+
try {
|
|
2725
|
+
const info = await stat(this.rebuildLockPath);
|
|
2726
|
+
if (Date.now() - info.mtimeMs <= REBUILD_LOCK_STALE_MS) {
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
staleIdentity = await readFile2(this.rebuildLockPath, "utf8");
|
|
2730
|
+
} catch {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (this.onBeforeBreakStaleUnlinkForTest) {
|
|
2734
|
+
await this.onBeforeBreakStaleUnlinkForTest();
|
|
2735
|
+
}
|
|
2736
|
+
try {
|
|
2737
|
+
const current = await readFile2(this.rebuildLockPath, "utf8");
|
|
2738
|
+
if (current !== staleIdentity) return;
|
|
2739
|
+
const recheck = await stat(this.rebuildLockPath);
|
|
2740
|
+
if (Date.now() - recheck.mtimeMs <= REBUILD_LOCK_STALE_MS) return;
|
|
2741
|
+
await unlink(this.rebuildLockPath).catch(() => void 0);
|
|
2742
|
+
} catch {
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Whether the rebuild lock file was written by THIS instance (round 6, codex
|
|
2747
|
+
* P2 — NBsGP). Matches the per-instance owner id, NOT just `process.pid`: two
|
|
2748
|
+
* NamespaceCatalog instances in the same process share a PID, so a PID-only
|
|
2749
|
+
* check would wrongly treat instance A's lock as self-held by instance B and
|
|
2750
|
+
* let B's touch skip the wait and append into A's rebuild window. Falls back to
|
|
2751
|
+
* the legacy PID-only form for lock files written before owner ids existed.
|
|
2752
|
+
*/
|
|
2753
|
+
async rebuildLockHeldBySelf() {
|
|
2754
|
+
try {
|
|
2755
|
+
const body = await readFile2(this.rebuildLockPath, "utf8");
|
|
2756
|
+
const parts = body.trim().split(/\s+/);
|
|
2757
|
+
const pid = Number.parseInt(parts[0] ?? "", 10);
|
|
2758
|
+
const ownerId = parts[1];
|
|
2759
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2760
|
+
if (ownerId && UUID_RE.test(ownerId)) {
|
|
2761
|
+
return ownerId === this.lockOwnerId;
|
|
2762
|
+
}
|
|
2763
|
+
return Number.isFinite(pid) && pid === process.pid;
|
|
2764
|
+
} catch {
|
|
2765
|
+
return false;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Merge a prior record's preserved metadata (timestamps, principal hints)
|
|
2770
|
+
* onto a freshly-discovered record. Disk-derived fields (storageDir, kind)
|
|
2771
|
+
* take precedence from the new record.
|
|
2772
|
+
*
|
|
2773
|
+
* PROVENANCE (round 3, cursor Low): `discoveredBy` and `createdAt` are
|
|
2774
|
+
* CREATION-ONLY — identical to the touch path's invariant. A rebuild must NOT
|
|
2775
|
+
* reset a namespace first seen via a `write`/`read` touch back to `config`
|
|
2776
|
+
* just because it is also listed in policies. So when a prior record exists we
|
|
2777
|
+
* carry its `discoveredBy` forward; only brand-new records keep the fresh
|
|
2778
|
+
* (config/scan) provenance.
|
|
2779
|
+
*/
|
|
2780
|
+
mergeForRebuild(prior, fresh) {
|
|
2781
|
+
if (!prior) return fresh;
|
|
2782
|
+
const merged = {
|
|
2783
|
+
...fresh,
|
|
2784
|
+
createdAt: prior.createdAt ?? fresh.createdAt,
|
|
2785
|
+
discoveredBy: prior.discoveredBy ?? fresh.discoveredBy
|
|
2786
|
+
};
|
|
2787
|
+
if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;
|
|
2788
|
+
if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;
|
|
2789
|
+
if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };
|
|
2790
|
+
if (prior.principal !== void 0) merged.principal = prior.principal;
|
|
2791
|
+
if (prior.projectId !== void 0) merged.projectId = prior.projectId;
|
|
2792
|
+
if (prior.branch !== void 0) merged.branch = prior.branch;
|
|
2793
|
+
if (prior.parentNamespace !== void 0) merged.parentNamespace = prior.parentNamespace;
|
|
2794
|
+
return merged;
|
|
2795
|
+
}
|
|
2796
|
+
// ── Persistence ──────────────────────────────────────────────────────────
|
|
2797
|
+
/** Load the JSONL log and fold it into current state (last-record-wins). */
|
|
2798
|
+
async loadCompacted() {
|
|
2799
|
+
const records = /* @__PURE__ */ new Map();
|
|
2800
|
+
let raw;
|
|
2801
|
+
try {
|
|
2802
|
+
raw = await readFile2(this.catalogPath, "utf8");
|
|
2803
|
+
} catch {
|
|
2804
|
+
return records;
|
|
2805
|
+
}
|
|
2806
|
+
for (const line of raw.split("\n")) {
|
|
2807
|
+
const trimmed = line.trim();
|
|
2808
|
+
if (trimmed.length === 0) continue;
|
|
2809
|
+
let parsed;
|
|
2810
|
+
try {
|
|
2811
|
+
parsed = JSON.parse(trimmed);
|
|
2812
|
+
} catch {
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
const record = coerceRecord(parsed);
|
|
2816
|
+
if (!record) continue;
|
|
2817
|
+
const prior = records.get(record.namespace);
|
|
2818
|
+
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
2819
|
+
}
|
|
2820
|
+
return records;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Serialize an arbitrary read-modify-write critical section through the single
|
|
2824
|
+
* write chain. Every catalog mutation (touch read+merge+append, full rewrite)
|
|
2825
|
+
* runs through this so they are mutually exclusive: a touch always reads the
|
|
2826
|
+
* latest persisted state before appending, and a rebuild rewrite cannot
|
|
2827
|
+
* interleave with a touch's append. The chain recovers from rejection
|
|
2828
|
+
* (CLAUDE.md rule #40) — one failed section never poisons subsequent ones —
|
|
2829
|
+
* while still surfacing the error to that section's awaited promise.
|
|
2830
|
+
*/
|
|
2831
|
+
queueCritical(fn) {
|
|
2832
|
+
const run = this.writeChain.then(fn);
|
|
2833
|
+
this.writeChain = run.then(
|
|
2834
|
+
() => void 0,
|
|
2835
|
+
() => void 0
|
|
2836
|
+
);
|
|
2837
|
+
return run;
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Append a single record to the JSONL log WITHOUT re-serializing through the
|
|
2841
|
+
* write chain. MUST only be called from inside a `queueCritical(...)` section
|
|
2842
|
+
* (which already holds the serialized turn); calling it directly would bypass
|
|
2843
|
+
* the read-before-append ordering that prevents lost-field races.
|
|
2844
|
+
*/
|
|
2845
|
+
async appendUnchained(record) {
|
|
2846
|
+
const line = serializeRecord(record) + "\n";
|
|
2847
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2848
|
+
await appendFile(this.catalogPath, line, "utf8");
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Atomic temp-file + rename rewrite (CLAUDE.md rule #54: write temp, then
|
|
2852
|
+
* rename — never delete-before-write) WITHOUT re-entering the write chain.
|
|
2853
|
+
* MUST only be called from inside a `queueCritical(...)` turn (e.g. the
|
|
2854
|
+
* rebuild critical section, which already holds the serialized turn so its
|
|
2855
|
+
* load and rewrite are atomic against concurrent touches). Re-entering the
|
|
2856
|
+
* chain from within a held turn would deadlock.
|
|
2857
|
+
*/
|
|
2858
|
+
async rewriteUnchained(records) {
|
|
2859
|
+
const body = records.map((r) => serializeRecord(r)).join("\n") + (records.length > 0 ? "\n" : "");
|
|
2860
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2861
|
+
const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2862
|
+
await writeFile2(tmp, body, "utf8");
|
|
2863
|
+
await rename(tmp, this.catalogPath);
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
function isPathInside(root, child) {
|
|
2867
|
+
const relative = path2.relative(root, child);
|
|
2868
|
+
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/maintenance/namespace-planner.ts
|
|
2872
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2873
|
+
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";
|
|
2874
|
+
import path3 from "path";
|
|
2875
|
+
var DEFAULT_MAX_NAMESPACES_PER_CYCLE = 20;
|
|
2876
|
+
var DEFAULT_LOCK_STALE_MS = 10 * 6e4;
|
|
2877
|
+
var LOCK_BASE = "maintenance-locks";
|
|
2878
|
+
var STATUS_BASE = "namespace-maintenance-status";
|
|
2879
|
+
var namespaceMaintenanceFs = { open: open2, rm };
|
|
2880
|
+
function configuredNamespaces(config) {
|
|
2881
|
+
return Array.from(
|
|
2882
|
+
new Set(
|
|
2883
|
+
[config.defaultNamespace, config.sharedNamespace, ...config.namespacePolicies.map((policy) => policy.name)].map((value) => value.trim()).filter(Boolean)
|
|
2884
|
+
)
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
function inferConfiguredKind(config, namespace) {
|
|
2888
|
+
if (namespace === config.defaultNamespace.trim()) return "default";
|
|
2889
|
+
if (namespace === config.sharedNamespace.trim()) return "shared";
|
|
2890
|
+
return "explicit";
|
|
2891
|
+
}
|
|
2892
|
+
function maxNamespacesPerCycle(config) {
|
|
2893
|
+
return Math.max(
|
|
2894
|
+
1,
|
|
2895
|
+
Math.floor(
|
|
2896
|
+
typeof config.maintenanceMaxNamespacesPerCycle === "number" && Number.isFinite(config.maintenanceMaxNamespacesPerCycle) ? config.maintenanceMaxNamespacesPerCycle : DEFAULT_MAX_NAMESPACES_PER_CYCLE
|
|
2897
|
+
)
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
function namespaceKindAllowed(config, kind) {
|
|
2901
|
+
switch (kind) {
|
|
2902
|
+
case "branch":
|
|
2903
|
+
return config.maintenanceIncludeBranchNamespaces === true;
|
|
2904
|
+
case "project":
|
|
2905
|
+
return config.maintenanceIncludeProjectNamespaces !== false;
|
|
2906
|
+
case "team-project":
|
|
2907
|
+
return config.maintenanceIncludeTeamProjectNamespaces !== false;
|
|
2908
|
+
default:
|
|
2909
|
+
return true;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
function disabledReasonForKind(kind) {
|
|
2913
|
+
if (kind === "branch") return "branch_disabled";
|
|
2914
|
+
if (kind === "project") return "project_disabled";
|
|
2915
|
+
if (kind === "team-project") return "team_project_disabled";
|
|
2916
|
+
return "fanout_disabled";
|
|
2917
|
+
}
|
|
2918
|
+
async function catalogRootIsLive(config, record) {
|
|
2919
|
+
if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
try {
|
|
2923
|
+
const liveRoot = await resolveNamespaceStorageRoot(config, record.namespace);
|
|
2924
|
+
if (path3.resolve(liveRoot) !== path3.resolve(record.storageDir)) return false;
|
|
2925
|
+
return hasMemoryData(liveRoot);
|
|
2926
|
+
} catch {
|
|
2927
|
+
return false;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
function candidateSortKey(candidate) {
|
|
2931
|
+
const write = candidate.lastWriteAt ?? "";
|
|
2932
|
+
return `${write}\0${candidate.namespace}`;
|
|
2933
|
+
}
|
|
2934
|
+
function candidatePriority(candidate) {
|
|
2935
|
+
if (candidate.kind === "default") return 0;
|
|
2936
|
+
if (candidate.kind === "shared") return 1;
|
|
2937
|
+
if (candidate.source === "configured") return 2;
|
|
2938
|
+
if (candidate.kind === "team-project") return 3;
|
|
2939
|
+
if (candidate.kind === "project") return 4;
|
|
2940
|
+
if (candidate.kind === "self") return 5;
|
|
2941
|
+
if (candidate.kind === "legacy") return 6;
|
|
2942
|
+
if (candidate.kind === "branch") return 8;
|
|
2943
|
+
return 7;
|
|
2944
|
+
}
|
|
2945
|
+
function sortCandidates(a, b) {
|
|
2946
|
+
const priority = candidatePriority(a) - candidatePriority(b);
|
|
2947
|
+
if (priority !== 0) return priority;
|
|
2948
|
+
const am = Date.parse(a.lastMaintenanceAt ?? "");
|
|
2949
|
+
const bm = Date.parse(b.lastMaintenanceAt ?? "");
|
|
2950
|
+
const aMaintained = Number.isFinite(am);
|
|
2951
|
+
const bMaintained = Number.isFinite(bm);
|
|
2952
|
+
if (aMaintained && bMaintained && am !== bm) return am - bm;
|
|
2953
|
+
if (aMaintained !== bMaintained) return aMaintained ? 1 : -1;
|
|
2954
|
+
const aw = Date.parse(a.lastWriteAt ?? "");
|
|
2955
|
+
const bw = Date.parse(b.lastWriteAt ?? "");
|
|
2956
|
+
const aValid = Number.isFinite(aw);
|
|
2957
|
+
const bValid = Number.isFinite(bw);
|
|
2958
|
+
if (aValid && bValid && aw !== bw) return bw - aw;
|
|
2959
|
+
if (aValid !== bValid) return aValid ? -1 : 1;
|
|
2960
|
+
const byKey = candidateSortKey(a).localeCompare(candidateSortKey(b));
|
|
2961
|
+
if (byKey !== 0) return byKey;
|
|
2962
|
+
return a.namespace.localeCompare(b.namespace);
|
|
2963
|
+
}
|
|
2964
|
+
async function planNamespaceMaintenance(config, options) {
|
|
2965
|
+
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2966
|
+
const configured = configuredNamespaces(config);
|
|
2967
|
+
const byNamespace = /* @__PURE__ */ new Map();
|
|
2968
|
+
const skipped = [];
|
|
2969
|
+
for (const namespace of configured) {
|
|
2970
|
+
const kind = inferConfiguredKind(config, namespace);
|
|
2971
|
+
byNamespace.set(namespace, {
|
|
2972
|
+
namespace,
|
|
2973
|
+
kind,
|
|
2974
|
+
source: "configured"
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
if (config.namespacesEnabled && config.maintenanceNamespaceFanoutEnabled !== false) {
|
|
2978
|
+
const configuredSet = new Set(configured);
|
|
2979
|
+
try {
|
|
2980
|
+
const records = options.catalog?.enabled ? await options.catalog.listNamespaces() : [];
|
|
2981
|
+
for (const record of records) {
|
|
2982
|
+
const namespace = record.namespace.trim();
|
|
2983
|
+
if (!namespace) continue;
|
|
2984
|
+
const isConfigured = configuredSet.has(namespace);
|
|
2985
|
+
const kind = isConfigured ? inferConfiguredKind(config, namespace) : record.kind;
|
|
2986
|
+
if (!namespaceKindAllowed(config, kind)) {
|
|
2987
|
+
skipped.push({
|
|
2988
|
+
namespace,
|
|
2989
|
+
kind,
|
|
2990
|
+
reason: disabledReasonForKind(kind)
|
|
2991
|
+
});
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
if (!isConfigured && !await catalogRootIsLive(config, record)) {
|
|
2995
|
+
skipped.push({
|
|
2996
|
+
namespace,
|
|
2997
|
+
kind,
|
|
2998
|
+
reason: "unsafe_or_stale_root"
|
|
2999
|
+
});
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
byNamespace.set(namespace, {
|
|
3003
|
+
namespace,
|
|
3004
|
+
kind,
|
|
3005
|
+
storageDir: record.storageDir,
|
|
3006
|
+
source: isConfigured ? "configured" : "catalog",
|
|
3007
|
+
lastWriteAt: record.lastWriteAt,
|
|
3008
|
+
lastMaintenanceAt: record.lastMaintenanceAt?.[options.jobName]
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
skipped.push({
|
|
3013
|
+
namespace: "*",
|
|
3014
|
+
reason: "catalog_read_failed",
|
|
3015
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
} else if (config.namespacesEnabled) {
|
|
3019
|
+
skipped.push({
|
|
3020
|
+
namespace: "*",
|
|
3021
|
+
reason: "fanout_disabled"
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
if (options.budgetMode !== "unbounded") {
|
|
3025
|
+
const latestStatusAtByNamespace = await readLatestStatusAtByNamespace(config, options.jobName);
|
|
3026
|
+
for (const candidate of byNamespace.values()) {
|
|
3027
|
+
if (!candidate.lastMaintenanceAt) {
|
|
3028
|
+
candidate.lastMaintenanceAt = latestStatusAtByNamespace.get(candidate.namespace);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const candidates = [...byNamespace.values()].filter((candidate) => namespaceKindAllowed(config, candidate.kind)).sort(sortCandidates);
|
|
3033
|
+
const max = maxNamespacesPerCycle(config);
|
|
3034
|
+
const applyCycleBudget = options.budgetMode !== "unbounded";
|
|
3035
|
+
const selected = applyCycleBudget ? candidates.slice(0, max) : candidates;
|
|
3036
|
+
if (applyCycleBudget) {
|
|
3037
|
+
for (const candidate of candidates.slice(max)) {
|
|
3038
|
+
skipped.push({
|
|
3039
|
+
namespace: candidate.namespace,
|
|
3040
|
+
kind: candidate.kind,
|
|
3041
|
+
reason: "budget_exhausted"
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
return {
|
|
3046
|
+
jobName: options.jobName,
|
|
3047
|
+
generatedAt,
|
|
3048
|
+
namespaces: selected,
|
|
3049
|
+
skipped,
|
|
3050
|
+
budget: {
|
|
3051
|
+
maxNamespacesPerCycle: max,
|
|
3052
|
+
selected: selected.length
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
function stablePathSegment(value) {
|
|
3057
|
+
const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 128) || "unnamed";
|
|
3058
|
+
if (sanitized.length <= 128 && sanitized === value) return sanitized;
|
|
3059
|
+
return `${sanitized.slice(0, 80)}-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
|
|
3060
|
+
}
|
|
3061
|
+
function namespacePathSegment(namespace) {
|
|
3062
|
+
const token = namespaceIdentityToken(namespace);
|
|
3063
|
+
if (token.length <= 160) return token;
|
|
3064
|
+
return `ns-${createHash("sha256").update(namespace).digest("hex")}`;
|
|
3065
|
+
}
|
|
3066
|
+
function lockPath(config, jobName, namespace) {
|
|
3067
|
+
return path3.join(
|
|
3068
|
+
config.memoryDir,
|
|
3069
|
+
"state",
|
|
3070
|
+
LOCK_BASE,
|
|
3071
|
+
stablePathSegment(jobName),
|
|
3072
|
+
`${namespacePathSegment(namespace)}.lock`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
function namespaceMaintenanceLockStaleMs(config) {
|
|
3076
|
+
if (typeof config.maintenanceNamespaceLockStaleMs === "number" && Number.isFinite(config.maintenanceNamespaceLockStaleMs) && config.maintenanceNamespaceLockStaleMs > 0) {
|
|
3077
|
+
return Math.floor(config.maintenanceNamespaceLockStaleMs);
|
|
3078
|
+
}
|
|
3079
|
+
return DEFAULT_LOCK_STALE_MS;
|
|
3080
|
+
}
|
|
3081
|
+
function namespaceMaintenanceLockHeartbeatMs(config) {
|
|
3082
|
+
const staleMs = namespaceMaintenanceLockStaleMs(config);
|
|
3083
|
+
return Math.max(1, Math.min(3e4, Math.floor(staleMs / 3) || 1));
|
|
3084
|
+
}
|
|
3085
|
+
function errorCode(error) {
|
|
3086
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
3087
|
+
}
|
|
3088
|
+
async function withNamespaceMaintenanceLockHeartbeat(config, locks, task) {
|
|
3089
|
+
const activeLocks = Array.isArray(locks) ? locks : [locks];
|
|
3090
|
+
const interval = setInterval(() => {
|
|
3091
|
+
for (const lock of activeLocks) {
|
|
3092
|
+
void lock.touch().catch(() => void 0);
|
|
3093
|
+
}
|
|
3094
|
+
}, namespaceMaintenanceLockHeartbeatMs(config));
|
|
3095
|
+
interval.unref?.();
|
|
3096
|
+
try {
|
|
3097
|
+
return await task();
|
|
3098
|
+
} finally {
|
|
3099
|
+
clearInterval(interval);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
async function removeStaleLockDirectory(filePath) {
|
|
3103
|
+
let entries;
|
|
3104
|
+
try {
|
|
3105
|
+
entries = await readdir2(filePath, { withFileTypes: true });
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
if (errorCode(error) === "ENOENT") return;
|
|
3108
|
+
throw error;
|
|
3109
|
+
}
|
|
3110
|
+
for (const entry of entries) {
|
|
3111
|
+
if (!entry.isFile()) continue;
|
|
3112
|
+
await namespaceMaintenanceFs.rm(path3.join(filePath, entry.name), { force: true });
|
|
3113
|
+
}
|
|
3114
|
+
try {
|
|
3115
|
+
await rmdir(filePath);
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
if (errorCode(error) === "ENOENT") return;
|
|
3118
|
+
throw error;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
async function tryAcquireNamespaceMaintenanceLock(config, jobName, namespace) {
|
|
3122
|
+
const filePath = lockPath(config, jobName, namespace);
|
|
3123
|
+
await mkdir3(path3.dirname(filePath), { recursive: true });
|
|
3124
|
+
try {
|
|
3125
|
+
const lockId = randomUUID2();
|
|
3126
|
+
await mkdir3(filePath);
|
|
3127
|
+
const ownerPath = path3.join(filePath, `${lockId}.json`);
|
|
3128
|
+
let handle;
|
|
3129
|
+
try {
|
|
3130
|
+
handle = await namespaceMaintenanceFs.open(ownerPath, "wx");
|
|
3131
|
+
await handle.writeFile(
|
|
3132
|
+
`${JSON.stringify({
|
|
3133
|
+
lockId,
|
|
3134
|
+
pid: process.pid,
|
|
3135
|
+
jobName,
|
|
3136
|
+
namespace,
|
|
3137
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3138
|
+
})}
|
|
3139
|
+
`,
|
|
3140
|
+
"utf8"
|
|
3141
|
+
);
|
|
3142
|
+
await handle.close();
|
|
3143
|
+
} catch (setupError) {
|
|
3144
|
+
await handle?.close().catch(() => void 0);
|
|
3145
|
+
await namespaceMaintenanceFs.rm(ownerPath, { force: true }).catch(() => void 0);
|
|
3146
|
+
await rmdir(filePath).catch(() => void 0);
|
|
3147
|
+
throw setupError;
|
|
3148
|
+
}
|
|
3149
|
+
return {
|
|
3150
|
+
path: filePath,
|
|
3151
|
+
async touch() {
|
|
3152
|
+
try {
|
|
3153
|
+
const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
|
|
3154
|
+
if (parsed.lockId === lockId) {
|
|
3155
|
+
const now = /* @__PURE__ */ new Date();
|
|
3156
|
+
await utimes2(ownerPath, now, now);
|
|
3157
|
+
await utimes2(filePath, now, now);
|
|
3158
|
+
}
|
|
3159
|
+
} catch {
|
|
3160
|
+
}
|
|
3161
|
+
},
|
|
3162
|
+
async release() {
|
|
3163
|
+
try {
|
|
3164
|
+
const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
|
|
3165
|
+
if (parsed.lockId === lockId) {
|
|
3166
|
+
await namespaceMaintenanceFs.rm(ownerPath, { force: true });
|
|
3167
|
+
await rmdir(filePath).catch(() => void 0);
|
|
3168
|
+
}
|
|
3169
|
+
} catch {
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
if (errorCode(error) === "EEXIST") {
|
|
3175
|
+
const staleMs = namespaceMaintenanceLockStaleMs(config);
|
|
3176
|
+
try {
|
|
3177
|
+
const s = await lstat2(filePath);
|
|
3178
|
+
if (s.isSymbolicLink()) {
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
if ((s.isFile() || s.isDirectory()) && Date.now() - s.mtimeMs > staleMs) {
|
|
3182
|
+
try {
|
|
3183
|
+
if (s.isDirectory()) {
|
|
3184
|
+
await removeStaleLockDirectory(filePath);
|
|
3185
|
+
} else {
|
|
3186
|
+
await namespaceMaintenanceFs.rm(filePath, { force: true });
|
|
3187
|
+
}
|
|
3188
|
+
} catch (removeError) {
|
|
3189
|
+
if (errorCode(removeError) === "ENOENT") {
|
|
3190
|
+
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
3191
|
+
}
|
|
3192
|
+
if (errorCode(removeError) === "ENOTEMPTY") {
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
throw removeError;
|
|
3196
|
+
}
|
|
3197
|
+
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
3198
|
+
}
|
|
3199
|
+
} catch (statError) {
|
|
3200
|
+
if (errorCode(statError) === "ENOENT") {
|
|
3201
|
+
return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
|
|
3202
|
+
}
|
|
3203
|
+
throw statError;
|
|
3204
|
+
}
|
|
3205
|
+
return null;
|
|
3206
|
+
}
|
|
3207
|
+
throw error;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
function statusBasePath(config) {
|
|
3211
|
+
return path3.join(config.memoryDir, "state", STATUS_BASE);
|
|
3212
|
+
}
|
|
3213
|
+
function statusPath(config, jobName, namespace) {
|
|
3214
|
+
return path3.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.json`);
|
|
3215
|
+
}
|
|
3216
|
+
function lastRanStatusPath(config, jobName, namespace) {
|
|
3217
|
+
return path3.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.last-ran.json`);
|
|
3218
|
+
}
|
|
3219
|
+
function parseStatus(value) {
|
|
3220
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3221
|
+
const v = value;
|
|
3222
|
+
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") {
|
|
3223
|
+
return v;
|
|
3224
|
+
}
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
async function readLatestStatusAtByNamespace(config, jobName) {
|
|
3228
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3229
|
+
const latestMs = /* @__PURE__ */ new Map();
|
|
3230
|
+
for (const status of [...await readStatusFiles(config), ...await readLastRanStatusFiles(config)]) {
|
|
3231
|
+
if (status.state !== "ran") continue;
|
|
3232
|
+
if (status.jobName !== jobName) continue;
|
|
3233
|
+
const completedAtMs = Date.parse(status.completedAt);
|
|
3234
|
+
if (!Number.isFinite(completedAtMs)) continue;
|
|
3235
|
+
const previousMs = latestMs.get(status.namespace);
|
|
3236
|
+
if (previousMs !== void 0 && previousMs >= completedAtMs) continue;
|
|
3237
|
+
latestMs.set(status.namespace, completedAtMs);
|
|
3238
|
+
latest.set(status.namespace, status.completedAt);
|
|
3239
|
+
}
|
|
3240
|
+
return latest;
|
|
3241
|
+
}
|
|
3242
|
+
async function readStatusFile(filePath) {
|
|
3243
|
+
try {
|
|
3244
|
+
const parsed = JSON.parse(await readFile3(filePath, "utf8"));
|
|
3245
|
+
return parseStatus(parsed);
|
|
3246
|
+
} catch {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async function readStatusFiles(config) {
|
|
3251
|
+
if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
|
|
3252
|
+
return [];
|
|
3253
|
+
}
|
|
3254
|
+
const root = statusBasePath(config);
|
|
3255
|
+
const statuses = [];
|
|
3256
|
+
let jobDirs;
|
|
3257
|
+
try {
|
|
3258
|
+
jobDirs = await readdir2(root, { withFileTypes: true });
|
|
3259
|
+
} catch {
|
|
3260
|
+
return statuses;
|
|
3261
|
+
}
|
|
3262
|
+
for (const jobDir of jobDirs) {
|
|
3263
|
+
if (!jobDir.isDirectory()) continue;
|
|
3264
|
+
let files;
|
|
3265
|
+
try {
|
|
3266
|
+
files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
|
|
3267
|
+
} catch {
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
for (const file of files) {
|
|
3271
|
+
if (!file.isFile() || !file.name.endsWith(".json")) continue;
|
|
3272
|
+
if (file.name.endsWith(".last-ran.json")) continue;
|
|
3273
|
+
const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
|
|
3274
|
+
if (status) statuses.push(status);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
return statuses;
|
|
3278
|
+
}
|
|
3279
|
+
async function readLastRanStatusFiles(config) {
|
|
3280
|
+
if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
|
|
3281
|
+
return [];
|
|
3282
|
+
}
|
|
3283
|
+
const root = statusBasePath(config);
|
|
3284
|
+
const statuses = [];
|
|
3285
|
+
let jobDirs;
|
|
3286
|
+
try {
|
|
3287
|
+
jobDirs = await readdir2(root, { withFileTypes: true });
|
|
3288
|
+
} catch {
|
|
3289
|
+
return statuses;
|
|
3290
|
+
}
|
|
3291
|
+
for (const jobDir of jobDirs) {
|
|
3292
|
+
if (!jobDir.isDirectory()) continue;
|
|
3293
|
+
let files;
|
|
3294
|
+
try {
|
|
3295
|
+
files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
|
|
3296
|
+
} catch {
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
for (const file of files) {
|
|
3300
|
+
if (!file.isFile() || !file.name.endsWith(".last-ran.json")) continue;
|
|
3301
|
+
const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
|
|
3302
|
+
if (status) statuses.push(status);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
return statuses;
|
|
3306
|
+
}
|
|
3307
|
+
async function writeStatusPayload(target, status) {
|
|
3308
|
+
const dir = path3.dirname(target);
|
|
3309
|
+
await mkdir3(dir, { recursive: true });
|
|
3310
|
+
const temp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3311
|
+
const payload = {
|
|
3312
|
+
version: 1,
|
|
3313
|
+
...status
|
|
3314
|
+
};
|
|
3315
|
+
await writeFile3(temp, `${JSON.stringify(payload, null, 2)}
|
|
3316
|
+
`, "utf8");
|
|
3317
|
+
await rename2(temp, target);
|
|
3318
|
+
}
|
|
3319
|
+
async function writeStatusFile(config, status) {
|
|
3320
|
+
await writeStatusPayload(statusPath(config, status.jobName, status.namespace), status);
|
|
3321
|
+
if (status.state === "ran") {
|
|
3322
|
+
await writeStatusPayload(lastRanStatusPath(config, status.jobName, status.namespace), status);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
async function recordNamespaceMaintenanceStatusSafely(config, status) {
|
|
3326
|
+
try {
|
|
3327
|
+
await writeStatusFile(config, status);
|
|
3328
|
+
} catch {
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
function maintenanceErrorDetail(error) {
|
|
3332
|
+
return displayErrorDetail(error) || "Error";
|
|
3333
|
+
}
|
|
3334
|
+
async function runNamespaceMaintenanceBatchPlan(config, plan, runner, catalog, options = {}) {
|
|
3335
|
+
const statuses = [];
|
|
3336
|
+
for (const skipped of plan.skipped) {
|
|
3337
|
+
if (skipped.namespace === "*") continue;
|
|
3338
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3339
|
+
const status = {
|
|
3340
|
+
namespace: skipped.namespace,
|
|
3341
|
+
jobName: plan.jobName,
|
|
3342
|
+
state: "skipped",
|
|
3343
|
+
reason: skipped.reason,
|
|
3344
|
+
startedAt: now,
|
|
3345
|
+
completedAt: now
|
|
3346
|
+
};
|
|
3347
|
+
statuses.push(status);
|
|
3348
|
+
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3349
|
+
}
|
|
3350
|
+
const acquired = [];
|
|
3351
|
+
try {
|
|
3352
|
+
for (const candidate of plan.namespaces) {
|
|
3353
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3354
|
+
const lock = await tryAcquireNamespaceMaintenanceLock(config, plan.jobName, candidate.namespace);
|
|
3355
|
+
if (!lock) {
|
|
3356
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3357
|
+
const status = {
|
|
3358
|
+
namespace: candidate.namespace,
|
|
3359
|
+
jobName: plan.jobName,
|
|
3360
|
+
state: "skipped",
|
|
3361
|
+
reason: "lock_held",
|
|
3362
|
+
startedAt,
|
|
3363
|
+
completedAt
|
|
3364
|
+
};
|
|
3365
|
+
statuses.push(status);
|
|
3366
|
+
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3367
|
+
continue;
|
|
3368
|
+
}
|
|
3369
|
+
acquired.push({ candidate, lock, startedAt });
|
|
3370
|
+
}
|
|
3371
|
+
} catch (error) {
|
|
3372
|
+
await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
|
|
3373
|
+
throw error;
|
|
3374
|
+
}
|
|
3375
|
+
if (options.requireAllLocks && acquired.length > 0 && acquired.length < plan.namespaces.length) {
|
|
3376
|
+
for (const { candidate, startedAt } of acquired) {
|
|
3377
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3378
|
+
const status = {
|
|
3379
|
+
namespace: candidate.namespace,
|
|
3380
|
+
jobName: plan.jobName,
|
|
3381
|
+
state: "skipped",
|
|
3382
|
+
reason: "batch_lock_incomplete",
|
|
3383
|
+
startedAt,
|
|
3384
|
+
completedAt
|
|
3385
|
+
};
|
|
3386
|
+
statuses.push(status);
|
|
3387
|
+
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
1473
3388
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
3389
|
+
await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
|
|
3390
|
+
return {
|
|
3391
|
+
jobName: plan.jobName,
|
|
3392
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3393
|
+
ran: statuses.filter((s) => s.state === "ran").length,
|
|
3394
|
+
skipped: statuses.filter((s) => s.state === "skipped").length,
|
|
3395
|
+
failed: statuses.filter((s) => s.state === "failed").length,
|
|
3396
|
+
statuses
|
|
3397
|
+
};
|
|
1477
3398
|
}
|
|
1478
|
-
|
|
1479
|
-
async listCorrections() {
|
|
1480
|
-
const storage = await this.deps.getStorage();
|
|
3399
|
+
if (acquired.length === 0) {
|
|
1481
3400
|
return {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
3401
|
+
jobName: plan.jobName,
|
|
3402
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3403
|
+
ran: statuses.filter((s) => s.state === "ran").length,
|
|
3404
|
+
skipped: statuses.filter((s) => s.state === "skipped").length,
|
|
3405
|
+
failed: statuses.filter((s) => s.state === "failed").length,
|
|
3406
|
+
statuses
|
|
1485
3407
|
};
|
|
1486
3408
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
3409
|
+
try {
|
|
3410
|
+
const result = await withNamespaceMaintenanceLockHeartbeat(
|
|
3411
|
+
config,
|
|
3412
|
+
acquired.map(({ lock }) => lock),
|
|
3413
|
+
() => runner(acquired.map(({ candidate }) => candidate))
|
|
1493
3414
|
);
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
3415
|
+
for (const { candidate, startedAt } of acquired) {
|
|
3416
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3417
|
+
const status = {
|
|
3418
|
+
namespace: candidate.namespace,
|
|
3419
|
+
jobName: plan.jobName,
|
|
3420
|
+
state: "ran",
|
|
3421
|
+
startedAt,
|
|
3422
|
+
completedAt,
|
|
3423
|
+
itemCount: itemCountForNamespace(result, candidate.namespace)
|
|
3424
|
+
};
|
|
3425
|
+
statuses.push(status);
|
|
3426
|
+
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
3427
|
+
try {
|
|
3428
|
+
await catalog?.markMaintenance(candidate.namespace, plan.jobName, new Date(completedAt));
|
|
3429
|
+
} catch {
|
|
3430
|
+
}
|
|
1505
3431
|
}
|
|
1506
|
-
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
const skipReason = options.skipReasonForError?.(error);
|
|
3434
|
+
for (const { candidate, startedAt } of acquired) {
|
|
3435
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3436
|
+
const status = skipReason ? {
|
|
3437
|
+
namespace: candidate.namespace,
|
|
3438
|
+
jobName: plan.jobName,
|
|
3439
|
+
state: "skipped",
|
|
3440
|
+
reason: skipReason,
|
|
3441
|
+
startedAt,
|
|
3442
|
+
completedAt
|
|
3443
|
+
} : {
|
|
3444
|
+
namespace: candidate.namespace,
|
|
3445
|
+
jobName: plan.jobName,
|
|
3446
|
+
state: "failed",
|
|
3447
|
+
reason: "job_failed",
|
|
3448
|
+
startedAt,
|
|
3449
|
+
completedAt,
|
|
3450
|
+
error: maintenanceErrorDetail(error)
|
|
3451
|
+
};
|
|
3452
|
+
statuses.push(status);
|
|
3453
|
+
await recordNamespaceMaintenanceStatusSafely(config, status);
|
|
1512
3454
|
}
|
|
1513
|
-
|
|
1514
|
-
await
|
|
1515
|
-
return removed;
|
|
1516
|
-
}
|
|
1517
|
-
};
|
|
1518
|
-
function clampLimit(value, fallback, max, label) {
|
|
1519
|
-
if (value === void 0) return fallback;
|
|
1520
|
-
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
|
|
1521
|
-
throw new WearablesInputError(
|
|
1522
|
-
`invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
|
|
1523
|
-
);
|
|
3455
|
+
} finally {
|
|
3456
|
+
await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
|
|
1524
3457
|
}
|
|
1525
|
-
return
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (!isValidTranscriptDate(match[2])) return null;
|
|
1534
|
-
return { source: match[1], date: match[2] };
|
|
3458
|
+
return {
|
|
3459
|
+
jobName: plan.jobName,
|
|
3460
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3461
|
+
ran: statuses.filter((s) => s.state === "ran").length,
|
|
3462
|
+
skipped: statuses.filter((s) => s.state === "skipped").length,
|
|
3463
|
+
failed: statuses.filter((s) => s.state === "failed").length,
|
|
3464
|
+
statuses
|
|
3465
|
+
};
|
|
1535
3466
|
}
|
|
1536
|
-
function
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
3467
|
+
function itemCountForNamespace(result, namespace) {
|
|
3468
|
+
const itemCounts = result?.itemCounts;
|
|
3469
|
+
if (itemCounts instanceof Map) return itemCounts.get(namespace);
|
|
3470
|
+
if (itemCounts && Object.prototype.hasOwnProperty.call(itemCounts, namespace)) {
|
|
3471
|
+
return itemCounts[namespace];
|
|
3472
|
+
}
|
|
3473
|
+
return result?.itemCount;
|
|
1542
3474
|
}
|
|
1543
3475
|
|
|
1544
3476
|
// src/orchestrator.ts
|
|
@@ -1602,7 +3534,7 @@ function flattenStructuredSectionEvidence(sections) {
|
|
|
1602
3534
|
);
|
|
1603
3535
|
}
|
|
1604
3536
|
function fingerprintEntitySynthesisEvidence(entity) {
|
|
1605
|
-
const fingerprint =
|
|
3537
|
+
const fingerprint = createHash2("sha256");
|
|
1606
3538
|
const timelineEntries = entity.timeline.map((entry) => [
|
|
1607
3539
|
entry.timestamp,
|
|
1608
3540
|
entry.source ?? "",
|
|
@@ -1652,7 +3584,7 @@ async function raceRecallAbort(promise, signal, message = "recall aborted") {
|
|
|
1652
3584
|
}
|
|
1653
3585
|
}
|
|
1654
3586
|
function qmdCollectionPathParts(resultPath) {
|
|
1655
|
-
if (!resultPath ||
|
|
3587
|
+
if (!resultPath || path4.isAbsolute(resultPath)) return null;
|
|
1656
3588
|
const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1657
3589
|
const slashIndex = normalized.indexOf("/");
|
|
1658
3590
|
if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
|
|
@@ -1665,9 +3597,9 @@ function qmdCollectionPathParts(resultPath) {
|
|
|
1665
3597
|
}
|
|
1666
3598
|
function qmdResultPathCandidates(storageDir, resultPath) {
|
|
1667
3599
|
const candidates = /* @__PURE__ */ new Set();
|
|
1668
|
-
const storageRoot =
|
|
3600
|
+
const storageRoot = path4.resolve(storageDir);
|
|
1669
3601
|
const addCandidate = (candidate) => {
|
|
1670
|
-
const resolved =
|
|
3602
|
+
const resolved = path4.resolve(candidate);
|
|
1671
3603
|
if (isPathInsideStorageRoot(storageRoot, resolved)) {
|
|
1672
3604
|
candidates.add(resolved);
|
|
1673
3605
|
}
|
|
@@ -1675,12 +3607,12 @@ function qmdResultPathCandidates(storageDir, resultPath) {
|
|
|
1675
3607
|
const addRelativeCandidates = (relativePath) => {
|
|
1676
3608
|
const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1677
3609
|
if (!normalized) return;
|
|
1678
|
-
addCandidate(
|
|
3610
|
+
addCandidate(path4.join(storageRoot, normalized));
|
|
1679
3611
|
if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
|
|
1680
|
-
addCandidate(
|
|
3612
|
+
addCandidate(path4.join(storageRoot, "facts", normalized));
|
|
1681
3613
|
}
|
|
1682
3614
|
};
|
|
1683
|
-
if (
|
|
3615
|
+
if (path4.isAbsolute(resultPath)) {
|
|
1684
3616
|
addCandidate(resultPath);
|
|
1685
3617
|
} else {
|
|
1686
3618
|
addRelativeCandidates(resultPath);
|
|
@@ -1797,11 +3729,11 @@ async function qmdStartupCollectionCheckWithTimeout(promise, controller, label)
|
|
|
1797
3729
|
return await Promise.race([checkedPromise, timeoutPromise]);
|
|
1798
3730
|
}
|
|
1799
3731
|
function defaultWorkspaceDir() {
|
|
1800
|
-
return
|
|
3732
|
+
return path4.join(os.homedir(), ".openclaw", "workspace");
|
|
1801
3733
|
}
|
|
1802
3734
|
function sanitizeSessionKeyForFilename(sessionKey) {
|
|
1803
3735
|
const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1804
|
-
const hash =
|
|
3736
|
+
const hash = createHash2("sha256").update(sessionKey).digest("hex").slice(0, 12);
|
|
1805
3737
|
return `${readable}-${hash}`;
|
|
1806
3738
|
}
|
|
1807
3739
|
function latestSourceValidAtFromTurns(turns) {
|
|
@@ -2102,11 +4034,11 @@ function mergeGraphExpandedResults(primary, expanded) {
|
|
|
2102
4034
|
return Array.from(mergedByPath.values());
|
|
2103
4035
|
}
|
|
2104
4036
|
function graphPathRelativeToStorage(storageDir, candidatePath) {
|
|
2105
|
-
const absolutePath =
|
|
2106
|
-
const rel =
|
|
4037
|
+
const absolutePath = path4.isAbsolute(candidatePath) ? candidatePath : path4.resolve(storageDir, candidatePath);
|
|
4038
|
+
const rel = path4.relative(storageDir, absolutePath);
|
|
2107
4039
|
if (!rel || rel === ".") return null;
|
|
2108
4040
|
if (rel.startsWith("..")) return null;
|
|
2109
|
-
return rel.split(
|
|
4041
|
+
return rel.split(path4.sep).join("/");
|
|
2110
4042
|
}
|
|
2111
4043
|
function normalizeGraphActivationScore(score) {
|
|
2112
4044
|
const bounded = Number.isFinite(score) && score > 0 ? score : 0;
|
|
@@ -2248,7 +4180,7 @@ function buildMemoryPathById(allMemsForGraph, storageDir) {
|
|
|
2248
4180
|
for (const mem of allMemsForGraph ?? []) {
|
|
2249
4181
|
const id = mem.frontmatter.id;
|
|
2250
4182
|
if (!id) continue;
|
|
2251
|
-
pathById.set(id,
|
|
4183
|
+
pathById.set(id, path4.relative(storageDir, mem.path));
|
|
2252
4184
|
}
|
|
2253
4185
|
return pathById;
|
|
2254
4186
|
}
|
|
@@ -2256,7 +4188,7 @@ function appendMemoryToGraphContext(options) {
|
|
|
2256
4188
|
if (!Array.isArray(options.allMemsForGraph)) return;
|
|
2257
4189
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2258
4190
|
options.allMemsForGraph.push({
|
|
2259
|
-
path:
|
|
4191
|
+
path: path4.join(options.storageDir, options.memoryRelPath),
|
|
2260
4192
|
content: options.content,
|
|
2261
4193
|
frontmatter: {
|
|
2262
4194
|
id: options.memoryId,
|
|
@@ -2276,20 +4208,28 @@ function resolvePersistedMemoryRelativePath(options) {
|
|
|
2276
4208
|
const persisted = options.pathById.get(options.memoryId);
|
|
2277
4209
|
if (persisted) return persisted;
|
|
2278
4210
|
if (options.category === "correction") {
|
|
2279
|
-
return
|
|
4211
|
+
return path4.join("corrections", `${options.memoryId}.md`);
|
|
2280
4212
|
}
|
|
2281
4213
|
const subtree = options.category === "procedure" ? "procedures" : options.category === "reasoning_trace" ? "reasoning-traces" : "facts";
|
|
2282
4214
|
const idParts = options.memoryId.split("-");
|
|
2283
4215
|
const maybeTimestamp = Number(idParts[1]);
|
|
2284
4216
|
if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
|
|
2285
4217
|
const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
|
|
2286
|
-
return
|
|
4218
|
+
return path4.join(subtree, day, `${options.memoryId}.md`);
|
|
2287
4219
|
}
|
|
2288
|
-
return
|
|
4220
|
+
return path4.join(subtree, `${options.memoryId}.md`);
|
|
4221
|
+
}
|
|
4222
|
+
function qmdMaintenanceSkipReasonForError(error) {
|
|
4223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4224
|
+
return /^QMD (?:update|embed) skipped by .*min-interval gate$/.test(message) ? "throttled" : null;
|
|
2289
4225
|
}
|
|
2290
4226
|
var Orchestrator = class _Orchestrator {
|
|
2291
4227
|
storage;
|
|
2292
4228
|
storageRouter;
|
|
4229
|
+
/** Rebuildable namespace catalog (issue #1499). Inert unless namespaces enabled. */
|
|
4230
|
+
namespaceCatalog;
|
|
4231
|
+
namespaceStorageDirHints = /* @__PURE__ */ new Map();
|
|
4232
|
+
namespaceStorageDirHintsLoaded = false;
|
|
2293
4233
|
namespaceSearchRouter;
|
|
2294
4234
|
qmd;
|
|
2295
4235
|
conversationQmd;
|
|
@@ -2409,6 +4349,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2409
4349
|
qmdMaintenancePending = false;
|
|
2410
4350
|
qmdMaintenanceInFlight = false;
|
|
2411
4351
|
lastQmdEmbedAtMs = 0;
|
|
4352
|
+
lastQmdEmbedAtMsByNamespace = /* @__PURE__ */ new Map();
|
|
2412
4353
|
lastQmdReprobeAtMs = 0;
|
|
2413
4354
|
tierMigrationInFlight = false;
|
|
2414
4355
|
lastTierMigrationRunAtMs = 0;
|
|
@@ -2703,6 +4644,142 @@ var Orchestrator = class _Orchestrator {
|
|
|
2703
4644
|
)
|
|
2704
4645
|
);
|
|
2705
4646
|
}
|
|
4647
|
+
rememberNamespaceStorageDirHint(namespace, storageDir) {
|
|
4648
|
+
if (!this.config.namespacesEnabled || !storageDir) return;
|
|
4649
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
4650
|
+
if (!ns) return;
|
|
4651
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4652
|
+
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
|
|
4653
|
+
if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
|
|
4654
|
+
const resolvedStorageDir = path4.resolve(storageDir);
|
|
4655
|
+
let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
4656
|
+
if (!hints) {
|
|
4657
|
+
hints = /* @__PURE__ */ new Set();
|
|
4658
|
+
this.namespaceStorageDirHints.set(resolvedStorageDir, hints);
|
|
4659
|
+
}
|
|
4660
|
+
hints.add(ns);
|
|
4661
|
+
}
|
|
4662
|
+
storageDirMatchesNamespaceHint(namespace, storageDir) {
|
|
4663
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
4664
|
+
if (!ns) return false;
|
|
4665
|
+
const resolvedStorageDir = path4.resolve(storageDir);
|
|
4666
|
+
const resolvedMemoryDir = path4.resolve(this.config.memoryDir);
|
|
4667
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4668
|
+
if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
|
|
4669
|
+
const resolvedNamespacesDir = path4.join(resolvedMemoryDir, "namespaces");
|
|
4670
|
+
if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
|
|
4671
|
+
const rawRoot = path4.resolve(resolvedNamespacesDir, ns);
|
|
4672
|
+
const tokenRoot = path4.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
|
|
4673
|
+
return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
|
|
4674
|
+
}
|
|
4675
|
+
namespaceStorageDirHintOwnershipRank(record, resolvedStorageDir, configured) {
|
|
4676
|
+
if (resolvedStorageDir === path4.resolve(this.config.memoryDir)) {
|
|
4677
|
+
return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace) ? 0 : 3;
|
|
4678
|
+
}
|
|
4679
|
+
const leaf = path4.basename(resolvedStorageDir);
|
|
4680
|
+
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
4681
|
+
if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
|
|
4682
|
+
if (record.namespace === leaf) return 1;
|
|
4683
|
+
if (tokenOwnsRoot) return 2;
|
|
4684
|
+
return 3;
|
|
4685
|
+
}
|
|
4686
|
+
preferNamespaceStorageDirHintOwner(current, candidate, resolvedStorageDir, configured) {
|
|
4687
|
+
const currentRank = this.namespaceStorageDirHintOwnershipRank(
|
|
4688
|
+
current,
|
|
4689
|
+
resolvedStorageDir,
|
|
4690
|
+
configured
|
|
4691
|
+
);
|
|
4692
|
+
const candidateRank = this.namespaceStorageDirHintOwnershipRank(
|
|
4693
|
+
candidate,
|
|
4694
|
+
resolvedStorageDir,
|
|
4695
|
+
configured
|
|
4696
|
+
);
|
|
4697
|
+
if (candidateRank < currentRank) return candidate;
|
|
4698
|
+
if (candidateRank > currentRank) return current;
|
|
4699
|
+
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
4700
|
+
if (byName < 0) return candidate;
|
|
4701
|
+
if (byName > 0) return current;
|
|
4702
|
+
return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
|
|
4703
|
+
}
|
|
4704
|
+
loadNamespaceStorageDirHintsFromCatalog() {
|
|
4705
|
+
if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
|
|
4706
|
+
this.namespaceStorageDirHintsLoaded = true;
|
|
4707
|
+
const catalogPath = path4.join(this.config.memoryDir, "state", "namespaces.jsonl");
|
|
4708
|
+
if (!existsSync(catalogPath)) return;
|
|
4709
|
+
let body;
|
|
4710
|
+
try {
|
|
4711
|
+
body = readFileSync(catalogPath, "utf8");
|
|
4712
|
+
} catch {
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
const compactedByNamespace = /* @__PURE__ */ new Map();
|
|
4716
|
+
for (const line of body.split(/\r?\n/)) {
|
|
4717
|
+
const trimmed = line.trim();
|
|
4718
|
+
if (!trimmed) continue;
|
|
4719
|
+
try {
|
|
4720
|
+
const parsed = JSON.parse(trimmed);
|
|
4721
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
4722
|
+
const record = parsed;
|
|
4723
|
+
if (typeof record.namespace !== "string" || typeof record.storageDir !== "string" || typeof record.identityToken !== "string") {
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
const namespace = normalizeNamespaceIdentity(record.namespace);
|
|
4727
|
+
if (!namespace || record.identityToken !== namespaceIdentityToken(namespace)) continue;
|
|
4728
|
+
compactedByNamespace.set(namespace, {
|
|
4729
|
+
namespace,
|
|
4730
|
+
identityToken: record.identityToken,
|
|
4731
|
+
storageDir: record.storageDir
|
|
4732
|
+
});
|
|
4733
|
+
} catch {
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
const configured = new Set(
|
|
4737
|
+
this.configuredNamespaces().map((namespace) => normalizeNamespaceIdentity(namespace))
|
|
4738
|
+
);
|
|
4739
|
+
const preferredByStorageDir = /* @__PURE__ */ new Map();
|
|
4740
|
+
for (const record of compactedByNamespace.values()) {
|
|
4741
|
+
if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
|
|
4742
|
+
continue;
|
|
4743
|
+
}
|
|
4744
|
+
const resolvedStorageDir = path4.resolve(record.storageDir);
|
|
4745
|
+
const current = preferredByStorageDir.get(resolvedStorageDir);
|
|
4746
|
+
preferredByStorageDir.set(
|
|
4747
|
+
resolvedStorageDir,
|
|
4748
|
+
current ? this.preferNamespaceStorageDirHintOwner(
|
|
4749
|
+
current,
|
|
4750
|
+
record,
|
|
4751
|
+
resolvedStorageDir,
|
|
4752
|
+
configured
|
|
4753
|
+
) : record
|
|
4754
|
+
);
|
|
4755
|
+
}
|
|
4756
|
+
for (const record of preferredByStorageDir.values()) {
|
|
4757
|
+
this.rememberNamespaceStorageDirHint(record.namespace, record.storageDir);
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
/**
|
|
4761
|
+
* Shared namespace maintenance planner (issue #1500). This extends the
|
|
4762
|
+
* #1499 catalog-union QMD helper into a reusable contract: configured
|
|
4763
|
+
* namespaces are always considered, dynamic catalog namespaces are admitted
|
|
4764
|
+
* only when their live router root still matches real memory data, and branch
|
|
4765
|
+
* namespaces are opt-in. Recurring jobs use the per-cycle budget; startup and
|
|
4766
|
+
* recovery discovery paths use the same safety filters without that cycle
|
|
4767
|
+
* budget so every live namespace is ensured/synced.
|
|
4768
|
+
*/
|
|
4769
|
+
async namespaceMaintenancePlan(jobName) {
|
|
4770
|
+
return planNamespaceMaintenance(this.config, {
|
|
4771
|
+
jobName,
|
|
4772
|
+
catalog: this.namespaceCatalog
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
async maintenanceNamespaces(jobName = "qmd", budgetMode = "unbounded") {
|
|
4776
|
+
const plan = await planNamespaceMaintenance(this.config, {
|
|
4777
|
+
jobName,
|
|
4778
|
+
catalog: this.namespaceCatalog,
|
|
4779
|
+
budgetMode
|
|
4780
|
+
});
|
|
4781
|
+
return plan.namespaces.map((candidate) => candidate.namespace);
|
|
4782
|
+
}
|
|
2706
4783
|
buildConfiguredQmdSearchOptions(queryText) {
|
|
2707
4784
|
const intentHint = this.config.qmdIntentHintsEnabled ? buildQmdIntentHint(inferIntentFromText(queryText)) : void 0;
|
|
2708
4785
|
const explain = this.config.qmdExplainEnabled === true;
|
|
@@ -2777,10 +4854,20 @@ var Orchestrator = class _Orchestrator {
|
|
|
2777
4854
|
this.config = config;
|
|
2778
4855
|
this.profiler = new ProfilingCollector({
|
|
2779
4856
|
enabled: config.profilingEnabled,
|
|
2780
|
-
storageDir: config.profilingStorageDir ||
|
|
4857
|
+
storageDir: config.profilingStorageDir || path4.join(config.memoryDir, "profiling"),
|
|
2781
4858
|
maxTraces: config.profilingMaxTraces
|
|
2782
4859
|
});
|
|
2783
|
-
this.
|
|
4860
|
+
this.namespaceCatalog = new NamespaceCatalog(config);
|
|
4861
|
+
this.storageRouter = new NamespaceStorageRouter(config, {
|
|
4862
|
+
// Return the registration promise (round 6, codex P2 — NEFoX) so the
|
|
4863
|
+
// router's resolve-hook dedup only marks a namespace notified when the
|
|
4864
|
+
// catalog actually APPENDED. A dropped append (rebuild-lock timeout) or a
|
|
4865
|
+
// failure resolves to `false`/rejects, so the next `storageFor` retries.
|
|
4866
|
+
onResolve: (namespace, storageDir) => {
|
|
4867
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
4868
|
+
return this.namespaceCatalog.registerResolved(namespace, storageDir);
|
|
4869
|
+
}
|
|
4870
|
+
});
|
|
2784
4871
|
this.namespaceSearchRouter = new NamespaceSearchRouter(
|
|
2785
4872
|
config,
|
|
2786
4873
|
this.storageRouter
|
|
@@ -2812,7 +4899,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2812
4899
|
this.compounding = config.compoundingEnabled ? new CompoundingEngine(config, this.storage) : void 0;
|
|
2813
4900
|
this.buffer = new SmartBuffer(config, this.storage);
|
|
2814
4901
|
this.transcript = new TranscriptManager(config);
|
|
2815
|
-
this.conversationIndexDir =
|
|
4902
|
+
this.conversationIndexDir = path4.join(
|
|
2816
4903
|
config.memoryDir,
|
|
2817
4904
|
"conversation-index",
|
|
2818
4905
|
"chunks"
|
|
@@ -2869,7 +4956,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2869
4956
|
this.modelRegistry
|
|
2870
4957
|
);
|
|
2871
4958
|
this.threading = new ThreadingManager(
|
|
2872
|
-
|
|
4959
|
+
path4.join(config.memoryDir, "threads"),
|
|
2873
4960
|
config.threadingGapMinutes
|
|
2874
4961
|
);
|
|
2875
4962
|
this.tmtBuilder = new TmtBuilder(config.memoryDir, {
|
|
@@ -2948,7 +5035,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2948
5035
|
utilityPromoteThresholdDelta: this.utilityRuntimeValues?.promoteThresholdDelta ?? 0,
|
|
2949
5036
|
utilityDemoteThresholdDelta: this.utilityRuntimeValues?.demoteThresholdDelta ?? 0
|
|
2950
5037
|
};
|
|
2951
|
-
return
|
|
5038
|
+
return createHash2("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 12);
|
|
2952
5039
|
}
|
|
2953
5040
|
effectiveLifecycleThresholds() {
|
|
2954
5041
|
const archiveDecayThreshold = this.config.lifecycleArchiveDecayThreshold;
|
|
@@ -3129,6 +5216,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3129
5216
|
await sm.ensureDirectories();
|
|
3130
5217
|
await sm.loadAliases().catch(() => void 0);
|
|
3131
5218
|
}
|
|
5219
|
+
await this.namespaceCatalog.registerConfiguredNamespaces().catch(() => void 0);
|
|
3132
5220
|
}
|
|
3133
5221
|
await this.relevance.load();
|
|
3134
5222
|
await this.negatives.load();
|
|
@@ -3167,13 +5255,13 @@ var Orchestrator = class _Orchestrator {
|
|
|
3167
5255
|
if (this.config.compactionResetEnabled) {
|
|
3168
5256
|
try {
|
|
3169
5257
|
const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
|
|
3170
|
-
const files = await
|
|
5258
|
+
const files = await readdir3(wsDir).catch(() => []);
|
|
3171
5259
|
for (const f of files) {
|
|
3172
5260
|
if (!f.startsWith(".compaction-reset-signal-")) continue;
|
|
3173
|
-
const fp =
|
|
3174
|
-
const s = await
|
|
5261
|
+
const fp = path4.join(wsDir, f);
|
|
5262
|
+
const s = await stat3(fp).catch(() => null);
|
|
3175
5263
|
if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
|
|
3176
|
-
await
|
|
5264
|
+
await unlink2(fp).catch(() => {
|
|
3177
5265
|
});
|
|
3178
5266
|
log.debug(`initialize: removed stale compaction signal ${f}`);
|
|
3179
5267
|
}
|
|
@@ -3186,7 +5274,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3186
5274
|
const available = await this.qmd.probe();
|
|
3187
5275
|
if (available) {
|
|
3188
5276
|
log.info(`Search backend: available ${this.qmd.debugStatus()}`);
|
|
3189
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
5277
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
3190
5278
|
const states = await Promise.all(
|
|
3191
5279
|
namespaces.map(async (namespace) => {
|
|
3192
5280
|
const collectionCheckAbort = new AbortController();
|
|
@@ -3270,7 +5358,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3270
5358
|
log.info("QMD startup sync: updating index to match current disk state");
|
|
3271
5359
|
if (this.config.namespacesEnabled) {
|
|
3272
5360
|
await this.namespaceSearchRouter.updateNamespaces(
|
|
3273
|
-
this.
|
|
5361
|
+
await this.maintenanceNamespaces(),
|
|
3274
5362
|
{ signal }
|
|
3275
5363
|
);
|
|
3276
5364
|
} else {
|
|
@@ -3449,9 +5537,9 @@ var Orchestrator = class _Orchestrator {
|
|
|
3449
5537
|
`wearables auto-sync started: every ${this.config.wearables.autoSyncIntervalMinutes}m over ${this.config.wearables.autoSyncDays}d (deep ${this.config.wearables.autoSyncDeepDays}d daily)`
|
|
3450
5538
|
);
|
|
3451
5539
|
} catch (err) {
|
|
3452
|
-
const { displayErrorDetail } = await import("./runtime/better-sqlite.js");
|
|
5540
|
+
const { displayErrorDetail: displayErrorDetail2 } = await import("./runtime/better-sqlite.js");
|
|
3453
5541
|
log.warn(
|
|
3454
|
-
`wearables auto-sync failed to start (non-fatal): ${
|
|
5542
|
+
`wearables auto-sync failed to start (non-fatal): ${displayErrorDetail2(err)}`
|
|
3455
5543
|
);
|
|
3456
5544
|
}
|
|
3457
5545
|
}
|
|
@@ -3482,7 +5570,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3482
5570
|
if (this.config.namespacesEnabled) {
|
|
3483
5571
|
this.namespaceSearchRouter.clearCache();
|
|
3484
5572
|
}
|
|
3485
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
5573
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
3486
5574
|
const states = await Promise.all(
|
|
3487
5575
|
namespaces.map(async (namespace) => ({
|
|
3488
5576
|
namespace,
|
|
@@ -3564,7 +5652,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3564
5652
|
*/
|
|
3565
5653
|
async autoRegisterDaySummaryCron() {
|
|
3566
5654
|
const home = resolveHomeDir();
|
|
3567
|
-
const jobsPath =
|
|
5655
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3568
5656
|
try {
|
|
3569
5657
|
if (!existsSync(jobsPath)) {
|
|
3570
5658
|
log.debug(
|
|
@@ -3617,7 +5705,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3617
5705
|
}
|
|
3618
5706
|
async autoRegisterNightlyGovernanceCron() {
|
|
3619
5707
|
const home = resolveHomeDir();
|
|
3620
|
-
const jobsPath =
|
|
5708
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3621
5709
|
try {
|
|
3622
5710
|
if (!existsSync(jobsPath)) {
|
|
3623
5711
|
log.debug("nightly governance cron: jobs.json not found, skipping auto-register");
|
|
@@ -3639,7 +5727,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3639
5727
|
}
|
|
3640
5728
|
async autoRegisterProceduralMiningCron() {
|
|
3641
5729
|
const home = resolveHomeDir();
|
|
3642
|
-
const jobsPath =
|
|
5730
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3643
5731
|
try {
|
|
3644
5732
|
if (!existsSync(jobsPath)) {
|
|
3645
5733
|
log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
|
|
@@ -3659,7 +5747,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3659
5747
|
}
|
|
3660
5748
|
async autoRegisterContradictionScanCron() {
|
|
3661
5749
|
const home = resolveHomeDir();
|
|
3662
|
-
const jobsPath =
|
|
5750
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3663
5751
|
try {
|
|
3664
5752
|
if (!existsSync(jobsPath)) {
|
|
3665
5753
|
log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
|
|
@@ -3679,7 +5767,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3679
5767
|
}
|
|
3680
5768
|
async autoRegisterPatternReinforcementCron() {
|
|
3681
5769
|
const home = resolveHomeDir();
|
|
3682
|
-
const jobsPath =
|
|
5770
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3683
5771
|
try {
|
|
3684
5772
|
if (!existsSync(jobsPath)) {
|
|
3685
5773
|
log.debug("pattern reinforcement cron: jobs.json not found, skipping auto-register");
|
|
@@ -3741,7 +5829,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3741
5829
|
}
|
|
3742
5830
|
async autoRegisterGraphEdgeDecayCron() {
|
|
3743
5831
|
const home = resolveHomeDir();
|
|
3744
|
-
const jobsPath =
|
|
5832
|
+
const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
|
|
3745
5833
|
try {
|
|
3746
5834
|
if (!existsSync(jobsPath)) {
|
|
3747
5835
|
log.debug("graph edge decay cron: jobs.json not found, skipping auto-register");
|
|
@@ -3798,15 +5886,15 @@ ${doc.content}` : doc.content,
|
|
|
3798
5886
|
this.lastFileHygieneRunAtMs = now;
|
|
3799
5887
|
if (hygiene.rotateEnabled) {
|
|
3800
5888
|
for (const rel of hygiene.rotatePaths) {
|
|
3801
|
-
const abs =
|
|
5889
|
+
const abs = path4.isAbsolute(rel) ? rel : path4.join(this.config.workspaceDir, rel);
|
|
3802
5890
|
try {
|
|
3803
|
-
const raw = await
|
|
5891
|
+
const raw = await readFile4(abs, "utf-8");
|
|
3804
5892
|
if (raw.length > hygiene.rotateMaxBytes) {
|
|
3805
|
-
const archiveDir =
|
|
5893
|
+
const archiveDir = path4.join(
|
|
3806
5894
|
this.config.workspaceDir,
|
|
3807
5895
|
hygiene.archiveDir
|
|
3808
5896
|
);
|
|
3809
|
-
const base =
|
|
5897
|
+
const base = path4.basename(abs);
|
|
3810
5898
|
const prefix = base.toUpperCase().replace(/\.MD$/i, "").replace(/[^A-Z0-9]+/g, "-") || "FILE";
|
|
3811
5899
|
const { newContent } = await rotateMarkdownFileToArchive({
|
|
3812
5900
|
filePath: abs,
|
|
@@ -3814,7 +5902,7 @@ ${doc.content}` : doc.content,
|
|
|
3814
5902
|
archivePrefix: prefix,
|
|
3815
5903
|
keepTailChars: hygiene.rotateKeepTailChars
|
|
3816
5904
|
});
|
|
3817
|
-
await
|
|
5905
|
+
await writeFile4(abs, newContent, "utf-8");
|
|
3818
5906
|
}
|
|
3819
5907
|
} catch {
|
|
3820
5908
|
}
|
|
@@ -3831,8 +5919,8 @@ ${doc.content}` : doc.content,
|
|
|
3831
5919
|
log.warn(w.message);
|
|
3832
5920
|
}
|
|
3833
5921
|
if (hygiene.warningsLogEnabled && warnings.length > 0) {
|
|
3834
|
-
const fp =
|
|
3835
|
-
await
|
|
5922
|
+
const fp = path4.join(this.config.memoryDir, hygiene.warningsLogPath);
|
|
5923
|
+
await mkdir4(path4.dirname(fp), { recursive: true });
|
|
3836
5924
|
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3837
5925
|
const block = `
|
|
3838
5926
|
|
|
@@ -3841,11 +5929,11 @@ ${doc.content}` : doc.content,
|
|
|
3841
5929
|
` + warnings.map((w) => `- ${w.message}`).join("\n") + "\n";
|
|
3842
5930
|
let existing = "";
|
|
3843
5931
|
try {
|
|
3844
|
-
existing = await
|
|
5932
|
+
existing = await readFile4(fp, "utf-8");
|
|
3845
5933
|
} catch {
|
|
3846
5934
|
existing = "# Engram File Hygiene Warnings\n";
|
|
3847
5935
|
}
|
|
3848
|
-
await
|
|
5936
|
+
await writeFile4(fp, existing + block, "utf-8");
|
|
3849
5937
|
}
|
|
3850
5938
|
}
|
|
3851
5939
|
}
|
|
@@ -3952,6 +6040,7 @@ ${doc.content}` : doc.content,
|
|
|
3952
6040
|
log.warn(`[semantic-consolidation] extension discovery failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
3953
6041
|
}
|
|
3954
6042
|
for (const cluster of clusters) {
|
|
6043
|
+
let canonicalWriteCompleted = false;
|
|
3955
6044
|
try {
|
|
3956
6045
|
const operatorAwareEnabled = this.config.operatorAwareConsolidationEnabled === true;
|
|
3957
6046
|
let prompt = operatorAwareEnabled ? buildOperatorAwareConsolidationPrompt(cluster) : buildConsolidationPrompt(cluster);
|
|
@@ -4030,6 +6119,7 @@ ${doc.content}` : doc.content,
|
|
|
4030
6119
|
derivedVia: operator
|
|
4031
6120
|
}
|
|
4032
6121
|
);
|
|
6122
|
+
canonicalWriteCompleted = true;
|
|
4033
6123
|
result.memoriesConsolidated++;
|
|
4034
6124
|
for (const m of cluster.memories) {
|
|
4035
6125
|
const archiveResult = await targetStorage.archiveMemory(m, {
|
|
@@ -4048,13 +6138,19 @@ ${doc.content}` : doc.content,
|
|
|
4048
6138
|
this.contentHashIndex.remove(m.content);
|
|
4049
6139
|
}
|
|
4050
6140
|
}
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
6141
|
+
try {
|
|
6142
|
+
await this.embeddingFallback.removeFromIndex(m.frontmatter.id);
|
|
6143
|
+
if (this.config.queryAwareIndexingEnabled && m.path && m.frontmatter?.created) {
|
|
6144
|
+
deindexMemory(
|
|
6145
|
+
targetStorage.dir,
|
|
6146
|
+
m.path,
|
|
6147
|
+
m.frontmatter.created,
|
|
6148
|
+
m.frontmatter.tags ?? []
|
|
6149
|
+
);
|
|
6150
|
+
}
|
|
6151
|
+
} catch (cleanupErr) {
|
|
6152
|
+
log.warn(
|
|
6153
|
+
`[semantic-consolidation] index cleanup failed (non-fatal): ${cleanupErr}`
|
|
4058
6154
|
);
|
|
4059
6155
|
}
|
|
4060
6156
|
result.memoriesArchived++;
|
|
@@ -4068,6 +6164,13 @@ ${doc.content}` : doc.content,
|
|
|
4068
6164
|
`[semantic-consolidation] cluster processing failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4069
6165
|
);
|
|
4070
6166
|
result.errors++;
|
|
6167
|
+
} finally {
|
|
6168
|
+
if (canonicalWriteCompleted) {
|
|
6169
|
+
this.markCatalogWrite(
|
|
6170
|
+
this.namespaceFromStorageDir(targetStorage.dir),
|
|
6171
|
+
targetStorage.dir
|
|
6172
|
+
);
|
|
6173
|
+
}
|
|
4071
6174
|
}
|
|
4072
6175
|
}
|
|
4073
6176
|
if (result.memoriesArchived > 0 && this.contentHashIndex) {
|
|
@@ -4300,18 +6403,18 @@ ${evidenceText}`
|
|
|
4300
6403
|
const now = options.now instanceof Date && Number.isFinite(options.now.getTime()) ? options.now : /* @__PURE__ */ new Date();
|
|
4301
6404
|
const targetLocalDate = formatDateInTimeZone(now, timeZone);
|
|
4302
6405
|
const datesToScan = utcDateKeysForLocalDay(now, timeZone);
|
|
4303
|
-
const factsBaseDir =
|
|
6406
|
+
const factsBaseDir = path4.join(storage.dir, "facts");
|
|
4304
6407
|
const MAX_CHARS = 1e5;
|
|
4305
6408
|
const facts = [];
|
|
4306
6409
|
for (const date of datesToScan) {
|
|
4307
|
-
const factsDir =
|
|
6410
|
+
const factsDir = path4.join(factsBaseDir, date);
|
|
4308
6411
|
try {
|
|
4309
|
-
const entries = await
|
|
6412
|
+
const entries = await readdir3(factsDir, { withFileTypes: true });
|
|
4310
6413
|
for (const entry of entries) {
|
|
4311
6414
|
if (!entry.name.endsWith(".md")) continue;
|
|
4312
|
-
const fullPath =
|
|
6415
|
+
const fullPath = path4.join(factsDir, entry.name);
|
|
4313
6416
|
try {
|
|
4314
|
-
const raw = await
|
|
6417
|
+
const raw = await readFile4(fullPath, "utf-8");
|
|
4315
6418
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
4316
6419
|
if (!fmMatch) continue;
|
|
4317
6420
|
const fmBlock = fmMatch[1];
|
|
@@ -4330,7 +6433,7 @@ ${evidenceText}`
|
|
|
4330
6433
|
facts.push({
|
|
4331
6434
|
path: fullPath,
|
|
4332
6435
|
frontmatter: {
|
|
4333
|
-
id: fm.id ||
|
|
6436
|
+
id: fm.id || path4.basename(entry.name, ".md"),
|
|
4334
6437
|
category: fm.category || "fact",
|
|
4335
6438
|
created,
|
|
4336
6439
|
updated: fm.updated || created,
|
|
@@ -4352,15 +6455,15 @@ ${evidenceText}`
|
|
|
4352
6455
|
return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
|
|
4353
6456
|
});
|
|
4354
6457
|
const hourlySummaries = [];
|
|
4355
|
-
const hourlyBaseDir =
|
|
6458
|
+
const hourlyBaseDir = path4.join(storage.dir, "summaries", "hourly");
|
|
4356
6459
|
try {
|
|
4357
|
-
const sessionKeys = await
|
|
6460
|
+
const sessionKeys = await readdir3(hourlyBaseDir, { withFileTypes: true });
|
|
4358
6461
|
for (const sk of sessionKeys) {
|
|
4359
6462
|
if (!sk.isDirectory()) continue;
|
|
4360
6463
|
for (const date of datesToScan) {
|
|
4361
|
-
const summaryFile =
|
|
6464
|
+
const summaryFile = path4.join(hourlyBaseDir, sk.name, `${date}.md`);
|
|
4362
6465
|
try {
|
|
4363
|
-
const raw = await
|
|
6466
|
+
const raw = await readFile4(summaryFile, "utf-8");
|
|
4364
6467
|
const filtered = filterHourlySummaryMarkdownForLocalDay(
|
|
4365
6468
|
raw,
|
|
4366
6469
|
date,
|
|
@@ -4462,13 +6565,13 @@ ${evidenceText}`
|
|
|
4462
6565
|
}
|
|
4463
6566
|
async getLastGraphRecallSnapshot(namespace) {
|
|
4464
6567
|
const storage = await this.getStorage(namespace);
|
|
4465
|
-
const snapshotPath =
|
|
6568
|
+
const snapshotPath = path4.join(
|
|
4466
6569
|
storage.dir,
|
|
4467
6570
|
"state",
|
|
4468
6571
|
"last_graph_recall.json"
|
|
4469
6572
|
);
|
|
4470
6573
|
try {
|
|
4471
|
-
const raw = await
|
|
6574
|
+
const raw = await readFile4(snapshotPath, "utf-8");
|
|
4472
6575
|
const parsed = JSON.parse(raw);
|
|
4473
6576
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4474
6577
|
return {
|
|
@@ -4501,9 +6604,9 @@ ${evidenceText}`
|
|
|
4501
6604
|
}
|
|
4502
6605
|
async getLastIntentSnapshot(namespace) {
|
|
4503
6606
|
const storage = await this.getStorage(namespace);
|
|
4504
|
-
const snapshotPath =
|
|
6607
|
+
const snapshotPath = path4.join(storage.dir, "state", "last_intent.json");
|
|
4505
6608
|
try {
|
|
4506
|
-
const raw = await
|
|
6609
|
+
const raw = await readFile4(snapshotPath, "utf-8");
|
|
4507
6610
|
const parsed = JSON.parse(raw);
|
|
4508
6611
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4509
6612
|
const graphDecision = parsed.graphDecision && typeof parsed.graphDecision === "object" ? parsed.graphDecision : void 0;
|
|
@@ -4534,13 +6637,13 @@ ${evidenceText}`
|
|
|
4534
6637
|
}
|
|
4535
6638
|
async getLastQmdRecallSnapshot(namespace) {
|
|
4536
6639
|
const storage = await this.getStorage(namespace);
|
|
4537
|
-
const snapshotPath =
|
|
6640
|
+
const snapshotPath = path4.join(
|
|
4538
6641
|
storage.dir,
|
|
4539
6642
|
"state",
|
|
4540
6643
|
"last_qmd_recall.json"
|
|
4541
6644
|
);
|
|
4542
6645
|
try {
|
|
4543
|
-
const raw = await
|
|
6646
|
+
const raw = await readFile4(snapshotPath, "utf-8");
|
|
4544
6647
|
const parsed = JSON.parse(raw);
|
|
4545
6648
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4546
6649
|
return {
|
|
@@ -4684,10 +6787,10 @@ ${r.snippet.trim()}
|
|
|
4684
6787
|
}
|
|
4685
6788
|
async countConversationChunkDocs(dir) {
|
|
4686
6789
|
try {
|
|
4687
|
-
const entries = await
|
|
6790
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
4688
6791
|
let total = 0;
|
|
4689
6792
|
for (const entry of entries) {
|
|
4690
|
-
const fullPath =
|
|
6793
|
+
const fullPath = path4.join(dir, entry.name);
|
|
4691
6794
|
if (entry.isDirectory()) {
|
|
4692
6795
|
total += await this.countConversationChunkDocs(fullPath);
|
|
4693
6796
|
continue;
|
|
@@ -5413,7 +7516,7 @@ ${r.snippet.trim()}
|
|
|
5413
7516
|
if (!options.onDebugSnapshot) return;
|
|
5414
7517
|
await options.onDebugSnapshot({
|
|
5415
7518
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5416
|
-
queryHash:
|
|
7519
|
+
queryHash: createHash2("sha256").update(prompt).digest("hex"),
|
|
5417
7520
|
queryLength: prompt.length,
|
|
5418
7521
|
collection: options.collection,
|
|
5419
7522
|
namespaces: options.recallNamespaces,
|
|
@@ -5586,7 +7689,7 @@ ${r.snippet.trim()}
|
|
|
5586
7689
|
resolvedPath = resolvedCold.result.path;
|
|
5587
7690
|
resolvedResult = resolvedCold.result;
|
|
5588
7691
|
}
|
|
5589
|
-
if (!
|
|
7692
|
+
if (!path4.isAbsolute(resolvedPath)) {
|
|
5590
7693
|
resolvedAmbiguousSeeds.set(result.path, null);
|
|
5591
7694
|
return null;
|
|
5592
7695
|
}
|
|
@@ -5611,7 +7714,7 @@ ${r.snippet.trim()}
|
|
|
5611
7714
|
}
|
|
5612
7715
|
continue;
|
|
5613
7716
|
}
|
|
5614
|
-
if (
|
|
7717
|
+
if (path4.isAbsolute(result.path)) {
|
|
5615
7718
|
const resolved = await resolveAmbiguousSeedOwner(result, null);
|
|
5616
7719
|
if (resolved) {
|
|
5617
7720
|
addResultForNamespace(resolved.namespace, resolved.result);
|
|
@@ -5654,7 +7757,7 @@ ${r.snippet.trim()}
|
|
|
5654
7757
|
0
|
|
5655
7758
|
);
|
|
5656
7759
|
seedPaths.push(
|
|
5657
|
-
...seedRelativePaths.map((rel) =>
|
|
7760
|
+
...seedRelativePaths.map((rel) => path4.join(storage.dir, rel))
|
|
5658
7761
|
);
|
|
5659
7762
|
const seedSet = new Set(seedRelativePaths);
|
|
5660
7763
|
const expanded = await this.graphIndexFor(storage).spreadingActivation(
|
|
@@ -5670,7 +7773,7 @@ ${r.snippet.trim()}
|
|
|
5670
7773
|
for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
|
|
5671
7774
|
if (deadlineExpired()) break;
|
|
5672
7775
|
if (seedSet.has(candidate.path)) continue;
|
|
5673
|
-
const memoryPath =
|
|
7776
|
+
const memoryPath = path4.resolve(storage.dir, candidate.path);
|
|
5674
7777
|
const memory = await storage.readMemoryByPath(memoryPath);
|
|
5675
7778
|
if (deadlineExpired()) break;
|
|
5676
7779
|
if (!memory) continue;
|
|
@@ -5695,7 +7798,7 @@ ${r.snippet.trim()}
|
|
|
5695
7798
|
path: memory.path,
|
|
5696
7799
|
score,
|
|
5697
7800
|
namespace,
|
|
5698
|
-
seed:
|
|
7801
|
+
seed: path4.resolve(storage.dir, candidate.seed),
|
|
5699
7802
|
hopDepth: candidate.hopDepth,
|
|
5700
7803
|
decayedWeight: candidate.decayedWeight,
|
|
5701
7804
|
graphType: candidate.graphType,
|
|
@@ -5716,12 +7819,12 @@ ${r.snippet.trim()}
|
|
|
5716
7819
|
}
|
|
5717
7820
|
async recordLastGraphRecallSnapshot(options) {
|
|
5718
7821
|
try {
|
|
5719
|
-
const snapshotPath =
|
|
7822
|
+
const snapshotPath = path4.join(
|
|
5720
7823
|
options.storage.dir,
|
|
5721
7824
|
"state",
|
|
5722
7825
|
"last_graph_recall.json"
|
|
5723
7826
|
);
|
|
5724
|
-
await
|
|
7827
|
+
await mkdir4(path4.dirname(snapshotPath), { recursive: true });
|
|
5725
7828
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5726
7829
|
const totalSeedCount = options.seedPaths.length;
|
|
5727
7830
|
const totalExpandedCount = options.expandedPaths.length;
|
|
@@ -5733,7 +7836,7 @@ ${r.snippet.trim()}
|
|
|
5733
7836
|
const payload = {
|
|
5734
7837
|
recordedAt: now,
|
|
5735
7838
|
mode: options.recallMode,
|
|
5736
|
-
queryHash:
|
|
7839
|
+
queryHash: createHash2("sha256").update(options.prompt).digest("hex"),
|
|
5737
7840
|
queryLength: options.prompt.length,
|
|
5738
7841
|
namespaces: options.recallNamespaces,
|
|
5739
7842
|
seedCount: totalSeedCount,
|
|
@@ -5748,20 +7851,20 @@ ${r.snippet.trim()}
|
|
|
5748
7851
|
finalResults: (options.finalResults ?? []).slice(0, 64),
|
|
5749
7852
|
shadowComparison: options.shadowComparison
|
|
5750
7853
|
};
|
|
5751
|
-
await
|
|
7854
|
+
await writeFile4(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
5752
7855
|
} catch (err) {
|
|
5753
7856
|
log.debug(`last graph recall write failed: ${err}`);
|
|
5754
7857
|
}
|
|
5755
7858
|
}
|
|
5756
7859
|
async recordLastIntentSnapshot(options) {
|
|
5757
7860
|
try {
|
|
5758
|
-
const snapshotPath =
|
|
7861
|
+
const snapshotPath = path4.join(
|
|
5759
7862
|
options.storage.dir,
|
|
5760
7863
|
"state",
|
|
5761
7864
|
"last_intent.json"
|
|
5762
7865
|
);
|
|
5763
|
-
await
|
|
5764
|
-
await
|
|
7866
|
+
await mkdir4(path4.dirname(snapshotPath), { recursive: true });
|
|
7867
|
+
await writeFile4(
|
|
5765
7868
|
snapshotPath,
|
|
5766
7869
|
JSON.stringify(options.snapshot, null, 2),
|
|
5767
7870
|
"utf-8"
|
|
@@ -5772,13 +7875,13 @@ ${r.snippet.trim()}
|
|
|
5772
7875
|
}
|
|
5773
7876
|
async recordLastQmdRecallSnapshot(options) {
|
|
5774
7877
|
try {
|
|
5775
|
-
const snapshotPath =
|
|
7878
|
+
const snapshotPath = path4.join(
|
|
5776
7879
|
options.storage.dir,
|
|
5777
7880
|
"state",
|
|
5778
7881
|
"last_qmd_recall.json"
|
|
5779
7882
|
);
|
|
5780
|
-
await
|
|
5781
|
-
await
|
|
7883
|
+
await mkdir4(path4.dirname(snapshotPath), { recursive: true });
|
|
7884
|
+
await writeFile4(
|
|
5782
7885
|
snapshotPath,
|
|
5783
7886
|
JSON.stringify(options.snapshot, null, 2),
|
|
5784
7887
|
"utf-8"
|
|
@@ -5792,9 +7895,9 @@ ${r.snippet.trim()}
|
|
|
5792
7895
|
const stateDir = await this.resolveStateDirForNamespace(
|
|
5793
7896
|
options.namespace
|
|
5794
7897
|
);
|
|
5795
|
-
const snapshotPath =
|
|
5796
|
-
await
|
|
5797
|
-
await
|
|
7898
|
+
const snapshotPath = path4.join(stateDir, "last_intent.json");
|
|
7899
|
+
await mkdir4(path4.dirname(snapshotPath), { recursive: true });
|
|
7900
|
+
await writeFile4(
|
|
5798
7901
|
snapshotPath,
|
|
5799
7902
|
JSON.stringify(options.snapshot, null, 2),
|
|
5800
7903
|
"utf-8"
|
|
@@ -5805,24 +7908,24 @@ ${r.snippet.trim()}
|
|
|
5805
7908
|
}
|
|
5806
7909
|
async resolveStateDirForNamespace(namespace) {
|
|
5807
7910
|
if (!this.config.namespacesEnabled) {
|
|
5808
|
-
return
|
|
7911
|
+
return path4.join(this.config.memoryDir, "state");
|
|
5809
7912
|
}
|
|
5810
7913
|
if (namespace !== this.config.defaultNamespace) {
|
|
5811
|
-
return
|
|
7914
|
+
return path4.join(this.config.memoryDir, "namespaces", namespace, "state");
|
|
5812
7915
|
}
|
|
5813
|
-
const candidate =
|
|
7916
|
+
const candidate = path4.join(
|
|
5814
7917
|
this.config.memoryDir,
|
|
5815
7918
|
"namespaces",
|
|
5816
7919
|
this.config.defaultNamespace
|
|
5817
7920
|
);
|
|
5818
7921
|
try {
|
|
5819
|
-
const candidateStat = await
|
|
7922
|
+
const candidateStat = await stat3(candidate);
|
|
5820
7923
|
if (candidateStat.isDirectory()) {
|
|
5821
|
-
return
|
|
7924
|
+
return path4.join(candidate, "state");
|
|
5822
7925
|
}
|
|
5823
7926
|
} catch {
|
|
5824
7927
|
}
|
|
5825
|
-
return
|
|
7928
|
+
return path4.join(this.config.memoryDir, "state");
|
|
5826
7929
|
}
|
|
5827
7930
|
buildGraphRecallRankedResults(results, sourceLabelResolver, limit = 64) {
|
|
5828
7931
|
return results.slice(0, limit).map((result) => ({
|
|
@@ -6064,8 +8167,8 @@ ${r.snippet.trim()}
|
|
|
6064
8167
|
timings,
|
|
6065
8168
|
logger: log
|
|
6066
8169
|
});
|
|
6067
|
-
const promptHash =
|
|
6068
|
-
const traceId =
|
|
8170
|
+
const promptHash = createHash2("sha256").update(prompt).digest("hex");
|
|
8171
|
+
const traceId = createHash2("sha256").update(`${sessionKey ?? "default"}:${recallStart}:${promptHash}`).digest("hex").slice(0, 16);
|
|
6069
8172
|
const sectionBuckets = /* @__PURE__ */ new Map();
|
|
6070
8173
|
const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
|
|
6071
8174
|
cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
|
|
@@ -6074,7 +8177,7 @@ ${r.snippet.trim()}
|
|
|
6074
8177
|
cronConversationRecallMode: this.config.cronConversationRecallMode
|
|
6075
8178
|
});
|
|
6076
8179
|
const retrievalQuery = queryPolicy.retrievalQuery || prompt;
|
|
6077
|
-
const retrievalQueryHash =
|
|
8180
|
+
const retrievalQueryHash = createHash2("sha256").update(retrievalQuery).digest("hex");
|
|
6078
8181
|
const policyVersion = this.currentPolicyVersion();
|
|
6079
8182
|
let impressionRecorded = false;
|
|
6080
8183
|
let recallSource = "none";
|
|
@@ -6232,7 +8335,7 @@ ${r.snippet.trim()}
|
|
|
6232
8335
|
const graphExpandedResultPaths = /* @__PURE__ */ new Set();
|
|
6233
8336
|
const graphSourceLabelsForPath = (resultPath) => {
|
|
6234
8337
|
const labels = [];
|
|
6235
|
-
const normalizedPath = resultPath.split(
|
|
8338
|
+
const normalizedPath = resultPath.split(path4.sep).join("/");
|
|
6236
8339
|
const isEntityPath = normalizedPath.startsWith("entities/") || normalizedPath.includes("/entities/");
|
|
6237
8340
|
if (graphBaselinePaths.has(resultPath)) labels.push("baseline");
|
|
6238
8341
|
if (graphExpandedResultPaths.has(resultPath))
|
|
@@ -6379,6 +8482,9 @@ ${r.snippet.trim()}
|
|
|
6379
8482
|
}
|
|
6380
8483
|
const profileStorage = await this.storageRouter.storageFor(selfNamespace);
|
|
6381
8484
|
throwIfRecallAborted(options.abortSignal);
|
|
8485
|
+
if (this.namespaceCatalog.enabled && recallResultLimit > 0 && !options.abortSignal?.aborted) {
|
|
8486
|
+
for (const ns of recallNamespaces) this.markCatalogRead(ns);
|
|
8487
|
+
}
|
|
6382
8488
|
const sharedContextPromise = (async () => {
|
|
6383
8489
|
if (!this.isRecallSectionEnabled(
|
|
6384
8490
|
"shared-context",
|
|
@@ -7547,16 +9653,16 @@ ${formatted}`;
|
|
|
7547
9653
|
if (!this.config.compactionResetEnabled) return null;
|
|
7548
9654
|
const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
|
|
7549
9655
|
const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
|
|
7550
|
-
const signalPath =
|
|
9656
|
+
const signalPath = path4.join(
|
|
7551
9657
|
workspaceDir,
|
|
7552
9658
|
`.compaction-reset-signal-${safeSessionKey}`
|
|
7553
9659
|
);
|
|
7554
|
-
const bootPath =
|
|
9660
|
+
const bootPath = path4.join(workspaceDir, "BOOT.md");
|
|
7555
9661
|
try {
|
|
7556
|
-
const signalStat = await
|
|
9662
|
+
const signalStat = await stat3(signalPath).catch(() => null);
|
|
7557
9663
|
if (!signalStat) return null;
|
|
7558
9664
|
const signalAge = Date.now() - signalStat.mtimeMs;
|
|
7559
|
-
const signalData = JSON.parse(await
|
|
9665
|
+
const signalData = JSON.parse(await readFile4(signalPath, "utf-8"));
|
|
7560
9666
|
if (signalData.sessionKey !== effectiveSessionKey) {
|
|
7561
9667
|
log.debug(
|
|
7562
9668
|
`recall: compaction signal is for ${signalData.sessionKey}, not ${effectiveSessionKey} \u2014 skipping`
|
|
@@ -7567,7 +9673,7 @@ ${formatted}`;
|
|
|
7567
9673
|
log.debug(
|
|
7568
9674
|
`recall: stale compaction signal (${Math.round(signalAge / 1e3)}s old), skipping`
|
|
7569
9675
|
);
|
|
7570
|
-
await
|
|
9676
|
+
await unlink2(signalPath).catch(() => {
|
|
7571
9677
|
});
|
|
7572
9678
|
return null;
|
|
7573
9679
|
}
|
|
@@ -7576,7 +9682,7 @@ ${formatted}`;
|
|
|
7576
9682
|
|
|
7577
9683
|
`;
|
|
7578
9684
|
try {
|
|
7579
|
-
const bootContent = await
|
|
9685
|
+
const bootContent = await readFile4(bootPath, "utf-8");
|
|
7580
9686
|
section += "### BOOT.md (working state before compaction)\n\n";
|
|
7581
9687
|
section += bootContent + "\n";
|
|
7582
9688
|
} catch {
|
|
@@ -7587,12 +9693,12 @@ ${formatted}`;
|
|
|
7587
9693
|
log.info(
|
|
7588
9694
|
`recall: injected compaction reset context for ${effectiveSessionKey}`
|
|
7589
9695
|
);
|
|
7590
|
-
await
|
|
9696
|
+
await unlink2(signalPath).catch(() => {
|
|
7591
9697
|
});
|
|
7592
9698
|
return section;
|
|
7593
9699
|
} catch (err) {
|
|
7594
9700
|
log.debug("recall: compaction signal check failed:", err);
|
|
7595
|
-
await
|
|
9701
|
+
await unlink2(signalPath).catch(() => {
|
|
7596
9702
|
});
|
|
7597
9703
|
return null;
|
|
7598
9704
|
}
|
|
@@ -9681,7 +11787,7 @@ _Context: ${topQuestion.context}_`
|
|
|
9681
11787
|
const shouldUseStableBatchKey = turns.some(
|
|
9682
11788
|
(turn) => turn.persistProcessedFingerprint === true || typeof turn.turnFingerprint === "string" && turn.turnFingerprint.length > 0
|
|
9683
11789
|
);
|
|
9684
|
-
const stableBatchFingerprint = shouldUseStableBatchKey ?
|
|
11790
|
+
const stableBatchFingerprint = shouldUseStableBatchKey ? createHash2("sha256").update(
|
|
9685
11791
|
turns.map(
|
|
9686
11792
|
(turn) => [
|
|
9687
11793
|
turn.role,
|
|
@@ -9938,7 +12044,7 @@ _Context: ${topQuestion.context}_`
|
|
|
9938
12044
|
buildExtractionFingerprint(turns, bufferKey) {
|
|
9939
12045
|
const normalized = this.normalizeExtractionFingerprintTurns(turns).join("\n");
|
|
9940
12046
|
if (!normalized) return null;
|
|
9941
|
-
return
|
|
12047
|
+
return createHash2("sha256").update(`${bufferKey}
|
|
9942
12048
|
${normalized}`).digest("hex");
|
|
9943
12049
|
}
|
|
9944
12050
|
shouldQueueExtraction(turns, options = {}) {
|
|
@@ -10211,7 +12317,10 @@ ${normalized}`).digest("hex");
|
|
|
10211
12317
|
result,
|
|
10212
12318
|
storage,
|
|
10213
12319
|
threadIdForExtraction,
|
|
10214
|
-
{ sessionKey, principal, validAt: sourceValidAt }
|
|
12320
|
+
{ sessionKey, principal, validAt: sourceValidAt },
|
|
12321
|
+
// Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
|
|
12322
|
+
// real namespace rather than a guess decoded from the storage dir.
|
|
12323
|
+
selfNamespace
|
|
10215
12324
|
);
|
|
10216
12325
|
let postPersistMetadataFailed = false;
|
|
10217
12326
|
meta ??= await storage.loadMeta();
|
|
@@ -10425,7 +12534,7 @@ ${normalized}`).digest("hex");
|
|
|
10425
12534
|
);
|
|
10426
12535
|
this.tierMigrationInFlight = true;
|
|
10427
12536
|
try {
|
|
10428
|
-
const coldStorage = new StorageManager(
|
|
12537
|
+
const coldStorage = new StorageManager(path4.join(storage.dir, "cold"));
|
|
10429
12538
|
const [hotMemories, coldMemories] = await Promise.all([
|
|
10430
12539
|
storage.readAllMemories(),
|
|
10431
12540
|
coldStorage.readAllMemories()
|
|
@@ -10569,22 +12678,68 @@ ${normalized}`).digest("hex");
|
|
|
10569
12678
|
this.qmdMaintenancePending = false;
|
|
10570
12679
|
try {
|
|
10571
12680
|
if (this.config.namespacesEnabled) {
|
|
10572
|
-
await this.
|
|
10573
|
-
|
|
12681
|
+
const plan = await this.namespaceMaintenancePlan("qmd");
|
|
12682
|
+
const now = Date.now();
|
|
12683
|
+
const lastEmbedAtByNamespace = this.lastQmdEmbedAtMsByNamespace ?? (this.lastQmdEmbedAtMsByNamespace = /* @__PURE__ */ new Map());
|
|
12684
|
+
const dueEmbedNamespaces = (namespaces) => {
|
|
12685
|
+
if (!this.config.qmdAutoEmbedEnabled) return [];
|
|
12686
|
+
return namespaces.filter(
|
|
12687
|
+
(namespace) => now - (lastEmbedAtByNamespace.get(namespace) ?? 0) >= this.config.qmdEmbedMinIntervalMs
|
|
12688
|
+
);
|
|
12689
|
+
};
|
|
12690
|
+
const markEmbedded = (namespaces) => {
|
|
12691
|
+
if (namespaces.length === 0) return;
|
|
12692
|
+
for (const namespace of namespaces) {
|
|
12693
|
+
lastEmbedAtByNamespace.set(namespace, now);
|
|
12694
|
+
}
|
|
12695
|
+
this.lastQmdEmbedAtMs = now;
|
|
12696
|
+
};
|
|
12697
|
+
await runNamespaceMaintenanceBatchPlan(
|
|
12698
|
+
this.config,
|
|
12699
|
+
plan,
|
|
12700
|
+
async (candidates) => {
|
|
12701
|
+
const namespaces = candidates.map((candidate) => candidate.namespace);
|
|
12702
|
+
const embedNamespaces = dueEmbedNamespaces(namespaces);
|
|
12703
|
+
let result;
|
|
12704
|
+
try {
|
|
12705
|
+
result = await this.namespaceSearchRouter.updateNamespacesDetailed(
|
|
12706
|
+
namespaces,
|
|
12707
|
+
void 0,
|
|
12708
|
+
{ strict: true }
|
|
12709
|
+
);
|
|
12710
|
+
} catch (error) {
|
|
12711
|
+
if (embedNamespaces.length > 0 && qmdMaintenanceSkipReasonForError(error) === "throttled") {
|
|
12712
|
+
await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
|
|
12713
|
+
markEmbedded(embedNamespaces);
|
|
12714
|
+
}
|
|
12715
|
+
throw error;
|
|
12716
|
+
}
|
|
12717
|
+
if (result.backendCount <= 0) {
|
|
12718
|
+
throw new Error("no eligible QMD backend for selected namespaces");
|
|
12719
|
+
}
|
|
12720
|
+
if (result.eligibleNamespaces.length !== namespaces.length) {
|
|
12721
|
+
const eligible = new Set(result.eligibleNamespaces);
|
|
12722
|
+
const missing = namespaces.filter((namespace) => !eligible.has(namespace));
|
|
12723
|
+
throw new Error(`QMD backend ineligible for selected namespaces (${missing.length})`);
|
|
12724
|
+
}
|
|
12725
|
+
if (embedNamespaces.length > 0) {
|
|
12726
|
+
await this.namespaceSearchRouter.embedNamespaces(embedNamespaces, { strict: true });
|
|
12727
|
+
markEmbedded(embedNamespaces);
|
|
12728
|
+
}
|
|
12729
|
+
return { itemCount: result.backendCount };
|
|
12730
|
+
},
|
|
12731
|
+
this.namespaceCatalog,
|
|
12732
|
+
{
|
|
12733
|
+
skipReasonForError: qmdMaintenanceSkipReasonForError
|
|
12734
|
+
}
|
|
10574
12735
|
);
|
|
10575
12736
|
} else {
|
|
10576
12737
|
await this.qmd.update();
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
if (this.config.qmdAutoEmbedEnabled && now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs) {
|
|
10580
|
-
if (this.config.namespacesEnabled) {
|
|
10581
|
-
await this.namespaceSearchRouter.embedNamespaces(
|
|
10582
|
-
this.configuredNamespaces()
|
|
10583
|
-
);
|
|
10584
|
-
} else {
|
|
12738
|
+
const now = Date.now();
|
|
12739
|
+
if (this.config.qmdAutoEmbedEnabled && now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs) {
|
|
10585
12740
|
await this.qmd.embed();
|
|
12741
|
+
this.lastQmdEmbedAtMs = now;
|
|
10586
12742
|
}
|
|
10587
|
-
this.lastQmdEmbedAtMs = now;
|
|
10588
12743
|
}
|
|
10589
12744
|
} finally {
|
|
10590
12745
|
this.qmdMaintenanceInFlight = false;
|
|
@@ -10593,7 +12748,7 @@ ${normalized}`).digest("hex");
|
|
|
10593
12748
|
}
|
|
10594
12749
|
}
|
|
10595
12750
|
}
|
|
10596
|
-
async persistExtraction(result, storage, threadIdForExtraction, sourceContext) {
|
|
12751
|
+
async persistExtraction(result, storage, threadIdForExtraction, sourceContext, baseNamespace) {
|
|
10597
12752
|
const citationEnabled = this.config.inlineSourceAttributionEnabled === true;
|
|
10598
12753
|
const citationTemplate = this.config.inlineSourceAttributionFormat;
|
|
10599
12754
|
const citationContextBase = citationEnabled ? {
|
|
@@ -10700,7 +12855,7 @@ ${normalized}`).digest("hex");
|
|
|
10700
12855
|
});
|
|
10701
12856
|
hashDedupLookupComplete = true;
|
|
10702
12857
|
if (hashDedupMatchingFact) {
|
|
10703
|
-
await applyTemporalSupersession({
|
|
12858
|
+
const hashDedupSupersession = await applyTemporalSupersession({
|
|
10704
12859
|
storage: sharedStorage,
|
|
10705
12860
|
newMemoryId: hashDedupMatchingFact.frontmatter.id,
|
|
10706
12861
|
entityRef: options.entityRef,
|
|
@@ -10709,6 +12864,9 @@ ${normalized}`).digest("hex");
|
|
|
10709
12864
|
enabled: true,
|
|
10710
12865
|
useCallerTimestamp: true
|
|
10711
12866
|
});
|
|
12867
|
+
if (hashDedupSupersession.supersededIds.length > 0) {
|
|
12868
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
12869
|
+
}
|
|
10712
12870
|
return;
|
|
10713
12871
|
}
|
|
10714
12872
|
log.debug(
|
|
@@ -10769,6 +12927,7 @@ ${normalized}`).digest("hex");
|
|
|
10769
12927
|
);
|
|
10770
12928
|
}
|
|
10771
12929
|
}
|
|
12930
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
10772
12931
|
trackPersistedId(sharedStorage, promotedId, {
|
|
10773
12932
|
includeReturnedIds: false
|
|
10774
12933
|
});
|
|
@@ -10995,6 +13154,7 @@ ${normalized}`).digest("hex");
|
|
|
10995
13154
|
fact.confidence = typeof fact.confidence === "number" ? fact.confidence : 0.7;
|
|
10996
13155
|
let writeCategory = fact.category;
|
|
10997
13156
|
let targetStorage = storage;
|
|
13157
|
+
let targetNamespaceName = baseNamespace && baseNamespace.length > 0 ? baseNamespace : this.namespaceFromStorageDir(targetStorage.dir);
|
|
10998
13158
|
let routedRuleId;
|
|
10999
13159
|
let routedNamespaceExplicit = false;
|
|
11000
13160
|
if (routeRules.length > 0) {
|
|
@@ -11011,6 +13171,7 @@ ${normalized}`).digest("hex");
|
|
|
11011
13171
|
targetStorage = await this.storageRouter.storageFor(
|
|
11012
13172
|
selected.target.namespace
|
|
11013
13173
|
);
|
|
13174
|
+
targetNamespaceName = selected.target.namespace;
|
|
11014
13175
|
}
|
|
11015
13176
|
}
|
|
11016
13177
|
} catch (err) {
|
|
@@ -11026,6 +13187,7 @@ ${normalized}`).digest("hex");
|
|
|
11026
13187
|
targetStorage = await this.storageRouter.storageFor(
|
|
11027
13188
|
this.config.sharedNamespace
|
|
11028
13189
|
);
|
|
13190
|
+
targetNamespaceName = this.config.sharedNamespace;
|
|
11029
13191
|
log.debug(
|
|
11030
13192
|
`scope-routing: fact "${fact.content.slice(0, 60)}\u2026" routed to shared namespace (scope=global)`
|
|
11031
13193
|
);
|
|
@@ -11227,34 +13389,38 @@ ${normalized}`).digest("hex");
|
|
|
11227
13389
|
contentHashSource: rawChunkedContent
|
|
11228
13390
|
}
|
|
11229
13391
|
);
|
|
11230
|
-
|
|
11231
|
-
const
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
13392
|
+
try {
|
|
13393
|
+
for (const chunk of chunkResult.chunks) {
|
|
13394
|
+
const chunkImportance = scoreImportance(
|
|
13395
|
+
chunk.content,
|
|
13396
|
+
writeCategory,
|
|
13397
|
+
fact.tags
|
|
13398
|
+
);
|
|
13399
|
+
const chunkWriteSource = fact.source === "proactive" ? "chunking-proactive" : "chunking";
|
|
13400
|
+
await targetStorage.writeChunk(
|
|
13401
|
+
parentId,
|
|
13402
|
+
chunk.index,
|
|
13403
|
+
chunkResult.chunks.length,
|
|
13404
|
+
writeCategory,
|
|
13405
|
+
// Each chunk carries its own inline citation so provenance
|
|
13406
|
+
// survives when a single chunk is quoted in isolation.
|
|
13407
|
+
applyInlineCitation(chunk.content),
|
|
13408
|
+
{
|
|
13409
|
+
confidence: fact.confidence,
|
|
13410
|
+
tags: fact.tags,
|
|
13411
|
+
entityRef: fact.entityRef,
|
|
13412
|
+
source: chunkWriteSource,
|
|
13413
|
+
importance: chunkImportance,
|
|
13414
|
+
intentGoal: inferredIntent?.goal,
|
|
13415
|
+
intentActionType: inferredIntent?.actionType,
|
|
13416
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
13417
|
+
memoryKind: memoryKind2,
|
|
13418
|
+
validAt: sourceContext?.validAt
|
|
13419
|
+
}
|
|
13420
|
+
);
|
|
13421
|
+
}
|
|
13422
|
+
} finally {
|
|
13423
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11258
13424
|
}
|
|
11259
13425
|
if (routedRuleId) {
|
|
11260
13426
|
log.debug(
|
|
@@ -11307,51 +13473,55 @@ ${normalized}`).digest("hex");
|
|
|
11307
13473
|
const chunkId = `${parentId}-chunk-${chunk.index}`;
|
|
11308
13474
|
await this.indexPersistedMemory(targetStorage, chunkId);
|
|
11309
13475
|
}
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
}
|
|
11321
|
-
if (this.config.multiGraphMemoryEnabled) {
|
|
11322
|
-
try {
|
|
11323
|
-
const graphContext = await ensureGraphContext(targetStorage);
|
|
11324
|
-
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
11325
|
-
const parentRelPath = resolvePersistedMemoryRelativePath({
|
|
11326
|
-
memoryId: parentId,
|
|
11327
|
-
pathById: graphContext.memoryPathById,
|
|
11328
|
-
category: writeCategory
|
|
11329
|
-
});
|
|
11330
|
-
graphContext.memoryPathById.set(parentId, parentRelPath);
|
|
11331
|
-
appendMemoryToGraphContext({
|
|
11332
|
-
allMemsForGraph: graphContext.allMemsForGraph,
|
|
11333
|
-
storageDir: targetStorage.dir,
|
|
11334
|
-
memoryRelPath: parentRelPath,
|
|
11335
|
-
memoryId: parentId,
|
|
11336
|
-
category: writeCategory,
|
|
11337
|
-
content: fact.content ?? "",
|
|
11338
|
-
entityRef
|
|
13476
|
+
try {
|
|
13477
|
+
if (this.config.verbatimArtifactsEnabled && this.config.verbatimArtifactCategories.includes(writeCategory) && fact.confidence >= this.config.verbatimArtifactsMinConfidence) {
|
|
13478
|
+
await targetStorage.writeArtifact(citedChunkedContent, {
|
|
13479
|
+
confidence: fact.confidence,
|
|
13480
|
+
tags: [...fact.tags, "artifact", "chunked-parent"],
|
|
13481
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
13482
|
+
sourceMemoryId: parentId,
|
|
13483
|
+
intentGoal: inferredIntent?.goal,
|
|
13484
|
+
intentActionType: inferredIntent?.actionType,
|
|
13485
|
+
intentEntityTypes: inferredIntent?.entityTypes
|
|
11339
13486
|
});
|
|
11340
|
-
await this.buildGraphEdge(
|
|
11341
|
-
targetStorage,
|
|
11342
|
-
parentRelPath,
|
|
11343
|
-
entityRef,
|
|
11344
|
-
parentId,
|
|
11345
|
-
fact.content ?? "",
|
|
11346
|
-
graphContext.allMemsForGraph,
|
|
11347
|
-
graphContext.memoryPathById,
|
|
11348
|
-
threadIdForExtraction ?? void 0,
|
|
11349
|
-
threadEpisodeIdsForGraph,
|
|
11350
|
-
graphContext.previousPersistedRelPath
|
|
11351
|
-
);
|
|
11352
|
-
graphContext.previousPersistedRelPath = parentRelPath;
|
|
11353
|
-
} catch {
|
|
11354
13487
|
}
|
|
13488
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
13489
|
+
try {
|
|
13490
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
13491
|
+
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
13492
|
+
const parentRelPath = resolvePersistedMemoryRelativePath({
|
|
13493
|
+
memoryId: parentId,
|
|
13494
|
+
pathById: graphContext.memoryPathById,
|
|
13495
|
+
category: writeCategory
|
|
13496
|
+
});
|
|
13497
|
+
graphContext.memoryPathById.set(parentId, parentRelPath);
|
|
13498
|
+
appendMemoryToGraphContext({
|
|
13499
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
13500
|
+
storageDir: targetStorage.dir,
|
|
13501
|
+
memoryRelPath: parentRelPath,
|
|
13502
|
+
memoryId: parentId,
|
|
13503
|
+
category: writeCategory,
|
|
13504
|
+
content: fact.content ?? "",
|
|
13505
|
+
entityRef
|
|
13506
|
+
});
|
|
13507
|
+
await this.buildGraphEdge(
|
|
13508
|
+
targetStorage,
|
|
13509
|
+
parentRelPath,
|
|
13510
|
+
entityRef,
|
|
13511
|
+
parentId,
|
|
13512
|
+
fact.content ?? "",
|
|
13513
|
+
graphContext.allMemsForGraph,
|
|
13514
|
+
graphContext.memoryPathById,
|
|
13515
|
+
threadIdForExtraction ?? void 0,
|
|
13516
|
+
threadEpisodeIdsForGraph,
|
|
13517
|
+
graphContext.previousPersistedRelPath
|
|
13518
|
+
);
|
|
13519
|
+
graphContext.previousPersistedRelPath = parentRelPath;
|
|
13520
|
+
} catch {
|
|
13521
|
+
}
|
|
13522
|
+
}
|
|
13523
|
+
} finally {
|
|
13524
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11355
13525
|
}
|
|
11356
13526
|
trackBehaviorSignals(
|
|
11357
13527
|
targetStorage,
|
|
@@ -11419,91 +13589,107 @@ ${normalized}`).digest("hex");
|
|
|
11419
13589
|
} catch (err) {
|
|
11420
13590
|
log.warn(`temporal-supersession: unexpected error: ${err}`);
|
|
11421
13591
|
}
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
category: writeCategory,
|
|
11427
|
-
content: fact.content,
|
|
11428
|
-
namespace: this.namespaceFromStorageDir(targetStorage.dir),
|
|
11429
|
-
confidence: fact.confidence,
|
|
11430
|
-
source: "extraction"
|
|
11431
|
-
})
|
|
11432
|
-
);
|
|
11433
|
-
trackPersistedId(targetStorage, memoryId);
|
|
11434
|
-
if (threadEpisodeIdsForGraph && !threadEpisodeIdsForGraph.includes(memoryId)) {
|
|
11435
|
-
threadEpisodeIdsForGraph.push(memoryId);
|
|
11436
|
-
}
|
|
11437
|
-
await this.indexPersistedMemory(targetStorage, memoryId);
|
|
11438
|
-
await promoteMemoryToShared({
|
|
11439
|
-
sourceStorage: targetStorage,
|
|
11440
|
-
category: writeCategory,
|
|
11441
|
-
content: fact.content,
|
|
11442
|
-
confidence: fact.confidence,
|
|
11443
|
-
tags: fact.tags,
|
|
11444
|
-
entityRef: typeof fact.entityRef === "string" ? fact.entityRef : void 0,
|
|
11445
|
-
structuredAttributes: fact.structuredAttributes,
|
|
11446
|
-
sourceMemoryId: memoryId,
|
|
11447
|
-
importance,
|
|
11448
|
-
intentGoal: inferredIntent?.goal,
|
|
11449
|
-
intentActionType: inferredIntent?.actionType,
|
|
11450
|
-
intentEntityTypes: inferredIntent?.entityTypes,
|
|
11451
|
-
memoryKind,
|
|
11452
|
-
validAt: sourceContext?.validAt,
|
|
11453
|
-
source: extractionWriteSource
|
|
11454
|
-
});
|
|
11455
|
-
if (this.config.multiGraphMemoryEnabled) {
|
|
11456
|
-
try {
|
|
11457
|
-
const graphContext = await ensureGraphContext(targetStorage);
|
|
11458
|
-
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
11459
|
-
const memoryRelPath = resolvePersistedMemoryRelativePath({
|
|
11460
|
-
memoryId,
|
|
11461
|
-
pathById: graphContext.memoryPathById,
|
|
11462
|
-
category: writeCategory
|
|
11463
|
-
});
|
|
11464
|
-
graphContext.memoryPathById.set(memoryId, memoryRelPath);
|
|
11465
|
-
appendMemoryToGraphContext({
|
|
11466
|
-
allMemsForGraph: graphContext.allMemsForGraph,
|
|
11467
|
-
storageDir: targetStorage.dir,
|
|
11468
|
-
memoryRelPath,
|
|
13592
|
+
try {
|
|
13593
|
+
trackBehaviorSignals(
|
|
13594
|
+
targetStorage,
|
|
13595
|
+
buildBehaviorSignalsForMemory({
|
|
11469
13596
|
memoryId,
|
|
11470
13597
|
category: writeCategory,
|
|
11471
|
-
content: fact.content
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
graphContext.allMemsForGraph,
|
|
11481
|
-
graphContext.memoryPathById,
|
|
11482
|
-
threadIdForExtraction ?? void 0,
|
|
11483
|
-
threadEpisodeIdsForGraph,
|
|
11484
|
-
graphContext.previousPersistedRelPath
|
|
11485
|
-
);
|
|
11486
|
-
graphContext.previousPersistedRelPath = memoryRelPath;
|
|
11487
|
-
} catch {
|
|
13598
|
+
content: fact.content,
|
|
13599
|
+
namespace: this.namespaceFromStorageDir(targetStorage.dir),
|
|
13600
|
+
confidence: fact.confidence,
|
|
13601
|
+
source: "extraction"
|
|
13602
|
+
})
|
|
13603
|
+
);
|
|
13604
|
+
trackPersistedId(targetStorage, memoryId);
|
|
13605
|
+
if (threadEpisodeIdsForGraph && !threadEpisodeIdsForGraph.includes(memoryId)) {
|
|
13606
|
+
threadEpisodeIdsForGraph.push(memoryId);
|
|
11488
13607
|
}
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
13608
|
+
await this.indexPersistedMemory(targetStorage, memoryId);
|
|
13609
|
+
await promoteMemoryToShared({
|
|
13610
|
+
sourceStorage: targetStorage,
|
|
13611
|
+
category: writeCategory,
|
|
13612
|
+
content: fact.content,
|
|
11492
13613
|
confidence: fact.confidence,
|
|
11493
|
-
tags:
|
|
11494
|
-
|
|
13614
|
+
tags: fact.tags,
|
|
13615
|
+
entityRef: typeof fact.entityRef === "string" ? fact.entityRef : void 0,
|
|
13616
|
+
structuredAttributes: fact.structuredAttributes,
|
|
11495
13617
|
sourceMemoryId: memoryId,
|
|
13618
|
+
importance,
|
|
11496
13619
|
intentGoal: inferredIntent?.goal,
|
|
11497
13620
|
intentActionType: inferredIntent?.actionType,
|
|
11498
|
-
intentEntityTypes: inferredIntent?.entityTypes
|
|
13621
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
13622
|
+
memoryKind,
|
|
13623
|
+
validAt: sourceContext?.validAt,
|
|
13624
|
+
source: extractionWriteSource
|
|
11499
13625
|
});
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
13626
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
13627
|
+
try {
|
|
13628
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
13629
|
+
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
13630
|
+
const memoryRelPath = resolvePersistedMemoryRelativePath({
|
|
13631
|
+
memoryId,
|
|
13632
|
+
pathById: graphContext.memoryPathById,
|
|
13633
|
+
category: writeCategory
|
|
13634
|
+
});
|
|
13635
|
+
graphContext.memoryPathById.set(memoryId, memoryRelPath);
|
|
13636
|
+
appendMemoryToGraphContext({
|
|
13637
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
13638
|
+
storageDir: targetStorage.dir,
|
|
13639
|
+
memoryRelPath,
|
|
13640
|
+
memoryId,
|
|
13641
|
+
category: writeCategory,
|
|
13642
|
+
content: fact.content ?? "",
|
|
13643
|
+
entityRef
|
|
13644
|
+
});
|
|
13645
|
+
await this.buildGraphEdge(
|
|
13646
|
+
targetStorage,
|
|
13647
|
+
memoryRelPath,
|
|
13648
|
+
entityRef,
|
|
13649
|
+
memoryId,
|
|
13650
|
+
fact.content ?? "",
|
|
13651
|
+
graphContext.allMemsForGraph,
|
|
13652
|
+
graphContext.memoryPathById,
|
|
13653
|
+
threadIdForExtraction ?? void 0,
|
|
13654
|
+
threadEpisodeIdsForGraph,
|
|
13655
|
+
graphContext.previousPersistedRelPath
|
|
13656
|
+
);
|
|
13657
|
+
graphContext.previousPersistedRelPath = memoryRelPath;
|
|
13658
|
+
} catch {
|
|
13659
|
+
}
|
|
13660
|
+
}
|
|
13661
|
+
if (this.config.verbatimArtifactsEnabled && this.config.verbatimArtifactCategories.includes(writeCategory) && fact.confidence >= this.config.verbatimArtifactsMinConfidence) {
|
|
13662
|
+
await targetStorage.writeArtifact(citedFactContent, {
|
|
13663
|
+
confidence: fact.confidence,
|
|
13664
|
+
tags: [...fact.tags, "artifact"],
|
|
13665
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
13666
|
+
sourceMemoryId: memoryId,
|
|
13667
|
+
intentGoal: inferredIntent?.goal,
|
|
13668
|
+
intentActionType: inferredIntent?.actionType,
|
|
13669
|
+
intentEntityTypes: inferredIntent?.entityTypes
|
|
13670
|
+
});
|
|
13671
|
+
}
|
|
13672
|
+
if (this.contentHashIndex) {
|
|
13673
|
+
const canonicalFactContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
|
|
13674
|
+
const hashRegisterKey = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : canonicalFactContent;
|
|
13675
|
+
this.contentHashIndex.add(hashRegisterKey);
|
|
13676
|
+
}
|
|
13677
|
+
} finally {
|
|
13678
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11505
13679
|
}
|
|
11506
13680
|
}
|
|
13681
|
+
let durableNonFactWritten = false;
|
|
13682
|
+
let durableNonFactTouchRecorded = false;
|
|
13683
|
+
const touchBaseNonFactNamespace = () => {
|
|
13684
|
+
const baseTouchNamespace = baseNamespace && baseNamespace.length > 0 ? baseNamespace : this.namespaceFromStorageDir(storage.dir);
|
|
13685
|
+
this.markCatalogWrite(baseTouchNamespace, storage.dir);
|
|
13686
|
+
};
|
|
13687
|
+
const recordDurableNonFactWrite = () => {
|
|
13688
|
+
durableNonFactWritten = true;
|
|
13689
|
+
if (durableNonFactTouchRecorded) return;
|
|
13690
|
+
durableNonFactTouchRecorded = true;
|
|
13691
|
+
touchBaseNonFactNamespace();
|
|
13692
|
+
};
|
|
11507
13693
|
for (const entity of entities) {
|
|
11508
13694
|
try {
|
|
11509
13695
|
const name = entity?.name;
|
|
@@ -11519,7 +13705,10 @@ ${normalized}`).digest("hex");
|
|
|
11519
13705
|
principal: sourceContext?.principal,
|
|
11520
13706
|
structuredSections: Array.isArray(entity?.structuredSections) ? entity.structuredSections : void 0
|
|
11521
13707
|
});
|
|
11522
|
-
if (id)
|
|
13708
|
+
if (id) {
|
|
13709
|
+
trackPersistedId(storage, id);
|
|
13710
|
+
recordDurableNonFactWrite();
|
|
13711
|
+
}
|
|
11523
13712
|
} catch (err) {
|
|
11524
13713
|
log.warn(`persistExtraction: entity write failed: ${err}`);
|
|
11525
13714
|
}
|
|
@@ -11532,10 +13721,12 @@ ${normalized}`).digest("hex");
|
|
|
11532
13721
|
target: rel.target,
|
|
11533
13722
|
label: rel.label
|
|
11534
13723
|
});
|
|
13724
|
+
recordDurableNonFactWrite();
|
|
11535
13725
|
await storage.addEntityRelationship(rel.target, {
|
|
11536
13726
|
target: rel.source,
|
|
11537
13727
|
label: `${rel.label} (reverse)`
|
|
11538
13728
|
});
|
|
13729
|
+
recordDurableNonFactWrite();
|
|
11539
13730
|
} catch (err) {
|
|
11540
13731
|
log.debug(`relationship persist failed: ${err}`);
|
|
11541
13732
|
}
|
|
@@ -11561,18 +13752,26 @@ ${normalized}`).digest("hex");
|
|
|
11561
13752
|
}
|
|
11562
13753
|
if (profileUpdates.length > 0) {
|
|
11563
13754
|
await storage.appendToProfile(profileUpdates);
|
|
13755
|
+
recordDurableNonFactWrite();
|
|
11564
13756
|
}
|
|
11565
13757
|
for (const q of questions) {
|
|
11566
13758
|
const id = await storage.writeQuestion(q.question, q.context, q.priority);
|
|
11567
|
-
if (id)
|
|
13759
|
+
if (id) {
|
|
13760
|
+
trackPersistedId(storage, id);
|
|
13761
|
+
recordDurableNonFactWrite();
|
|
13762
|
+
}
|
|
11568
13763
|
}
|
|
11569
13764
|
if (this.config.identityEnabled && result.identityReflection) {
|
|
11570
13765
|
try {
|
|
11571
13766
|
await storage.appendIdentityReflection(result.identityReflection);
|
|
13767
|
+
recordDurableNonFactWrite();
|
|
11572
13768
|
} catch (err) {
|
|
11573
13769
|
log.debug(`identity reflection write failed: ${err}`);
|
|
11574
13770
|
}
|
|
11575
13771
|
}
|
|
13772
|
+
if (durableNonFactWritten) {
|
|
13773
|
+
touchBaseNonFactNamespace();
|
|
13774
|
+
}
|
|
11576
13775
|
if (this.contentHashIndex) {
|
|
11577
13776
|
await this.contentHashIndex.save().catch((err) => log.warn(`content-hash index save failed: ${err}`));
|
|
11578
13777
|
}
|
|
@@ -11628,7 +13827,7 @@ ${normalized}`).digest("hex");
|
|
|
11628
13827
|
const allMems = allMemsForGraph ?? [];
|
|
11629
13828
|
for (const m of allMems) {
|
|
11630
13829
|
if (m.frontmatter.entityRef === entityRef) {
|
|
11631
|
-
const rel =
|
|
13830
|
+
const rel = path4.relative(storage.dir, m.path);
|
|
11632
13831
|
if (rel !== memoryRelPath) entitySiblings.push(rel);
|
|
11633
13832
|
}
|
|
11634
13833
|
}
|
|
@@ -11730,6 +13929,7 @@ ${normalized}`).digest("hex");
|
|
|
11730
13929
|
log.info("running consolidation pass");
|
|
11731
13930
|
let merged = 0;
|
|
11732
13931
|
let invalidated = 0;
|
|
13932
|
+
let memoryItemMutated = false;
|
|
11733
13933
|
if (this.accessTrackingBuffer.size > 0) {
|
|
11734
13934
|
await this.flushAccessTracking();
|
|
11735
13935
|
}
|
|
@@ -11752,6 +13952,7 @@ ${normalized}`).digest("hex");
|
|
|
11752
13952
|
const toInvalidate = this.config.queryAwareIndexingEnabled ? memoryLookup?.get(item.existingId) ?? null : null;
|
|
11753
13953
|
if (await this.storage.invalidateMemory(item.existingId)) {
|
|
11754
13954
|
invalidated += 1;
|
|
13955
|
+
memoryItemMutated = true;
|
|
11755
13956
|
await this.embeddingFallback.removeFromIndex(item.existingId);
|
|
11756
13957
|
if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
|
|
11757
13958
|
deindexMemory(
|
|
@@ -11773,6 +13974,7 @@ ${normalized}`).digest("hex");
|
|
|
11773
13974
|
lineage: [item.existingId]
|
|
11774
13975
|
}
|
|
11775
13976
|
);
|
|
13977
|
+
memoryItemMutated = true;
|
|
11776
13978
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
11777
13979
|
}
|
|
11778
13980
|
break;
|
|
@@ -11786,6 +13988,7 @@ ${normalized}`).digest("hex");
|
|
|
11786
13988
|
lineage: [item.existingId, item.mergeWith]
|
|
11787
13989
|
}
|
|
11788
13990
|
);
|
|
13991
|
+
memoryItemMutated = true;
|
|
11789
13992
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
11790
13993
|
const toMergeInvalidate = this.config.queryAwareIndexingEnabled ? memoryLookup?.get(item.mergeWith) ?? null : null;
|
|
11791
13994
|
if (await this.storage.invalidateMemory(item.mergeWith)) {
|
|
@@ -11815,8 +14018,12 @@ ${normalized}`).digest("hex");
|
|
|
11815
14018
|
structuredSections: Array.isArray(entity?.structuredSections) ? entity.structuredSections : void 0
|
|
11816
14019
|
});
|
|
11817
14020
|
}
|
|
14021
|
+
if (result.profileUpdates.length > 0 || result.entityUpdates.length > 0) {
|
|
14022
|
+
memoryItemMutated = true;
|
|
14023
|
+
}
|
|
11818
14024
|
const entitiesMerged = await this.storage.mergeFragmentedEntities();
|
|
11819
14025
|
if (entitiesMerged > 0) {
|
|
14026
|
+
memoryItemMutated = true;
|
|
11820
14027
|
log.info(`merged ${entitiesMerged} fragmented entity files`);
|
|
11821
14028
|
}
|
|
11822
14029
|
if (this.config.entitySummaryEnabled) {
|
|
@@ -11826,6 +14033,7 @@ ${normalized}`).digest("hex");
|
|
|
11826
14033
|
5
|
|
11827
14034
|
);
|
|
11828
14035
|
if (synthesized > 0) {
|
|
14036
|
+
memoryItemMutated = true;
|
|
11829
14037
|
log.info(`refreshed ${synthesized} entity syntheses`);
|
|
11830
14038
|
}
|
|
11831
14039
|
} catch (err) {
|
|
@@ -11836,6 +14044,7 @@ ${normalized}`).digest("hex");
|
|
|
11836
14044
|
this.config.commitmentDecayDays
|
|
11837
14045
|
);
|
|
11838
14046
|
if (deletedCommitments.length > 0) {
|
|
14047
|
+
memoryItemMutated = true;
|
|
11839
14048
|
log.info(`cleaned ${deletedCommitments.length} expired commitments`);
|
|
11840
14049
|
if (this.config.queryAwareIndexingEnabled) {
|
|
11841
14050
|
for (const m of deletedCommitments) {
|
|
@@ -11857,6 +14066,7 @@ ${normalized}`).digest("hex");
|
|
|
11857
14066
|
decayDays: this.config.commitmentDecayDays
|
|
11858
14067
|
});
|
|
11859
14068
|
if (lifecycle.transitionedToExpired.length > 0 || lifecycle.deletedResolved.length > 0) {
|
|
14069
|
+
memoryItemMutated = true;
|
|
11860
14070
|
log.info(
|
|
11861
14071
|
`commitment ledger lifecycle: expired ${lifecycle.transitionedToExpired.length}, cleaned ${lifecycle.deletedResolved.length}`
|
|
11862
14072
|
);
|
|
@@ -11867,6 +14077,7 @@ ${normalized}`).digest("hex");
|
|
|
11867
14077
|
}
|
|
11868
14078
|
const deletedTTL = await this.storage.cleanExpiredTTL();
|
|
11869
14079
|
if (deletedTTL.length > 0) {
|
|
14080
|
+
memoryItemMutated = true;
|
|
11870
14081
|
log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
|
|
11871
14082
|
if (this.config.queryAwareIndexingEnabled) {
|
|
11872
14083
|
for (const m of deletedTTL) {
|
|
@@ -11883,7 +14094,9 @@ ${normalized}`).digest("hex");
|
|
|
11883
14094
|
try {
|
|
11884
14095
|
const lightSleepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11885
14096
|
const lifecycleCorpus = await this.storage.readAllMemories();
|
|
11886
|
-
await this.runLifecyclePolicyPass(lifecycleCorpus)
|
|
14097
|
+
if (await this.runLifecyclePolicyPass(lifecycleCorpus) > 0) {
|
|
14098
|
+
memoryItemMutated = true;
|
|
14099
|
+
}
|
|
11887
14100
|
await this.recordScheduledDreamsPhaseRun(
|
|
11888
14101
|
"lightSleep",
|
|
11889
14102
|
lifecycleCorpus.length,
|
|
@@ -11900,11 +14113,13 @@ ${normalized}`).digest("hex");
|
|
|
11900
14113
|
await this.runCompressionGuidelineLearningPass();
|
|
11901
14114
|
try {
|
|
11902
14115
|
const deepSleepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11903
|
-
await this.runTierMigrationCycle(this.storage, "maintenance");
|
|
14116
|
+
const tierMigration = await this.runTierMigrationCycle(this.storage, "maintenance");
|
|
14117
|
+
if (tierMigration.migrated > 0) memoryItemMutated = true;
|
|
11904
14118
|
allMemories = await this.storage.readAllMemories();
|
|
11905
14119
|
if (this.config.factArchivalEnabled) {
|
|
11906
14120
|
const archived = await this.runFactArchival(allMemories);
|
|
11907
14121
|
if (archived > 0) {
|
|
14122
|
+
memoryItemMutated = true;
|
|
11908
14123
|
log.info(`archived ${archived} old low-importance facts`);
|
|
11909
14124
|
}
|
|
11910
14125
|
}
|
|
@@ -11928,14 +14143,14 @@ ${normalized}`).digest("hex");
|
|
|
11928
14143
|
}
|
|
11929
14144
|
if (this.config.semanticConsolidationEnabled) {
|
|
11930
14145
|
try {
|
|
11931
|
-
const stateFilePath =
|
|
14146
|
+
const stateFilePath = path4.join(
|
|
11932
14147
|
this.config.memoryDir,
|
|
11933
14148
|
"state",
|
|
11934
14149
|
"semantic-consolidation-last-run.json"
|
|
11935
14150
|
);
|
|
11936
14151
|
let shouldRun = true;
|
|
11937
14152
|
try {
|
|
11938
|
-
const stateRaw = await
|
|
14153
|
+
const stateRaw = await readFile4(stateFilePath, "utf-8");
|
|
11939
14154
|
const stateData = JSON.parse(stateRaw);
|
|
11940
14155
|
if (stateData.lastRunAt) {
|
|
11941
14156
|
const lastRunMs = new Date(stateData.lastRunAt).getTime();
|
|
@@ -11976,9 +14191,9 @@ ${normalized}`).digest("hex");
|
|
|
11976
14191
|
);
|
|
11977
14192
|
}
|
|
11978
14193
|
if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
|
|
11979
|
-
const stateDir =
|
|
11980
|
-
await
|
|
11981
|
-
await
|
|
14194
|
+
const stateDir = path4.join(this.config.memoryDir, "state");
|
|
14195
|
+
await mkdir4(stateDir, { recursive: true });
|
|
14196
|
+
await writeFile4(
|
|
11982
14197
|
stateFilePath,
|
|
11983
14198
|
JSON.stringify({ lastRunAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
11984
14199
|
"utf-8"
|
|
@@ -12009,6 +14224,7 @@ ${normalized}`).digest("hex");
|
|
|
12009
14224
|
);
|
|
12010
14225
|
if (profileResult) {
|
|
12011
14226
|
await this.storage.writeProfile(profileResult.consolidatedProfile);
|
|
14227
|
+
memoryItemMutated = true;
|
|
12012
14228
|
log.info(
|
|
12013
14229
|
`profile.md consolidated: removed ${profileResult.removedCount} items \u2014 ${profileResult.summary}`
|
|
12014
14230
|
);
|
|
@@ -12080,6 +14296,12 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12080
14296
|
}
|
|
12081
14297
|
}
|
|
12082
14298
|
}
|
|
14299
|
+
if (memoryItemMutated) {
|
|
14300
|
+
this.markCatalogWrite(
|
|
14301
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
14302
|
+
this.storage.dir
|
|
14303
|
+
);
|
|
14304
|
+
}
|
|
12083
14305
|
log.info("consolidation complete");
|
|
12084
14306
|
return { memoriesProcessed: allMemories.length, merged, invalidated };
|
|
12085
14307
|
}
|
|
@@ -12170,7 +14392,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12170
14392
|
}
|
|
12171
14393
|
});
|
|
12172
14394
|
const content = renderCompressionGuidelinesMarkdown(refinedCandidate);
|
|
12173
|
-
const contentHash =
|
|
14395
|
+
const contentHash = createHash2("sha256").update(content).digest("hex");
|
|
12174
14396
|
const semanticRefinementApplied = JSON.stringify(refinedCandidate.ruleUpdates) !== JSON.stringify(candidate.ruleUpdates);
|
|
12175
14397
|
const changedRules = refinedCandidate.ruleUpdates.filter(
|
|
12176
14398
|
(rule) => rule.delta !== 0
|
|
@@ -12398,7 +14620,9 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12398
14620
|
}
|
|
12399
14621
|
async runLifecyclePolicyNow(storage = this.storage) {
|
|
12400
14622
|
const lifecycleCorpus = await storage.readAllMemories();
|
|
12401
|
-
await this.runLifecyclePolicyPass(lifecycleCorpus, storage)
|
|
14623
|
+
if (await this.runLifecyclePolicyPass(lifecycleCorpus, storage) > 0) {
|
|
14624
|
+
this.markCatalogWrite(this.namespaceFromStorageDir(storage.dir), storage.dir);
|
|
14625
|
+
}
|
|
12402
14626
|
return { memoriesAssessed: lifecycleCorpus.length };
|
|
12403
14627
|
}
|
|
12404
14628
|
async runLifecyclePolicyPass(allMemories, storage = this.storage) {
|
|
@@ -12454,7 +14678,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12454
14678
|
});
|
|
12455
14679
|
if (wrote) updatedCount += 1;
|
|
12456
14680
|
}
|
|
12457
|
-
if (!this.config.lifecycleMetricsEnabled) return;
|
|
14681
|
+
if (!this.config.lifecycleMetricsEnabled) return updatedCount;
|
|
12458
14682
|
const total = evaluatedCount;
|
|
12459
14683
|
const metrics = {
|
|
12460
14684
|
generatedAt: nowIso,
|
|
@@ -12471,13 +14695,14 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12471
14695
|
protectedCategories: this.config.lifecycleProtectedCategories
|
|
12472
14696
|
}
|
|
12473
14697
|
};
|
|
12474
|
-
const metricsPath =
|
|
14698
|
+
const metricsPath = path4.join(
|
|
12475
14699
|
storage.dir,
|
|
12476
14700
|
"state",
|
|
12477
14701
|
"lifecycle-metrics.json"
|
|
12478
14702
|
);
|
|
12479
|
-
await
|
|
12480
|
-
await
|
|
14703
|
+
await mkdir4(path4.dirname(metricsPath), { recursive: true });
|
|
14704
|
+
await writeFile4(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
|
|
14705
|
+
return updatedCount;
|
|
12481
14706
|
}
|
|
12482
14707
|
/**
|
|
12483
14708
|
* Archive old, low-importance, rarely-accessed facts (v6.0).
|
|
@@ -12549,8 +14774,8 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12549
14774
|
const sorted = activeMemories.sort(
|
|
12550
14775
|
(a, b) => new Date(a.frontmatter.created).getTime() - new Date(b.frontmatter.created).getTime()
|
|
12551
14776
|
);
|
|
12552
|
-
const
|
|
12553
|
-
const toSummarize = sorted.slice(0, -
|
|
14777
|
+
const recentToKeep = Math.max(0, this.config.summarizationRecentToKeep);
|
|
14778
|
+
const toSummarize = recentToKeep > 0 ? sorted.slice(0, -recentToKeep) : sorted;
|
|
12554
14779
|
const candidates = toSummarize.filter((m) => {
|
|
12555
14780
|
if (m.frontmatter.entityRef) return false;
|
|
12556
14781
|
const protectedTags = this.config.summarizationProtectedTags;
|
|
@@ -12593,6 +14818,10 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12593
14818
|
batch.map((m) => m.frontmatter.id),
|
|
12594
14819
|
summary.id
|
|
12595
14820
|
);
|
|
14821
|
+
this.markCatalogWrite(
|
|
14822
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
14823
|
+
this.storage.dir
|
|
14824
|
+
);
|
|
12596
14825
|
log.info(
|
|
12597
14826
|
`created summary ${summary.id} from ${batch.length} memories, archived ${archived}`
|
|
12598
14827
|
);
|
|
@@ -12618,7 +14847,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12618
14847
|
/** Threshold (bytes) at which IDENTITY.md reflections get auto-consolidated */
|
|
12619
14848
|
static IDENTITY_CONSOLIDATE_THRESHOLD = 8e3;
|
|
12620
14849
|
async autoConsolidateIdentity() {
|
|
12621
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
14850
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
12622
14851
|
for (const namespace of namespaces) {
|
|
12623
14852
|
const storage = await this.storageRouter.storageFor(namespace);
|
|
12624
14853
|
const identityNamespace = this.config.namespacesEnabled && namespace !== this.config.defaultNamespace ? namespace : void 0;
|
|
@@ -12661,6 +14890,7 @@ ${reflectionsContent.trim()}
|
|
|
12661
14890
|
identityNamespace
|
|
12662
14891
|
);
|
|
12663
14892
|
await storage.writeIdentityReflections("");
|
|
14893
|
+
this.markCatalogWrite(namespace, storage.dir);
|
|
12664
14894
|
log.info(
|
|
12665
14895
|
`IDENTITY(${namespace}) consolidated: ${identityContent.length} \u2192 ${newContent.length} chars, ${result.learnedPatterns.length} patterns`
|
|
12666
14896
|
);
|
|
@@ -13004,7 +15234,7 @@ ${lines.join("\n\n")}`;
|
|
|
13004
15234
|
const seenStorageDirs = /* @__PURE__ */ new Set();
|
|
13005
15235
|
const addStorage = (storage) => {
|
|
13006
15236
|
const storageDir = storageDirFor(storage);
|
|
13007
|
-
const storageKey = storageDir ?
|
|
15237
|
+
const storageKey = storageDir ? path4.resolve(storageDir) : `storage-without-dir-${storages.length}`;
|
|
13008
15238
|
if (seenStorageDirs.has(storageKey)) return;
|
|
13009
15239
|
seenStorageDirs.add(storageKey);
|
|
13010
15240
|
storages.push(storage);
|
|
@@ -13034,7 +15264,7 @@ ${lines.join("\n\n")}`;
|
|
|
13034
15264
|
continue;
|
|
13035
15265
|
}
|
|
13036
15266
|
try {
|
|
13037
|
-
const coldRoot =
|
|
15267
|
+
const coldRoot = path4.join(storageDir, "cold");
|
|
13038
15268
|
for (const candidate of qmdResultPathCandidates(
|
|
13039
15269
|
coldRoot,
|
|
13040
15270
|
parts.relativePath
|
|
@@ -13075,7 +15305,7 @@ ${lines.join("\n\n")}`;
|
|
|
13075
15305
|
return null;
|
|
13076
15306
|
}
|
|
13077
15307
|
}
|
|
13078
|
-
if (
|
|
15308
|
+
if (path4.isAbsolute(resultPath)) {
|
|
13079
15309
|
if (!fallbackStorageDir) {
|
|
13080
15310
|
return await fallbackStorage.readMemoryByPath(resultPath);
|
|
13081
15311
|
}
|
|
@@ -13114,7 +15344,7 @@ ${lines.join("\n\n")}`;
|
|
|
13114
15344
|
);
|
|
13115
15345
|
if (!memory) return null;
|
|
13116
15346
|
let ownerNamespace = null;
|
|
13117
|
-
if (
|
|
15347
|
+
if (path4.isAbsolute(memory.path)) {
|
|
13118
15348
|
const ownerStorage = await this.storageForAbsoluteQmdResultPath(
|
|
13119
15349
|
memory.path,
|
|
13120
15350
|
fallbackStorage,
|
|
@@ -13138,16 +15368,16 @@ ${lines.join("\n\n")}`;
|
|
|
13138
15368
|
};
|
|
13139
15369
|
}
|
|
13140
15370
|
async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
|
|
13141
|
-
const resolvedPath =
|
|
13142
|
-
const memoryRoot =
|
|
13143
|
-
const namespacesRoot =
|
|
15371
|
+
const resolvedPath = path4.resolve(resultPath);
|
|
15372
|
+
const memoryRoot = path4.resolve(this.config.memoryDir);
|
|
15373
|
+
const namespacesRoot = path4.join(memoryRoot, "namespaces");
|
|
13144
15374
|
const fallbackStorageDir = typeof fallbackStorage.dir === "string" && fallbackStorage.dir ? fallbackStorage.dir : null;
|
|
13145
15375
|
const matches = [];
|
|
13146
15376
|
const seenDirs = /* @__PURE__ */ new Set();
|
|
13147
15377
|
const maybeAddStorage = (storage, namespace) => {
|
|
13148
15378
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
13149
15379
|
if (!storageDir) return;
|
|
13150
|
-
const candidateRoot =
|
|
15380
|
+
const candidateRoot = path4.resolve(storageDir);
|
|
13151
15381
|
if (seenDirs.has(candidateRoot)) return;
|
|
13152
15382
|
if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
|
|
13153
15383
|
if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
@@ -13165,7 +15395,7 @@ ${lines.join("\n\n")}`;
|
|
|
13165
15395
|
candidateNamespaces.add(ns);
|
|
13166
15396
|
}
|
|
13167
15397
|
if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
13168
|
-
const relativeToNamespaces =
|
|
15398
|
+
const relativeToNamespaces = path4.relative(namespacesRoot, resolvedPath);
|
|
13169
15399
|
const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
|
|
13170
15400
|
if (namespaceSegment) {
|
|
13171
15401
|
candidateNamespaces.add(
|
|
@@ -13210,7 +15440,7 @@ ${lines.join("\n\n")}`;
|
|
|
13210
15440
|
nsMap = buildMemoryWorthCounterMap(memories);
|
|
13211
15441
|
this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
|
|
13212
15442
|
}
|
|
13213
|
-
for (const [
|
|
15443
|
+
for (const [path5, c] of nsMap) counters.set(path5, c);
|
|
13214
15444
|
} catch (err) {
|
|
13215
15445
|
log.debug("memory-worth: failed to read namespace, skipping", {
|
|
13216
15446
|
namespace: ns,
|
|
@@ -13381,12 +15611,12 @@ ${lines.join("\n\n")}`;
|
|
|
13381
15611
|
*/
|
|
13382
15612
|
semanticDedupScopeFor(targetStorage) {
|
|
13383
15613
|
if (!this.config.namespacesEnabled) return {};
|
|
13384
|
-
const memoryDir =
|
|
13385
|
-
const storageDir =
|
|
15614
|
+
const memoryDir = path4.resolve(this.config.memoryDir);
|
|
15615
|
+
const storageDir = path4.resolve(targetStorage.dir);
|
|
13386
15616
|
if (storageDir === memoryDir) {
|
|
13387
15617
|
return { pathExcludePrefixes: ["namespaces/"] };
|
|
13388
15618
|
}
|
|
13389
|
-
let rel =
|
|
15619
|
+
let rel = path4.relative(memoryDir, storageDir);
|
|
13390
15620
|
if (!rel || rel.startsWith("..")) {
|
|
13391
15621
|
log.debug(
|
|
13392
15622
|
`semantic dedup: target storage dir ${storageDir} is outside memoryDir ${memoryDir}; scoping lookup to absolute path prefix`
|
|
@@ -13405,7 +15635,7 @@ ${lines.join("\n\n")}`;
|
|
|
13405
15635
|
if (hits.length === 0) return [];
|
|
13406
15636
|
const results = [];
|
|
13407
15637
|
for (const hit of hits) {
|
|
13408
|
-
const fullPath =
|
|
15638
|
+
const fullPath = path4.isAbsolute(hit.path) ? hit.path : path4.join(this.config.memoryDir, hit.path);
|
|
13409
15639
|
const memory = await this.storage.readMemoryByPath(fullPath);
|
|
13410
15640
|
if (!memory) continue;
|
|
13411
15641
|
results.push({
|
|
@@ -13580,7 +15810,7 @@ ${lines.join("\n\n")}`;
|
|
|
13580
15810
|
const storage = await this.storageRouter.storageFor(namespace);
|
|
13581
15811
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
13582
15812
|
if (!storageDir) continue;
|
|
13583
|
-
const recallRoot =
|
|
15813
|
+
const recallRoot = path4.resolve(storageDir);
|
|
13584
15814
|
if (seenRecallRoots.has(recallRoot)) continue;
|
|
13585
15815
|
seenRecallRoots.add(recallRoot);
|
|
13586
15816
|
recallRoots.push(recallRoot);
|
|
@@ -13604,8 +15834,8 @@ ${lines.join("\n\n")}`;
|
|
|
13604
15834
|
if (resolvedCold) scopedResults.push(resolvedCold.result);
|
|
13605
15835
|
continue;
|
|
13606
15836
|
}
|
|
13607
|
-
if (
|
|
13608
|
-
const resolvedPath =
|
|
15837
|
+
if (path4.isAbsolute(result.path)) {
|
|
15838
|
+
const resolvedPath = path4.resolve(result.path);
|
|
13609
15839
|
if (recallRoots.some(
|
|
13610
15840
|
(recallRoot) => isPathInsideStorageRoot(recallRoot, resolvedPath)
|
|
13611
15841
|
)) {
|
|
@@ -14346,13 +16576,65 @@ ${lines.join("\n\n")}`;
|
|
|
14346
16576
|
}
|
|
14347
16577
|
namespaceFromStorageDir(storageDir) {
|
|
14348
16578
|
if (!this.config.namespacesEnabled) return this.config.defaultNamespace;
|
|
14349
|
-
const resolvedStorageDir =
|
|
14350
|
-
const resolvedMemoryDir =
|
|
16579
|
+
const resolvedStorageDir = path4.resolve(storageDir);
|
|
16580
|
+
const resolvedMemoryDir = path4.resolve(this.config.memoryDir);
|
|
14351
16581
|
if (resolvedStorageDir === resolvedMemoryDir)
|
|
14352
16582
|
return this.config.defaultNamespace;
|
|
14353
16583
|
const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
|
|
14354
16584
|
if (!m?.[1]) return this.config.defaultNamespace;
|
|
14355
|
-
|
|
16585
|
+
const dirName = m[1];
|
|
16586
|
+
if (this.configuredNamespaces().includes(dirName)) {
|
|
16587
|
+
return dirName;
|
|
16588
|
+
}
|
|
16589
|
+
this.loadNamespaceStorageDirHintsFromCatalog();
|
|
16590
|
+
const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
16591
|
+
if (hintedNamespaces?.has(dirName)) {
|
|
16592
|
+
return dirName;
|
|
16593
|
+
}
|
|
16594
|
+
if (hintedNamespaces?.size === 1) {
|
|
16595
|
+
const [hintedNamespace] = hintedNamespaces;
|
|
16596
|
+
if (hintedNamespace) return hintedNamespace;
|
|
16597
|
+
}
|
|
16598
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
16599
|
+
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
16600
|
+
return decoded;
|
|
16601
|
+
}
|
|
16602
|
+
return dirName;
|
|
16603
|
+
}
|
|
16604
|
+
/**
|
|
16605
|
+
* Record a namespace write in the catalog (issue #1499). Best-effort and
|
|
16606
|
+
* failure-tolerant: a catalog write error MUST NOT crash the primary memory
|
|
16607
|
+
* write (CLAUDE.md gotcha #13, rule #40). Fire-and-forget by design.
|
|
16608
|
+
*/
|
|
16609
|
+
markCatalogWrite(namespace, storageDir) {
|
|
16610
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
16611
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
16612
|
+
void this.namespaceCatalog.markWrite(namespace, { discoveredBy: "write", storageDir }).catch(() => void 0);
|
|
16613
|
+
}
|
|
16614
|
+
/**
|
|
16615
|
+
* Public best-effort catalog write touch (issue #1499). User-facing explicit
|
|
16616
|
+
* captures (`memory_store`) and review-queue approvals persist via
|
|
16617
|
+
* `persistExplicitCapture()` → `storage.writeMemory()`, which bypasses the
|
|
16618
|
+
* extraction write path that calls `markCatalogWrite`. Without this their
|
|
16619
|
+
* namespaces never record `lastWriteAt`, so the catalog under-reports write
|
|
16620
|
+
* recency (round 5, codex P2). Fire-and-forget and failure-tolerant — a
|
|
16621
|
+
* catalog error must never affect the explicit write (gotcha #13, rule #40).
|
|
16622
|
+
*
|
|
16623
|
+
* An undefined/empty `namespace` means the write targeted the DEFAULT namespace
|
|
16624
|
+
* (`getStorage(undefined)` routes there), so we record it under the configured
|
|
16625
|
+
* default rather than skipping it (round 6, codex P2 — default `memory_store`
|
|
16626
|
+
* and inline-note writes were missing from `writtenSince`/maintenance).
|
|
16627
|
+
*/
|
|
16628
|
+
recordCatalogWrite(namespace, storageDir) {
|
|
16629
|
+
const ns = namespace && namespace.trim().length > 0 ? namespace : this.config.defaultNamespace;
|
|
16630
|
+
if (!ns) return;
|
|
16631
|
+
this.markCatalogWrite(ns, storageDir);
|
|
16632
|
+
}
|
|
16633
|
+
/** Record a namespace read in the catalog. Best-effort, failure-tolerant. */
|
|
16634
|
+
markCatalogRead(namespace, storageDir) {
|
|
16635
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
16636
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
16637
|
+
void this.namespaceCatalog.markRead(namespace, { discoveredBy: "read", storageDir }).catch(() => void 0);
|
|
14356
16638
|
}
|
|
14357
16639
|
async readAllMemoriesForNamespaces(namespaces) {
|
|
14358
16640
|
const uniq = Array.from(new Set(namespaces.filter(Boolean)));
|
|
@@ -14394,6 +16676,7 @@ export {
|
|
|
14394
16676
|
ensureBuiltInWearableConnectors,
|
|
14395
16677
|
WearablesService,
|
|
14396
16678
|
locateTranscriptPath,
|
|
16679
|
+
NamespaceCatalog,
|
|
14397
16680
|
BulkImportBatchPartialFailureError,
|
|
14398
16681
|
dedupeEntitySynthesisEvidenceEntries,
|
|
14399
16682
|
defaultWorkspaceDir,
|
|
@@ -14424,4 +16707,4 @@ export {
|
|
|
14424
16707
|
resolvePersistedMemoryRelativePath,
|
|
14425
16708
|
Orchestrator
|
|
14426
16709
|
};
|
|
14427
|
-
//# sourceMappingURL=chunk-
|
|
16710
|
+
//# sourceMappingURL=chunk-5QD3QD76.js.map
|