@remnic/core 9.3.652 → 9.3.654
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 +17 -17
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +11 -11
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +10 -10
- package/dist/access-schema.d.ts +15 -12
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-CdJFd3_b.d.ts → access-service-C8A5hoXJ.d.ts} +11 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +8 -8
- 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-KJDKZVF3.js → chunk-2DSTAWNZ.js} +3 -3
- package/dist/chunk-3RACUBII.js +212 -0
- package/dist/chunk-3RACUBII.js.map +1 -0
- package/dist/{chunk-Y7NWBBHV.js → chunk-6CVI6BP6.js} +2 -2
- package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
- package/dist/chunk-6IMKOIZ6.js.map +1 -0
- package/dist/{chunk-WTI35CVJ.js → chunk-BJA6DQOC.js} +5 -5
- package/dist/{chunk-GI45G4BK.js → chunk-BP2EV6W5.js} +3 -3
- package/dist/{chunk-WLGE6KEO.js → chunk-DBM2BD22.js} +3 -3
- package/dist/{chunk-IENGGY2C.js → chunk-ENV6RDTD.js} +2 -2
- package/dist/{chunk-BEMWL2FZ.js → chunk-FVRBLJP6.js} +2 -2
- package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
- package/dist/chunk-GKKAXVAJ.js.map +1 -0
- package/dist/{chunk-MGGNV3H2.js → chunk-GPW2E4LN.js} +23 -8
- package/dist/chunk-GPW2E4LN.js.map +1 -0
- package/dist/{chunk-KWM33SPU.js → chunk-JMQSYGXS.js} +2 -2
- package/dist/{chunk-WSFNYPAT.js → chunk-JYN7QNTA.js} +87 -18
- package/dist/chunk-JYN7QNTA.js.map +1 -0
- package/dist/{chunk-AJE7FJVE.js → chunk-K6X553JB.js} +2 -2
- package/dist/{chunk-5V3TAB7D.js → chunk-LJCEWTG3.js} +19 -8
- package/dist/{chunk-5V3TAB7D.js.map → chunk-LJCEWTG3.js.map} +1 -1
- package/dist/{chunk-YOVKPOMD.js → chunk-NAZWHTYV.js} +13 -6
- package/dist/chunk-NAZWHTYV.js.map +1 -0
- package/dist/{chunk-XMN6MMTU.js → chunk-NCGWXCSW.js} +2 -2
- package/dist/{chunk-C43KEWEV.js → chunk-NE2JBMLN.js} +1 -1
- package/dist/chunk-NE2JBMLN.js.map +1 -0
- package/dist/{chunk-TCX4WLKK.js → chunk-OL2364SB.js} +2020 -368
- package/dist/chunk-OL2364SB.js.map +1 -0
- package/dist/{chunk-JF7SFXTG.js → chunk-QKK64Z6M.js} +2 -2
- package/dist/{chunk-IVYSVAC6.js → chunk-QW6JZO5P.js} +2 -2
- package/dist/{chunk-EHQLDFSH.js → chunk-RGPUQ66K.js} +2 -2
- package/dist/{chunk-CFOCZPIQ.js → chunk-T2C6QJG2.js} +2 -2
- package/dist/{chunk-4HYSMH7D.js → chunk-UAU5U5ML.js} +3 -2
- package/dist/chunk-UAU5U5ML.js.map +1 -0
- package/dist/{chunk-V4UDXYGG.js → chunk-XWQ6ERUG.js} +2 -2
- package/dist/{chunk-IJHLC5CH.js → chunk-Y2RIIF6H.js} +32 -22
- package/dist/{chunk-IJHLC5CH.js.map → chunk-Y2RIIF6H.js.map} +1 -1
- package/dist/{chunk-C63WC454.js → chunk-YLZLPVKK.js} +22 -1
- package/dist/chunk-YLZLPVKK.js.map +1 -0
- package/dist/{chunk-RZOBQ23O.js → chunk-Z5MQI7K2.js} +2 -2
- package/dist/{chunk-PRQXUSQV.js → chunk-ZCORQM74.js} +2 -2
- package/dist/{cli-DDo7Qgs-.d.ts → cli-uQgvDFNE.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +23 -23
- 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 +31 -29
- 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 +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- 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 +7 -7
- package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-B4Y4sWQH.d.ts} +503 -3
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +13 -13
- 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 +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-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/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-BKd0Pype.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/{types-D8yUmSik.d.ts → types-BgChEr0M.d.ts} +11 -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.test.ts +70 -1
- package/src/access-mcp.ts +19 -2
- package/src/access-schema.ts +1 -0
- package/src/access-service.ts +12 -0
- package/src/briefing.test.ts +70 -0
- package/src/briefing.ts +30 -20
- package/src/cli.ts +104 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +29 -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/namespaces/catalog.test.ts +3356 -0
- package/src/namespaces/catalog.ts +2123 -0
- package/src/namespaces/storage.ts +210 -30
- package/src/orchestrator-flush.test.ts +300 -0
- package/src/orchestrator.ts +851 -240
- package/src/types.ts +11 -0
- package/dist/chunk-4HYSMH7D.js.map +0 -1
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-MGGNV3H2.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-TCX4WLKK.js.map +0 -1
- package/dist/chunk-WSFNYPAT.js.map +0 -1
- package/dist/chunk-YOVKPOMD.js.map +0 -1
- /package/dist/{chunk-KJDKZVF3.js.map → chunk-2DSTAWNZ.js.map} +0 -0
- /package/dist/{chunk-Y7NWBBHV.js.map → chunk-6CVI6BP6.js.map} +0 -0
- /package/dist/{chunk-WTI35CVJ.js.map → chunk-BJA6DQOC.js.map} +0 -0
- /package/dist/{chunk-GI45G4BK.js.map → chunk-BP2EV6W5.js.map} +0 -0
- /package/dist/{chunk-WLGE6KEO.js.map → chunk-DBM2BD22.js.map} +0 -0
- /package/dist/{chunk-IENGGY2C.js.map → chunk-ENV6RDTD.js.map} +0 -0
- /package/dist/{chunk-BEMWL2FZ.js.map → chunk-FVRBLJP6.js.map} +0 -0
- /package/dist/{chunk-KWM33SPU.js.map → chunk-JMQSYGXS.js.map} +0 -0
- /package/dist/{chunk-AJE7FJVE.js.map → chunk-K6X553JB.js.map} +0 -0
- /package/dist/{chunk-XMN6MMTU.js.map → chunk-NCGWXCSW.js.map} +0 -0
- /package/dist/{chunk-JF7SFXTG.js.map → chunk-QKK64Z6M.js.map} +0 -0
- /package/dist/{chunk-IVYSVAC6.js.map → chunk-QW6JZO5P.js.map} +0 -0
- /package/dist/{chunk-EHQLDFSH.js.map → chunk-RGPUQ66K.js.map} +0 -0
- /package/dist/{chunk-CFOCZPIQ.js.map → chunk-T2C6QJG2.js.map} +0 -0
- /package/dist/{chunk-V4UDXYGG.js.map → chunk-XWQ6ERUG.js.map} +0 -0
- /package/dist/{chunk-RZOBQ23O.js.map → chunk-Z5MQI7K2.js.map} +0 -0
- /package/dist/{chunk-PRQXUSQV.js.map → chunk-ZCORQM74.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-ZCORQM74.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-NCGWXCSW.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-T2C6QJG2.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-QKK64Z6M.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-K6X553JB.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-3RACUBII.js";
|
|
241
243
|
import {
|
|
242
244
|
isAboveImportanceThreshold,
|
|
243
245
|
scoreImportance
|
|
@@ -315,7 +317,7 @@ import {
|
|
|
315
317
|
} from "./chunk-FF4KLI5W.js";
|
|
316
318
|
import {
|
|
317
319
|
buildXraySnapshot
|
|
318
|
-
} from "./chunk-
|
|
320
|
+
} from "./chunk-FVRBLJP6.js";
|
|
319
321
|
import {
|
|
320
322
|
NamespaceSearchRouter
|
|
321
323
|
} from "./chunk-JVRPJ7D4.js";
|
|
@@ -327,7 +329,9 @@ 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
|
|
@@ -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-Y2RIIF6H.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-6CVI6BP6.js";
|
|
367
372
|
import {
|
|
368
373
|
isValidTranscriptDate,
|
|
369
374
|
loadSpeakerRegistry,
|
|
@@ -378,7 +383,7 @@ import {
|
|
|
378
383
|
} from "./chunk-J6A3CX5N.js";
|
|
379
384
|
import {
|
|
380
385
|
confidenceTier
|
|
381
|
-
} from "./chunk-
|
|
386
|
+
} from "./chunk-NE2JBMLN.js";
|
|
382
387
|
import {
|
|
383
388
|
inferMemoryStatus,
|
|
384
389
|
isActiveMemoryStatus
|
|
@@ -400,6 +405,7 @@ import {
|
|
|
400
405
|
resolvePrincipal
|
|
401
406
|
} from "./chunk-UZYLX7M6.js";
|
|
402
407
|
import {
|
|
408
|
+
isSafeRouteNamespace,
|
|
403
409
|
selectRouteRule
|
|
404
410
|
} from "./chunk-U3PN77QT.js";
|
|
405
411
|
import {
|
|
@@ -432,17 +438,17 @@ import {
|
|
|
432
438
|
} from "./chunk-AC5LO7IU.js";
|
|
433
439
|
|
|
434
440
|
// src/orchestrator.ts
|
|
435
|
-
import
|
|
441
|
+
import path3 from "path";
|
|
436
442
|
import os from "os";
|
|
437
443
|
import { createHash, randomBytes } from "crypto";
|
|
438
|
-
import { existsSync } from "fs";
|
|
444
|
+
import { existsSync, readFileSync } from "fs";
|
|
439
445
|
import {
|
|
440
|
-
mkdir as
|
|
441
|
-
readdir,
|
|
442
|
-
readFile as
|
|
443
|
-
stat,
|
|
444
|
-
unlink,
|
|
445
|
-
writeFile as
|
|
446
|
+
mkdir as mkdir3,
|
|
447
|
+
readdir as readdir2,
|
|
448
|
+
readFile as readFile3,
|
|
449
|
+
stat as stat2,
|
|
450
|
+
unlink as unlink2,
|
|
451
|
+
writeFile as writeFile3
|
|
446
452
|
} from "fs/promises";
|
|
447
453
|
|
|
448
454
|
// src/procedural/procedure-recall.ts
|
|
@@ -1484,61 +1490,1379 @@ Install it alongside Remnic:
|
|
|
1484
1490
|
stateFilePath: correctionsFilePath(storage.dir)
|
|
1485
1491
|
};
|
|
1486
1492
|
}
|
|
1487
|
-
async addCorrection(rule) {
|
|
1488
|
-
compileCorrectionRule(rule, "correction");
|
|
1489
|
-
const storage = await this.deps.getStorage();
|
|
1490
|
-
const rules = await loadCorrectionsFile(storage.dir);
|
|
1491
|
-
const duplicate = rules.some(
|
|
1492
|
-
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
1493
|
-
);
|
|
1494
|
-
if (duplicate) {
|
|
1495
|
-
throw new WearablesInputError(
|
|
1496
|
-
`an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
|
|
1497
|
-
);
|
|
1493
|
+
async addCorrection(rule) {
|
|
1494
|
+
compileCorrectionRule(rule, "correction");
|
|
1495
|
+
const storage = await this.deps.getStorage();
|
|
1496
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1497
|
+
const duplicate = rules.some(
|
|
1498
|
+
(existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
|
|
1499
|
+
);
|
|
1500
|
+
if (duplicate) {
|
|
1501
|
+
throw new WearablesInputError(
|
|
1502
|
+
`an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
rules.push(rule);
|
|
1506
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1507
|
+
}
|
|
1508
|
+
async removeCorrection(index) {
|
|
1509
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
1510
|
+
throw new WearablesInputError(`invalid correction index '${index}'`);
|
|
1511
|
+
}
|
|
1512
|
+
const storage = await this.deps.getStorage();
|
|
1513
|
+
const rules = await loadCorrectionsFile(storage.dir);
|
|
1514
|
+
if (index >= rules.length) {
|
|
1515
|
+
throw new WearablesInputError(
|
|
1516
|
+
`correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const [removed] = rules.splice(index, 1);
|
|
1520
|
+
await saveCorrectionsFile(storage.dir, rules);
|
|
1521
|
+
return removed;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
function clampLimit(value, fallback, max, label) {
|
|
1525
|
+
if (value === void 0) return fallback;
|
|
1526
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
|
|
1527
|
+
throw new WearablesInputError(
|
|
1528
|
+
`invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
return value;
|
|
1532
|
+
}
|
|
1533
|
+
function locateTranscriptPath(hitPath) {
|
|
1534
|
+
const normalized = hitPath.replace(/\\/g, "/");
|
|
1535
|
+
const match = normalized.match(
|
|
1536
|
+
/(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
|
|
1537
|
+
);
|
|
1538
|
+
if (!match) return null;
|
|
1539
|
+
if (!isValidTranscriptDate(match[2])) return null;
|
|
1540
|
+
return { source: match[1], date: match[2] };
|
|
1541
|
+
}
|
|
1542
|
+
function extractSnippet(body, index, matchLength) {
|
|
1543
|
+
const start = Math.max(0, index - 80);
|
|
1544
|
+
const end = Math.min(body.length, index + matchLength + 80);
|
|
1545
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
1546
|
+
const suffix = end < body.length ? "\u2026" : "";
|
|
1547
|
+
return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/namespaces/catalog.ts
|
|
1551
|
+
import path2 from "path";
|
|
1552
|
+
import { randomUUID } from "crypto";
|
|
1553
|
+
import {
|
|
1554
|
+
appendFile,
|
|
1555
|
+
lstat,
|
|
1556
|
+
mkdir as mkdir2,
|
|
1557
|
+
open,
|
|
1558
|
+
readdir,
|
|
1559
|
+
readFile as readFile2,
|
|
1560
|
+
realpath,
|
|
1561
|
+
rename,
|
|
1562
|
+
stat,
|
|
1563
|
+
unlink,
|
|
1564
|
+
utimes,
|
|
1565
|
+
writeFile as writeFile2
|
|
1566
|
+
} from "fs/promises";
|
|
1567
|
+
var NAMESPACE_KINDS = [
|
|
1568
|
+
"default",
|
|
1569
|
+
"self",
|
|
1570
|
+
"shared",
|
|
1571
|
+
"project",
|
|
1572
|
+
"branch",
|
|
1573
|
+
"team-project",
|
|
1574
|
+
"explicit",
|
|
1575
|
+
"legacy"
|
|
1576
|
+
];
|
|
1577
|
+
var NAMESPACE_DISCOVERY_SOURCES = [
|
|
1578
|
+
"config",
|
|
1579
|
+
"write",
|
|
1580
|
+
"read",
|
|
1581
|
+
"scan",
|
|
1582
|
+
"migration"
|
|
1583
|
+
];
|
|
1584
|
+
var CATALOG_FILE = "namespaces.jsonl";
|
|
1585
|
+
var STATE_DIR = "state";
|
|
1586
|
+
var REBUILD_LOCK_FILE = "namespaces.rebuild.lock";
|
|
1587
|
+
var REBUILD_LOCK_STALE_MS = 3e4;
|
|
1588
|
+
var REBUILD_LOCK_MAX_WAIT_MS = 5e3;
|
|
1589
|
+
var REBUILD_LOCK_POLL_MS = 50;
|
|
1590
|
+
var REBUILD_LOCK_HEARTBEAT_MS = 1e4;
|
|
1591
|
+
var MEMORY_DATA_CHILDREN = [
|
|
1592
|
+
...ALL_CATEGORY_DIRS,
|
|
1593
|
+
"entities",
|
|
1594
|
+
"artifacts",
|
|
1595
|
+
"identity",
|
|
1596
|
+
"config",
|
|
1597
|
+
"summaries",
|
|
1598
|
+
"profile.md",
|
|
1599
|
+
"state"
|
|
1600
|
+
];
|
|
1601
|
+
function isCatalogEnabled(config) {
|
|
1602
|
+
if (config.namespacesEnabled !== true) return false;
|
|
1603
|
+
return config.namespaceCatalogEnabled !== false;
|
|
1604
|
+
}
|
|
1605
|
+
var FILE_MEMORY_DATA_CHILDREN = /* @__PURE__ */ new Set(["profile.md"]);
|
|
1606
|
+
function isNotFoundError(err) {
|
|
1607
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1608
|
+
}
|
|
1609
|
+
async function inspectMemoryDataMarker(rootDir, child) {
|
|
1610
|
+
const childPath = path2.join(rootDir, child);
|
|
1611
|
+
let entry;
|
|
1612
|
+
try {
|
|
1613
|
+
entry = await lstat(childPath);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
return isNotFoundError(err) ? { state: "absent" } : { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1616
|
+
}
|
|
1617
|
+
if (entry.isSymbolicLink()) return { state: "invalid", detail: `${child}: symlink` };
|
|
1618
|
+
if (FILE_MEMORY_DATA_CHILDREN.has(child)) {
|
|
1619
|
+
return entry.isFile() ? { state: "valid" } : { state: "invalid", detail: `${child}: expected file` };
|
|
1620
|
+
}
|
|
1621
|
+
if (!entry.isDirectory()) return { state: "invalid", detail: `${child}: expected directory` };
|
|
1622
|
+
try {
|
|
1623
|
+
const rootReal = await realpath(rootDir);
|
|
1624
|
+
const childReal = await realpath(childPath);
|
|
1625
|
+
return isPathInside(rootReal, childReal) ? { state: "valid" } : { state: "invalid", detail: `${child}: escapes namespace root` };
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
return { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async function inspectMemoryDataRoot(rootDir) {
|
|
1631
|
+
let hasData = false;
|
|
1632
|
+
for (const child of MEMORY_DATA_CHILDREN) {
|
|
1633
|
+
const marker = await inspectMemoryDataMarker(rootDir, child);
|
|
1634
|
+
if (marker.state === "invalid") {
|
|
1635
|
+
return { hasData: false, invalidMarker: marker.detail };
|
|
1636
|
+
}
|
|
1637
|
+
if (marker.state === "valid") {
|
|
1638
|
+
hasData = true;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return { hasData };
|
|
1642
|
+
}
|
|
1643
|
+
async function hasMemoryData(rootDir) {
|
|
1644
|
+
return (await inspectMemoryDataRoot(rootDir)).hasData;
|
|
1645
|
+
}
|
|
1646
|
+
function isValidIsoTimestamp(value) {
|
|
1647
|
+
const ms = Date.parse(value);
|
|
1648
|
+
return Number.isFinite(ms);
|
|
1649
|
+
}
|
|
1650
|
+
function isNamespaceKind(value) {
|
|
1651
|
+
return typeof value === "string" && NAMESPACE_KINDS.includes(value);
|
|
1652
|
+
}
|
|
1653
|
+
function isNamespaceDiscoverySource(value) {
|
|
1654
|
+
return typeof value === "string" && NAMESPACE_DISCOVERY_SOURCES.includes(value);
|
|
1655
|
+
}
|
|
1656
|
+
function coerceRecord(value) {
|
|
1657
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1658
|
+
const v = value;
|
|
1659
|
+
if (typeof v.namespace !== "string") return null;
|
|
1660
|
+
const namespace = normalizeNamespaceIdentity(v.namespace);
|
|
1661
|
+
if (namespace.length === 0) return null;
|
|
1662
|
+
if (typeof v.identityToken !== "string" || v.identityToken.length === 0) return null;
|
|
1663
|
+
const expectedIdentityToken = namespaceIdentityToken(namespace);
|
|
1664
|
+
if (v.identityToken !== expectedIdentityToken) return null;
|
|
1665
|
+
if (typeof v.storageDir !== "string" || v.storageDir.length === 0) return null;
|
|
1666
|
+
if (typeof v.createdAt !== "string" || v.createdAt.length === 0) return null;
|
|
1667
|
+
if (!isValidIsoTimestamp(v.createdAt)) return null;
|
|
1668
|
+
const kind = v.kind === void 0 ? "explicit" : isNamespaceKind(v.kind) ? v.kind : null;
|
|
1669
|
+
if (!kind) return null;
|
|
1670
|
+
const discoveredBy = v.discoveredBy === void 0 ? "scan" : isNamespaceDiscoverySource(v.discoveredBy) ? v.discoveredBy : null;
|
|
1671
|
+
if (!discoveredBy) return null;
|
|
1672
|
+
const record = {
|
|
1673
|
+
namespace,
|
|
1674
|
+
identityToken: expectedIdentityToken,
|
|
1675
|
+
kind,
|
|
1676
|
+
createdAt: v.createdAt,
|
|
1677
|
+
storageDir: v.storageDir,
|
|
1678
|
+
discoveredBy
|
|
1679
|
+
};
|
|
1680
|
+
if (typeof v.principal === "string") record.principal = v.principal;
|
|
1681
|
+
if (typeof v.projectId === "string") record.projectId = v.projectId;
|
|
1682
|
+
if (typeof v.branch === "string") record.branch = v.branch;
|
|
1683
|
+
if (typeof v.parentNamespace === "string") record.parentNamespace = v.parentNamespace;
|
|
1684
|
+
if (typeof v.lastReadAt === "string" && isValidIsoTimestamp(v.lastReadAt)) {
|
|
1685
|
+
record.lastReadAt = v.lastReadAt;
|
|
1686
|
+
}
|
|
1687
|
+
if (typeof v.lastWriteAt === "string" && isValidIsoTimestamp(v.lastWriteAt)) {
|
|
1688
|
+
record.lastWriteAt = v.lastWriteAt;
|
|
1689
|
+
}
|
|
1690
|
+
if (v.lastMaintenanceAt && typeof v.lastMaintenanceAt === "object") {
|
|
1691
|
+
const out = {};
|
|
1692
|
+
for (const [k, val] of Object.entries(v.lastMaintenanceAt)) {
|
|
1693
|
+
if (typeof val === "string" && isValidIsoTimestamp(val)) out[k] = val;
|
|
1694
|
+
}
|
|
1695
|
+
if (Object.keys(out).length > 0) record.lastMaintenanceAt = out;
|
|
1696
|
+
}
|
|
1697
|
+
return record;
|
|
1698
|
+
}
|
|
1699
|
+
function laterIso(a, b) {
|
|
1700
|
+
if (!a) return b;
|
|
1701
|
+
if (!b) return a;
|
|
1702
|
+
const am = Date.parse(a);
|
|
1703
|
+
const bm = Date.parse(b);
|
|
1704
|
+
if (!Number.isFinite(am)) return b;
|
|
1705
|
+
if (!Number.isFinite(bm)) return a;
|
|
1706
|
+
return bm > am ? b : a;
|
|
1707
|
+
}
|
|
1708
|
+
function mergeNewerTouchFields(base, fresh) {
|
|
1709
|
+
const merged = { ...base };
|
|
1710
|
+
const lr = laterIso(base.lastReadAt, fresh.lastReadAt);
|
|
1711
|
+
if (lr) merged.lastReadAt = lr;
|
|
1712
|
+
const lw = laterIso(base.lastWriteAt, fresh.lastWriteAt);
|
|
1713
|
+
if (lw) merged.lastWriteAt = lw;
|
|
1714
|
+
if (base.lastMaintenanceAt || fresh.lastMaintenanceAt) {
|
|
1715
|
+
const jobs = { ...base.lastMaintenanceAt ?? {} };
|
|
1716
|
+
for (const [job, ts] of Object.entries(fresh.lastMaintenanceAt ?? {})) {
|
|
1717
|
+
const latest = laterIso(jobs[job], ts);
|
|
1718
|
+
if (latest) jobs[job] = latest;
|
|
1719
|
+
}
|
|
1720
|
+
if (Object.keys(jobs).length > 0) merged.lastMaintenanceAt = jobs;
|
|
1721
|
+
}
|
|
1722
|
+
return merged;
|
|
1723
|
+
}
|
|
1724
|
+
function serializeRecord(record) {
|
|
1725
|
+
const ordered = {};
|
|
1726
|
+
const source = record;
|
|
1727
|
+
for (const key of Object.keys(source).sort()) {
|
|
1728
|
+
const value = source[key];
|
|
1729
|
+
if (value === void 0) continue;
|
|
1730
|
+
if (key === "lastMaintenanceAt" && value && typeof value === "object") {
|
|
1731
|
+
const sortedJobs = {};
|
|
1732
|
+
for (const jobKey of Object.keys(value).sort()) {
|
|
1733
|
+
sortedJobs[jobKey] = value[jobKey];
|
|
1734
|
+
}
|
|
1735
|
+
ordered[key] = sortedJobs;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
ordered[key] = value;
|
|
1739
|
+
}
|
|
1740
|
+
return JSON.stringify(ordered);
|
|
1741
|
+
}
|
|
1742
|
+
function inferKind(namespace, config) {
|
|
1743
|
+
if (namespace === normalizeNamespaceIdentity(config.defaultNamespace)) return "default";
|
|
1744
|
+
if (namespace === normalizeNamespaceIdentity(config.sharedNamespace)) return "shared";
|
|
1745
|
+
if (config.namespacePolicies.some((p) => normalizeNamespaceIdentity(p.name) === namespace)) {
|
|
1746
|
+
return "explicit";
|
|
1747
|
+
}
|
|
1748
|
+
if (/-branch-|^project-[^-]+-branch-/.test(namespace) || namespace.includes("-branch-")) {
|
|
1749
|
+
return "branch";
|
|
1750
|
+
}
|
|
1751
|
+
if (/^team-.*-project-/.test(namespace) || /^team-.*project-/.test(namespace)) {
|
|
1752
|
+
return "team-project";
|
|
1753
|
+
}
|
|
1754
|
+
if (/^project-/.test(namespace) || /-project-/.test(namespace)) {
|
|
1755
|
+
return "project";
|
|
1756
|
+
}
|
|
1757
|
+
return "explicit";
|
|
1758
|
+
}
|
|
1759
|
+
var NamespaceCatalog = class {
|
|
1760
|
+
constructor(config) {
|
|
1761
|
+
this.config = config;
|
|
1762
|
+
this.memoryDir = config.memoryDir;
|
|
1763
|
+
this.stateDir = path2.join(this.memoryDir, STATE_DIR);
|
|
1764
|
+
this.catalogPath = path2.join(this.stateDir, CATALOG_FILE);
|
|
1765
|
+
this.rebuildLockPath = path2.join(this.stateDir, REBUILD_LOCK_FILE);
|
|
1766
|
+
this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
1767
|
+
}
|
|
1768
|
+
config;
|
|
1769
|
+
memoryDir;
|
|
1770
|
+
stateDir;
|
|
1771
|
+
catalogPath;
|
|
1772
|
+
rebuildLockPath;
|
|
1773
|
+
// Per-INSTANCE lock owner id (round 6, codex P2 — NBsGP). The rebuild lock
|
|
1774
|
+
// file records this id, not just `process.pid`, so two NamespaceCatalog
|
|
1775
|
+
// instances in the SAME process sharing a memoryDir are NOT mistaken for each
|
|
1776
|
+
// other: a touch on instance B must still wait for instance A's rebuild lock
|
|
1777
|
+
// (different owner id, same PID) instead of skipping as "self-held".
|
|
1778
|
+
lockOwnerId = randomUUID();
|
|
1779
|
+
// Serialized write chain that recovers from rejection (CLAUDE.md rule #40)
|
|
1780
|
+
// so a single failed append cannot permanently poison subsequent writes.
|
|
1781
|
+
writeChain = Promise.resolve();
|
|
1782
|
+
// Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
|
|
1783
|
+
// section, after the lock is acquired but BEFORE the read→merge→append. A
|
|
1784
|
+
// deterministic concurrency test installs a hook here to widen the (otherwise
|
|
1785
|
+
// microscopic) window and prove that a cross-process rebuild CANNOT run its
|
|
1786
|
+
// load→rename while a touch holds the lock. Never set in production code.
|
|
1787
|
+
onTouchCriticalSectionForTest;
|
|
1788
|
+
// Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock
|
|
1789
|
+
// critical section, after the final cross-process re-merge `loadCompacted()` and
|
|
1790
|
+
// BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-
|
|
1791
|
+
// append touch (the old bug) would clobber its append. A deterministic test
|
|
1792
|
+
// installs a hook here to attempt a cross-instance touch in this window and
|
|
1793
|
+
// assert the held mutex blocks it. Never set in production code.
|
|
1794
|
+
onRebuildBeforeRenameForTest;
|
|
1795
|
+
// Test-only seam (NFgCT, codex P2): fires AFTER the lockless disk scan but
|
|
1796
|
+
// BEFORE the rebuild acquires the cross-process file lock for its final
|
|
1797
|
+
// load→merge→rename window. A deterministic test installs a hook here to attempt
|
|
1798
|
+
// a cross-instance touch DURING the scan window and assert it is NOT blocked or
|
|
1799
|
+
// dropped — proving the scan no longer holds the mutex. Never set in production.
|
|
1800
|
+
onRebuildAfterScanForTest;
|
|
1801
|
+
// Test-only seam (NG7Bg, codex P2): fires inside `breakStaleRebuildLock` AFTER it
|
|
1802
|
+
// has judged the lock stale and captured its identity, but BEFORE the final
|
|
1803
|
+
// re-validation+unlink. A deterministic test installs a hook here to REPLACE the
|
|
1804
|
+
// lock file (a fresh holder created a new lock in the race window) and assert the
|
|
1805
|
+
// break is skipped — the replacement's active lock is not deleted. Never set in
|
|
1806
|
+
// production.
|
|
1807
|
+
onBeforeBreakStaleUnlinkForTest;
|
|
1808
|
+
// Normalized (trimmed) default namespace identity (NH-FH, cursor Medium).
|
|
1809
|
+
// Catalog records key namespaces by their NORMALIZED identity
|
|
1810
|
+
// (`normalizeNamespaceIdentity`), but several default-namespace exemptions and
|
|
1811
|
+
// memoryDir-ownership checks compared against the RAW `config.defaultNamespace`.
|
|
1812
|
+
// If the configured default name carries surrounding whitespace the record key
|
|
1813
|
+
// is trimmed while the comparison string is not, so the default row is
|
|
1814
|
+
// misclassified, dropped at read time, or given the wrong storage root. Compare
|
|
1815
|
+
// against this normalized form everywhere instead.
|
|
1816
|
+
defaultNamespaceIdentity;
|
|
1817
|
+
/** Whether the catalog is active (namespaces enabled and catalog not opted out). */
|
|
1818
|
+
get enabled() {
|
|
1819
|
+
return isCatalogEnabled(this.config);
|
|
1820
|
+
}
|
|
1821
|
+
// ── Public enumeration API ──────────────────────────────────────────────
|
|
1822
|
+
/**
|
|
1823
|
+
* Sanitize a record at the enumeration boundary (round 5, cursor Medium + codex
|
|
1824
|
+
* P2; round 6 — NDXHe). Reads return whatever is in `namespaces.jsonl` after
|
|
1825
|
+
* schema checks only, so a tampered or pre-fix row could surface unsafe data to
|
|
1826
|
+
* maintenance/QMD until a rewrite occurs. Two distinct defenses:
|
|
1827
|
+
*
|
|
1828
|
+
* 1. UNSAFE NAMESPACE NAME (NGZqr, codex P2): an unsafe non-default namespace
|
|
1829
|
+
* (e.g. `../evil`, a name with separators, or >64 chars) is REJECTED outright
|
|
1830
|
+
* — return `null` so the caller drops it. The disk SCAN and the hot touch
|
|
1831
|
+
* path both reject such names with the SAME default-exempt `isSafeRouteNamespace`
|
|
1832
|
+
* gate, so the read boundary MUST agree, or `listNamespaces()`/`getNamespaceRecord()`
|
|
1833
|
+
* would expose a namespace those paths reject (note `isStorageDirForNamespace`
|
|
1834
|
+
* can still build a tokenized root even for `../evil`, so storageDir sanitation
|
|
1835
|
+
* alone does not catch it). The default namespace is exempt (it may be a
|
|
1836
|
+
* non-route literal), matching every other validation site.
|
|
1837
|
+
*
|
|
1838
|
+
* 2. UNSAFE storageDir: for an otherwise-valid namespace, apply the SAME contract
|
|
1839
|
+
* as the write path — full containment (`isContainedStorageDir`: lexical +
|
|
1840
|
+
* symlink/realpath) AND namespace ownership (`isStorageDirForNamespace`). When
|
|
1841
|
+
* a record fails EITHER check we substitute the trusted resolved-and-safe root
|
|
1842
|
+
* for that namespace (rule 42: read and write stay symmetric).
|
|
1843
|
+
*/
|
|
1844
|
+
async sanitizeRecordForRead(record) {
|
|
1845
|
+
if (record.namespace !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(record.namespace)) {
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
if (await this.isContainedStorageDir(record.storageDir) && await this.isStorageDirForNamespace(record.namespace, record.storageDir)) {
|
|
1849
|
+
return record;
|
|
1850
|
+
}
|
|
1851
|
+
const safe = await this.resolveSafeStorageDir(record.namespace);
|
|
1852
|
+
return { ...record, storageDir: safe };
|
|
1853
|
+
}
|
|
1854
|
+
storageRootOwnershipRank(record, resolvedStorageDir, configured) {
|
|
1855
|
+
if (resolvedStorageDir === path2.resolve(this.memoryDir)) {
|
|
1856
|
+
return record.namespace === this.defaultNamespaceIdentity ? 0 : 3;
|
|
1857
|
+
}
|
|
1858
|
+
const leaf = path2.basename(resolvedStorageDir);
|
|
1859
|
+
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
1860
|
+
if (tokenOwnsRoot && configured.has(record.namespace)) {
|
|
1861
|
+
return 0;
|
|
1862
|
+
}
|
|
1863
|
+
if (record.namespace === leaf) return 1;
|
|
1864
|
+
if (tokenOwnsRoot) return 2;
|
|
1865
|
+
return 3;
|
|
1866
|
+
}
|
|
1867
|
+
configuredNamespaceIdentities() {
|
|
1868
|
+
return new Set(
|
|
1869
|
+
[
|
|
1870
|
+
this.config.defaultNamespace,
|
|
1871
|
+
this.config.sharedNamespace,
|
|
1872
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
1873
|
+
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
preferStorageRootOwner(current, candidate, resolvedStorageDir, configured) {
|
|
1877
|
+
const currentRank = this.storageRootOwnershipRank(current, resolvedStorageDir, configured);
|
|
1878
|
+
const candidateRank = this.storageRootOwnershipRank(candidate, resolvedStorageDir, configured);
|
|
1879
|
+
if (candidateRank < currentRank) return candidate;
|
|
1880
|
+
if (candidateRank > currentRank) return current;
|
|
1881
|
+
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
1882
|
+
if (byName < 0) return candidate;
|
|
1883
|
+
if (byName > 0) return current;
|
|
1884
|
+
return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
|
|
1885
|
+
}
|
|
1886
|
+
dropDuplicateStorageRootAliases(records) {
|
|
1887
|
+
const byStorageDir = /* @__PURE__ */ new Map();
|
|
1888
|
+
const configured = this.configuredNamespaceIdentities();
|
|
1889
|
+
for (const record of records) {
|
|
1890
|
+
const resolvedStorageDir = path2.resolve(record.storageDir);
|
|
1891
|
+
const current = byStorageDir.get(resolvedStorageDir);
|
|
1892
|
+
if (!current) {
|
|
1893
|
+
byStorageDir.set(resolvedStorageDir, record);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const owner = this.preferStorageRootOwner(current, record, resolvedStorageDir, configured);
|
|
1897
|
+
const alias = owner === current ? record : current;
|
|
1898
|
+
byStorageDir.set(resolvedStorageDir, mergeNewerTouchFields(owner, alias));
|
|
1899
|
+
}
|
|
1900
|
+
return [...byStorageDir.values()];
|
|
1901
|
+
}
|
|
1902
|
+
async loadSanitizedRecords() {
|
|
1903
|
+
const records = await this.loadCompacted();
|
|
1904
|
+
const sanitized = await Promise.all(
|
|
1905
|
+
[...records.values()].map((r) => this.sanitizeRecordForRead(r))
|
|
1906
|
+
);
|
|
1907
|
+
return this.dropDuplicateStorageRootAliases(
|
|
1908
|
+
sanitized.filter((r) => r !== null)
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
async listNamespaces(filter) {
|
|
1912
|
+
if (!this.enabled) return [];
|
|
1913
|
+
let out = await this.loadSanitizedRecords();
|
|
1914
|
+
if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);
|
|
1915
|
+
if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);
|
|
1916
|
+
if (filter?.writtenSince) {
|
|
1917
|
+
const sinceMs = filter.writtenSince.getTime();
|
|
1918
|
+
out = out.filter((r) => {
|
|
1919
|
+
if (!r.lastWriteAt) return false;
|
|
1920
|
+
const ms = Date.parse(r.lastWriteAt);
|
|
1921
|
+
return Number.isFinite(ms) && ms >= sinceMs;
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
return out.sort((a, b) => {
|
|
1925
|
+
const byName = a.namespace.localeCompare(b.namespace);
|
|
1926
|
+
if (byName !== 0) return byName;
|
|
1927
|
+
return a.identityToken.localeCompare(b.identityToken);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
async getNamespaceRecord(namespace) {
|
|
1931
|
+
if (!this.enabled) return null;
|
|
1932
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
1933
|
+
return (await this.loadSanitizedRecords()).find((record) => record.namespace === ns) ?? null;
|
|
1934
|
+
}
|
|
1935
|
+
// ── Touch API (cheap, failure-tolerant) ─────────────────────────────────
|
|
1936
|
+
async markRead(namespace, metadata) {
|
|
1937
|
+
await this.touch(namespace, "read", metadata);
|
|
1938
|
+
}
|
|
1939
|
+
async markWrite(namespace, metadata) {
|
|
1940
|
+
await this.touch(namespace, "write", metadata);
|
|
1941
|
+
}
|
|
1942
|
+
async markMaintenance(namespace, jobName, at) {
|
|
1943
|
+
if (typeof jobName !== "string" || jobName.trim().length === 0) {
|
|
1944
|
+
throw new Error("markMaintenance requires a non-empty jobName");
|
|
1945
|
+
}
|
|
1946
|
+
await this.touch(namespace, "maintenance", { at }, jobName.trim());
|
|
1947
|
+
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Register namespaces known purely from config (default, shared, explicit
|
|
1950
|
+
* policies). Source `config`. Cheap and idempotent.
|
|
1951
|
+
*/
|
|
1952
|
+
async registerConfiguredNamespaces() {
|
|
1953
|
+
if (!this.enabled) return;
|
|
1954
|
+
const names = /* @__PURE__ */ new Set([
|
|
1955
|
+
this.config.defaultNamespace,
|
|
1956
|
+
this.config.sharedNamespace,
|
|
1957
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
1958
|
+
]);
|
|
1959
|
+
for (const ns of names) {
|
|
1960
|
+
if (!ns) continue;
|
|
1961
|
+
if (normalizeNamespaceIdentity(ns) !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
try {
|
|
1965
|
+
await this.register(ns, { discoveredBy: "config" });
|
|
1966
|
+
} catch {
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Register a namespace whose storage was just resolved by the router. Used as
|
|
1972
|
+
* the router's integration hook (`discoveredBy: config`). Storage dir is
|
|
1973
|
+
* provided so we do not re-resolve it. Failure-tolerant. Returns whether the
|
|
1974
|
+
* registration actually APPENDED (round 6, codex P2 — NEFoX), so the router's
|
|
1975
|
+
* resolve-hook dedup only marks a namespace notified when it truly persisted —
|
|
1976
|
+
* a dropped append (disabled catalog or rebuild-lock-timeout drop) returns
|
|
1977
|
+
* `false` and is retried on the next resolve.
|
|
1978
|
+
*/
|
|
1979
|
+
async registerResolved(namespace, storageDir) {
|
|
1980
|
+
if (!this.enabled) return false;
|
|
1981
|
+
return this.register(namespace, { discoveredBy: "config", storageDir });
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Generic register/touch without changing read/write timestamps unless the
|
|
1985
|
+
* source implies it. Validates the namespace and resolves a storage dir.
|
|
1986
|
+
* Returns whether the touch actually appended.
|
|
1987
|
+
*/
|
|
1988
|
+
async register(namespace, metadata) {
|
|
1989
|
+
return this.touch(namespace, "register", metadata);
|
|
1990
|
+
}
|
|
1991
|
+
validateNamespace(namespace) {
|
|
1992
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
1993
|
+
if (ns.length === 0) throw new Error("empty namespace");
|
|
1994
|
+
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
1995
|
+
throw new Error(`unsafe namespace: ${ns}`);
|
|
1996
|
+
}
|
|
1997
|
+
return ns;
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Resolve the on-disk storage dir for a namespace WITHOUT trusting caller
|
|
2001
|
+
* input. The default namespace may use the legacy memoryDir root; everything
|
|
2002
|
+
* else lives under `<memoryDir>/namespaces/<token>`. Containment is enforced
|
|
2003
|
+
* by rejecting separators/parent-refs in the token.
|
|
2004
|
+
*/
|
|
2005
|
+
resolveStorageDir(namespace) {
|
|
2006
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2007
|
+
return this.memoryDir;
|
|
2008
|
+
}
|
|
2009
|
+
const token = namespaceIdentityToken(namespace);
|
|
2010
|
+
return this.namespaceTokenDir(token);
|
|
2011
|
+
}
|
|
2012
|
+
namespaceTokenDir(token) {
|
|
2013
|
+
if (token.length === 0 || token.includes("/") || token.includes("\\") || token.includes("..") || path2.isAbsolute(token)) {
|
|
2014
|
+
throw new Error(`unsafe namespace token: ${token}`);
|
|
2015
|
+
}
|
|
2016
|
+
return path2.join(this.memoryDir, "namespaces", token);
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Whether a candidate storage dir is LEXICALLY contained: it is either the
|
|
2020
|
+
* legacy default root (`memoryDir`) or a strict descendant of
|
|
2021
|
+
* `<memoryDir>/namespaces/`. The router legitimately resolves a namespace to
|
|
2022
|
+
* EITHER the tokenized dir or a legacy raw-name dir under `namespaces/`, so we
|
|
2023
|
+
* accept any contained child rather than a single exact token path. This is a
|
|
2024
|
+
* pure string check — symlink escape is checked separately via realpath.
|
|
2025
|
+
*/
|
|
2026
|
+
isLexicallyContained(candidate) {
|
|
2027
|
+
const resolved = path2.resolve(candidate);
|
|
2028
|
+
if (resolved === path2.resolve(this.memoryDir)) return true;
|
|
2029
|
+
const nsBase = path2.resolve(path2.join(this.memoryDir, "namespaces"));
|
|
2030
|
+
const rel = path2.relative(nsBase, resolved);
|
|
2031
|
+
return rel.length > 0 && !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Whether a candidate storage dir satisfies the catalog containment contract,
|
|
2035
|
+
* including SYMLINK-escape rejection (round 5, codex P2). A lexically-contained
|
|
2036
|
+
* path that is actually a symlink to an outside directory would let maintenance
|
|
2037
|
+
* or QMD follow it outside `memoryDir`. We mirror `rebuildFromDisk`'s posture:
|
|
2038
|
+
* the path must be lexically contained AND, if it exists on disk, neither the
|
|
2039
|
+
* path itself a symlink nor its realpath escaping the memory root. Non-existent
|
|
2040
|
+
* paths pass the realpath stage (nothing to follow yet) but still must be
|
|
2041
|
+
* lexically contained.
|
|
2042
|
+
*/
|
|
2043
|
+
async isContainedStorageDir(candidate) {
|
|
2044
|
+
if (!this.isLexicallyContained(candidate)) return false;
|
|
2045
|
+
if (path2.resolve(candidate) === path2.resolve(this.memoryDir)) return true;
|
|
2046
|
+
let memoryReal;
|
|
2047
|
+
try {
|
|
2048
|
+
memoryReal = await realpath(this.memoryDir);
|
|
2049
|
+
} catch {
|
|
2050
|
+
memoryReal = path2.resolve(this.memoryDir);
|
|
2051
|
+
}
|
|
2052
|
+
if (await this.hasSymlinkedAncestor(candidate)) return false;
|
|
2053
|
+
try {
|
|
2054
|
+
const stat3 = await lstat(candidate);
|
|
2055
|
+
if (stat3.isSymbolicLink()) return false;
|
|
2056
|
+
if (!stat3.isDirectory()) return false;
|
|
2057
|
+
} catch {
|
|
2058
|
+
return this.isNearestExistingAncestorContained(candidate, memoryReal);
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const real = await realpath(candidate);
|
|
2062
|
+
return isPathInside(memoryReal, real);
|
|
2063
|
+
} catch {
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Reject a candidate whose path crosses a SYMLINKED ancestor strictly between
|
|
2069
|
+
* memoryDir and the leaf (codex NVuq5). `realpath`-based containment accepts a
|
|
2070
|
+
* symlinked `<memoryDir>/namespaces` that currently resolves back inside
|
|
2071
|
+
* memoryDir, but the disk scanner rejects such a root and a later retarget would
|
|
2072
|
+
* escape the memory tree — so refuse it here too. The leaf itself is
|
|
2073
|
+
* symlink-checked by the caller; this walks only the intermediate ancestors.
|
|
2074
|
+
*/
|
|
2075
|
+
async hasSymlinkedAncestor(candidate) {
|
|
2076
|
+
const stopAt = path2.resolve(this.memoryDir);
|
|
2077
|
+
let dir = path2.dirname(path2.resolve(candidate));
|
|
2078
|
+
const root = path2.parse(dir).root;
|
|
2079
|
+
while (dir !== stopAt && dir !== root && dir !== path2.dirname(dir)) {
|
|
2080
|
+
try {
|
|
2081
|
+
if ((await lstat(dir)).isSymbolicLink()) return true;
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
dir = path2.dirname(dir);
|
|
2085
|
+
}
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Walk up from a not-yet-existing candidate to the nearest ancestor that exists
|
|
2090
|
+
* on disk and verify its realpath stays inside `memoryReal` (round 6, codex P2
|
|
2091
|
+
* — NDo79). Rejects a non-existent leaf whose existing parent chain escapes
|
|
2092
|
+
* memoryDir via a symlink. Stops at memoryDir's resolved root.
|
|
2093
|
+
*
|
|
2094
|
+
* The nearest existing ancestor must also be a DIRECTORY (NHIdt, codex P2): if
|
|
2095
|
+
* an existing parent such as `<memoryDir>/namespaces` is a regular FILE (or
|
|
2096
|
+
* socket/fifo), `realpath(parent)` still succeeds and resolves inside memoryDir,
|
|
2097
|
+
* so a containment-only check would ACCEPT a leaf that can never be created — you
|
|
2098
|
+
* cannot mkdir a child under a file. We `lstat` the nearest existing ancestor and
|
|
2099
|
+
* reject when it is not a directory, mirroring the leaf non-directory rejection
|
|
2100
|
+
* (NF21i) and the disk scan, so every containment consumer agrees.
|
|
2101
|
+
*/
|
|
2102
|
+
async isNearestExistingAncestorContained(candidate, memoryReal) {
|
|
2103
|
+
let dir = path2.resolve(candidate);
|
|
2104
|
+
const root = path2.parse(dir).root;
|
|
2105
|
+
for (; ; ) {
|
|
2106
|
+
const parent = path2.dirname(dir);
|
|
2107
|
+
if (parent === dir || dir === root) return false;
|
|
2108
|
+
let real;
|
|
2109
|
+
try {
|
|
2110
|
+
real = await realpath(parent);
|
|
2111
|
+
} catch {
|
|
2112
|
+
dir = parent;
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (!(isPathInside(memoryReal, real) || real === memoryReal)) return false;
|
|
2116
|
+
try {
|
|
2117
|
+
const stat3 = await lstat(real);
|
|
2118
|
+
return stat3.isDirectory();
|
|
2119
|
+
} catch {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Resolve the storage dir to persist for a touch, validating any caller-
|
|
2126
|
+
* provided `metadata.storageDir` against the catalog containment contract
|
|
2127
|
+
* (round 4 + round 5, codex P2). `markWrite`/`registerResolved` accept an
|
|
2128
|
+
* explicit storageDir, but persisting it verbatim would let a bad hook or
|
|
2129
|
+
* external consumer write an arbitrary path — including one outside `memoryDir`
|
|
2130
|
+
* or a symlink that escapes it — into the catalog, handing maintenance/QMD an
|
|
2131
|
+
* unsafe root. We accept an explicit (or previously-stored) dir ONLY when it
|
|
2132
|
+
* stays contained under memoryDir (lexically AND via realpath); otherwise we
|
|
2133
|
+
* drop it and fall back to the trusted resolved dir.
|
|
2134
|
+
*/
|
|
2135
|
+
async resolveTouchStorageDir(namespace, explicit, existingDir) {
|
|
2136
|
+
if (explicit !== void 0 && await this.isContainedStorageDir(explicit) && await this.isStorageDirForNamespace(namespace, explicit)) {
|
|
2137
|
+
return explicit;
|
|
2138
|
+
}
|
|
2139
|
+
if (existingDir !== void 0 && await this.isContainedStorageDir(existingDir) && await this.isStorageDirForNamespace(namespace, existingDir)) {
|
|
2140
|
+
return existingDir;
|
|
2141
|
+
}
|
|
2142
|
+
return this.resolveSafeStorageDir(namespace);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Whether `candidate` is a legitimate storage root FOR `namespace` (round 6,
|
|
2146
|
+
* codex P2 — NDATT). Accepts the namespace's router-resolved root, its canonical
|
|
2147
|
+
* lexical tokenized dir, and (for the default namespace only) memoryDir. This
|
|
2148
|
+
* prevents a contained-but-CROSS-NAMESPACE path — another namespace's tree, or
|
|
2149
|
+
* memoryDir for a non-default namespace — from being persisted as this
|
|
2150
|
+
* namespace's root. Compared on resolved (absolute) paths.
|
|
2151
|
+
*/
|
|
2152
|
+
async isStorageDirForNamespace(namespace, candidate) {
|
|
2153
|
+
const resolvedCandidate = path2.resolve(candidate);
|
|
2154
|
+
const valid = /* @__PURE__ */ new Set();
|
|
2155
|
+
try {
|
|
2156
|
+
valid.add(path2.resolve(this.namespaceTokenDir(namespaceIdentityToken(namespace))));
|
|
2157
|
+
} catch {
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
valid.add(path2.resolve(this.namespaceTokenDir(namespace)));
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
valid.add(path2.resolve(await resolveNamespaceStorageRoot(this.config, namespace)));
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
2167
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
2168
|
+
valid.add(path2.resolve(this.memoryDir));
|
|
2169
|
+
try {
|
|
2170
|
+
valid.add(path2.resolve(await resolveDefaultNamespaceRoot(this.config)));
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return valid.has(resolvedCandidate);
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Resolve the canonical storage dir for a namespace as the LIVE ROUTER would,
|
|
2178
|
+
* but NEVER return a path that escapes the memory root.
|
|
2179
|
+
*
|
|
2180
|
+
* Router alignment (round 4, cursor Medium): a read/register touch with no
|
|
2181
|
+
* explicit storageDir previously used the lexical `resolveStorageDir`, which
|
|
2182
|
+
* always picks `<memoryDir>/namespaces/<token>` (or `memoryDir` for the
|
|
2183
|
+
* default). That diverges from `NamespaceStorageRouter`, which can route to a
|
|
2184
|
+
* legacy raw-name dir or a migrated default root — so a recall touch could
|
|
2185
|
+
* record a contained-but-WRONG root that maintenance/rebuild then targets. We
|
|
2186
|
+
* now delegate to the shared `resolveNamespaceStorageRoot` (the very helper the
|
|
2187
|
+
* router uses) so the catalog records the same on-disk root the router serves.
|
|
2188
|
+
*
|
|
2189
|
+
* Containment (round 5, codex P2): the resolved path can still be a symlink
|
|
2190
|
+
* escaping memoryDir, so we run the full (lexical + realpath) containment
|
|
2191
|
+
* contract. When it FAILS we fall back to a NAMESPACE-SPECIFIC safe root, NOT
|
|
2192
|
+
* a blanket `memoryDir`. Recording `memoryDir` for a non-default namespace
|
|
2193
|
+
* would point enumeration/maintenance at the DEFAULT namespace's tree (round 5,
|
|
2194
|
+
* cursor/codex Medium/P2) — a cross-namespace fanout error. The correct safe
|
|
2195
|
+
* root is the namespace's own lexical tokenized dir
|
|
2196
|
+
* (`<memoryDir>/namespaces/<token>`), which is always contained and is that
|
|
2197
|
+
* namespace's canonical location (we record the lexical PATH as metadata; we do
|
|
2198
|
+
* not follow the escaping symlink). Only the default namespace — or a token so
|
|
2199
|
+
* unsafe even the lexical dir cannot be built — falls back to `memoryDir`.
|
|
2200
|
+
*/
|
|
2201
|
+
async resolveSafeStorageDir(namespace) {
|
|
2202
|
+
let resolved;
|
|
2203
|
+
try {
|
|
2204
|
+
resolved = await resolveNamespaceStorageRoot(this.config, namespace);
|
|
2205
|
+
} catch {
|
|
2206
|
+
return this.safeFallbackStorageDir(namespace);
|
|
2207
|
+
}
|
|
2208
|
+
if (await this.isContainedStorageDir(resolved)) return resolved;
|
|
2209
|
+
return this.safeFallbackStorageDir(namespace);
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* The namespace-specific contained fallback root, used when the router-resolved
|
|
2213
|
+
* root fails containment (round 5, cursor/codex Medium/P2).
|
|
2214
|
+
*
|
|
2215
|
+
* Preference order:
|
|
2216
|
+
* 1. The namespace's OWN lexical tokenized dir (`namespaces/<token>`) — so a
|
|
2217
|
+
* non-default namespace is NOT pointed at the DEFAULT namespace's `memoryDir`
|
|
2218
|
+
* tree (which would misdirect maintenance fanout). Returned only when the
|
|
2219
|
+
* token dir itself stays CONTAINED (it is not a symlink, and its realpath
|
|
2220
|
+
* does not escape memoryDir — e.g. via a symlinked `namespaces/` parent).
|
|
2221
|
+
* 2. `memoryDir` as a LAST resort — for the default namespace, an unsafe token
|
|
2222
|
+
* that cannot build a contained path, OR the irreparable case where the
|
|
2223
|
+
* token dir's realpath escapes the root (so even its lexical path resolves
|
|
2224
|
+
* outside). NF21m note (codex P2): we deliberately do NOT record the lexical
|
|
2225
|
+
* token dir in that irreparable case — its realpath escapes memoryDir, and
|
|
2226
|
+
* the NDo79 contract REQUIRES that an escaping path is never persisted (a
|
|
2227
|
+
* later mkdir/maintenance/QMD op would follow it outside the root). Since no
|
|
2228
|
+
* contained namespace-specific path exists, containment wins: `memoryDir` is
|
|
2229
|
+
* the only safe root left. A namespace whose token dir's realpath escapes is
|
|
2230
|
+
* an irreparable on-disk state; recording the contained default root is
|
|
2231
|
+
* strictly safer than persisting an escaping one. The common case where the
|
|
2232
|
+
* token dir IS contained is handled by branch 1, so a healthy non-default
|
|
2233
|
+
* namespace never reaches `memoryDir`.
|
|
2234
|
+
*/
|
|
2235
|
+
async safeFallbackStorageDir(namespace) {
|
|
2236
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) return this.memoryDir;
|
|
2237
|
+
let tokenDir;
|
|
2238
|
+
try {
|
|
2239
|
+
tokenDir = this.namespaceTokenDir(namespaceIdentityToken(namespace));
|
|
2240
|
+
} catch {
|
|
2241
|
+
return this.memoryDir;
|
|
2242
|
+
}
|
|
2243
|
+
if (await this.isContainedStorageDir(tokenDir)) return tokenDir;
|
|
2244
|
+
return this.memoryDir;
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Re-check, NOW, whether a namespace's storage root currently EXISTS on disk
|
|
2248
|
+
* with the SAME safety the directory scan uses (NFJV8, codex P2).
|
|
2249
|
+
*
|
|
2250
|
+
* The rebuild's final re-merge runs under the held lock and folds the freshly
|
|
2251
|
+
* re-read log (`latest`) into the scanned `rebuilt` set. A namespace present in
|
|
2252
|
+
* `latest` (a live touch row) but ABSENT from `rebuilt` is normally PURGED as
|
|
2253
|
+
* deleted (the NATqU "disk scan is authoritative" rule). But there is a TOCTOU
|
|
2254
|
+
* window: a dynamic namespace can be CREATED on disk AFTER `rebuildFromDisk()`
|
|
2255
|
+
* already enumerated `namespaces/` but BEFORE this re-merge. The scan snapshot
|
|
2256
|
+
* missed its new root, yet a gateway `markWrite` already appended a row for it.
|
|
2257
|
+
* Blindly purging that row would rewrite the catalog WITHOUT a live namespace
|
|
2258
|
+
* that now has data on disk, so `writtenSince`/maintenance/QMD consumers miss
|
|
2259
|
+
* it until another touch or rebuild.
|
|
2260
|
+
*
|
|
2261
|
+
* So before purging, we re-resolve the namespace's safe storage root (the same
|
|
2262
|
+
* router-aligned, containment-checked path the scan would have catalogued) and
|
|
2263
|
+
* confirm it is a real, contained, non-symlink directory that actually holds
|
|
2264
|
+
* memory data RIGHT NOW. If so the namespace was created-after-scan and is LIVE
|
|
2265
|
+
* — KEEP its row. This is the precise inverse of NATqU and does NOT reintroduce
|
|
2266
|
+
* it: a touch on a REMOVED root re-checks as ABSENT (no data on disk) and is
|
|
2267
|
+
* still purged; only a root that EXISTS on a fresh re-check is kept.
|
|
2268
|
+
*
|
|
2269
|
+
* Mirrors the per-entry scan checks (symlink rejection + realpath containment +
|
|
2270
|
+
* `hasMemoryData`) so a symlinked/escaping root is never resurrected.
|
|
2271
|
+
*/
|
|
2272
|
+
async liveStorageRootExistsForRebuild(namespace, memoryReal) {
|
|
2273
|
+
let root;
|
|
2274
|
+
try {
|
|
2275
|
+
root = await this.resolveSafeStorageDir(namespace);
|
|
2276
|
+
} catch {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
if (normalizeNamespaceIdentity(namespace) !== this.defaultNamespaceIdentity && path2.resolve(root) === path2.resolve(this.memoryDir)) {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
let stat3;
|
|
2283
|
+
try {
|
|
2284
|
+
stat3 = await lstat(root);
|
|
2285
|
+
} catch {
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
if (stat3.isSymbolicLink()) return false;
|
|
2289
|
+
if (!stat3.isDirectory()) return false;
|
|
2290
|
+
try {
|
|
2291
|
+
const real = await realpath(root);
|
|
2292
|
+
if (memoryReal && !isPathInside(memoryReal, real)) return false;
|
|
2293
|
+
} catch {
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
return hasMemoryData(root);
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Record a namespace touch. Returns whether the touch actually APPENDED to the
|
|
2300
|
+
* log (round 6, codex P2 — NEFoX): a disabled catalog or a dropped append (the
|
|
2301
|
+
* NAUf7 rebuild-lock-timeout drop) returns `false`, so callers (e.g. the router
|
|
2302
|
+
* resolve-hook dedup) can avoid marking a dropped registration as completed and
|
|
2303
|
+
* suppressing its retry.
|
|
2304
|
+
*/
|
|
2305
|
+
async touch(namespace, kind, metadata, jobName) {
|
|
2306
|
+
if (!this.enabled) return false;
|
|
2307
|
+
const ns = this.validateNamespace(namespace);
|
|
2308
|
+
const nowIso = (metadata?.at ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2309
|
+
return this.queueCritical(
|
|
2310
|
+
async () => this.withHeldCatalogLock(async (acquired) => {
|
|
2311
|
+
if (!acquired) return false;
|
|
2312
|
+
if (this.onTouchCriticalSectionForTest) {
|
|
2313
|
+
await this.onTouchCriticalSectionForTest();
|
|
2314
|
+
}
|
|
2315
|
+
const records = await this.loadCompacted();
|
|
2316
|
+
const existing = records.get(ns);
|
|
2317
|
+
const storageDir = await this.resolveTouchStorageDir(
|
|
2318
|
+
ns,
|
|
2319
|
+
metadata?.storageDir,
|
|
2320
|
+
existing?.storageDir
|
|
2321
|
+
);
|
|
2322
|
+
const record = existing ? { ...existing } : {
|
|
2323
|
+
namespace: ns,
|
|
2324
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2325
|
+
kind: metadata?.kind ?? inferKind(ns, this.config),
|
|
2326
|
+
createdAt: nowIso,
|
|
2327
|
+
storageDir,
|
|
2328
|
+
discoveredBy: metadata?.discoveredBy ?? (kind === "register" ? "config" : kind === "maintenance" ? "scan" : kind)
|
|
2329
|
+
};
|
|
2330
|
+
record.storageDir = storageDir;
|
|
2331
|
+
if (metadata?.kind) record.kind = metadata.kind;
|
|
2332
|
+
if (metadata?.principal !== void 0) record.principal = metadata.principal;
|
|
2333
|
+
if (metadata?.projectId !== void 0) record.projectId = metadata.projectId;
|
|
2334
|
+
if (metadata?.branch !== void 0) record.branch = metadata.branch;
|
|
2335
|
+
if (metadata?.parentNamespace !== void 0)
|
|
2336
|
+
record.parentNamespace = metadata.parentNamespace;
|
|
2337
|
+
if (kind === "write" && existing && record.discoveredBy === "config") {
|
|
2338
|
+
record.discoveredBy = "write";
|
|
2339
|
+
}
|
|
2340
|
+
if (kind === "read") record.lastReadAt = nowIso;
|
|
2341
|
+
if (kind === "write") record.lastWriteAt = nowIso;
|
|
2342
|
+
if (kind === "maintenance" && jobName) {
|
|
2343
|
+
record.lastMaintenanceAt = { ...record.lastMaintenanceAt ?? {}, [jobName]: nowIso };
|
|
2344
|
+
}
|
|
2345
|
+
await this.appendUnchained(record);
|
|
2346
|
+
return true;
|
|
2347
|
+
})
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
// ── Rebuild from disk ────────────────────────────────────────────────────
|
|
2351
|
+
async rebuildFromDisk(options) {
|
|
2352
|
+
const dryRun = options?.dryRun === true;
|
|
2353
|
+
if (!this.enabled) {
|
|
2354
|
+
return { dryRun, records: [], skipped: [], applied: false };
|
|
2355
|
+
}
|
|
2356
|
+
if (dryRun) {
|
|
2357
|
+
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, false));
|
|
2358
|
+
}
|
|
2359
|
+
return this.queueCritical(async () => this.rebuildInsideChain(dryRun, true));
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Body of `rebuildFromDisk`, run inside a single `queueCritical` turn. MUST
|
|
2363
|
+
* only be invoked from within the serialized chain so the load and the
|
|
2364
|
+
* rewrite are atomic with respect to concurrent touches (in-process).
|
|
2365
|
+
*
|
|
2366
|
+
* `wantMutate` is true for an `--apply` (the caller intends to rewrite). The
|
|
2367
|
+
* cross-process file lock is acquired LATE — only around the final
|
|
2368
|
+
* load→merge→rename window (NFgCT, codex P2) — never across the disk scan, so a
|
|
2369
|
+
* long scan does not force concurrent gateway touches to wait (and drop their
|
|
2370
|
+
* append). Whether the rewrite actually happened is reported via the result's
|
|
2371
|
+
* `applied`: true only when `wantMutate` AND the lock was acquired.
|
|
2372
|
+
*/
|
|
2373
|
+
async rebuildInsideChain(dryRun, wantMutate) {
|
|
2374
|
+
const existing = await this.loadCompacted();
|
|
2375
|
+
const skipped = [];
|
|
2376
|
+
const rebuilt = /* @__PURE__ */ new Map();
|
|
2377
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2378
|
+
let memoryReal = null;
|
|
2379
|
+
try {
|
|
2380
|
+
memoryReal = await realpath(this.memoryDir);
|
|
2381
|
+
} catch {
|
|
2382
|
+
memoryReal = this.memoryDir;
|
|
2383
|
+
}
|
|
2384
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
2385
|
+
const configured = new Set(
|
|
2386
|
+
[
|
|
2387
|
+
this.config.defaultNamespace,
|
|
2388
|
+
this.config.sharedNamespace,
|
|
2389
|
+
...this.config.namespacePolicies.map((p) => p.name)
|
|
2390
|
+
].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
|
|
2391
|
+
);
|
|
2392
|
+
const resolvedDefaultRoot = await resolveDefaultNamespaceRoot(this.config);
|
|
2393
|
+
const defaultStorageDir = await this.isContainedStorageDir(resolvedDefaultRoot) ? resolvedDefaultRoot : this.memoryDir;
|
|
2394
|
+
const legacyDefaultHasData = defaultStorageDir === this.memoryDir;
|
|
2395
|
+
for (const ns of configured) {
|
|
2396
|
+
if (!ns) continue;
|
|
2397
|
+
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) {
|
|
2398
|
+
let token;
|
|
2399
|
+
try {
|
|
2400
|
+
token = namespaceIdentityToken(ns);
|
|
2401
|
+
} catch {
|
|
2402
|
+
token = ns;
|
|
2403
|
+
}
|
|
2404
|
+
skipped.push({ token, reason: "unsafe", detail: ns });
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
let storageDir;
|
|
2408
|
+
if (ns === defaultNs) {
|
|
2409
|
+
storageDir = defaultStorageDir;
|
|
2410
|
+
} else {
|
|
2411
|
+
try {
|
|
2412
|
+
storageDir = await resolveNamespaceStorageRoot(this.config, ns);
|
|
2413
|
+
} catch {
|
|
2414
|
+
storageDir = this.namespaceTokenDir(namespaceIdentityToken(ns));
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
if (!await this.isContainedStorageDir(storageDir)) {
|
|
2418
|
+
if (ns === defaultNs) {
|
|
2419
|
+
storageDir = this.memoryDir;
|
|
2420
|
+
} else {
|
|
2421
|
+
skipped.push({ token: namespaceIdentityToken(ns), reason: "escape", detail: storageDir });
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
rebuilt.set(
|
|
2426
|
+
ns,
|
|
2427
|
+
this.mergeForRebuild(existing.get(ns), {
|
|
2428
|
+
namespace: ns,
|
|
2429
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2430
|
+
kind: inferKind(ns, this.config),
|
|
2431
|
+
createdAt: existing.get(ns)?.createdAt ?? nowIso,
|
|
2432
|
+
storageDir,
|
|
2433
|
+
discoveredBy: "config"
|
|
2434
|
+
})
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
const namespacesDir = path2.join(this.memoryDir, "namespaces");
|
|
2438
|
+
let entries = [];
|
|
2439
|
+
let namespacesDirSafe = true;
|
|
2440
|
+
try {
|
|
2441
|
+
const rootStat = await lstat(namespacesDir);
|
|
2442
|
+
if (rootStat.isSymbolicLink()) {
|
|
2443
|
+
namespacesDirSafe = false;
|
|
2444
|
+
} else {
|
|
2445
|
+
const realNamespacesDir = await realpath(namespacesDir);
|
|
2446
|
+
if (memoryReal && !isPathInside(memoryReal, realNamespacesDir)) {
|
|
2447
|
+
namespacesDirSafe = false;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
} catch {
|
|
2451
|
+
namespacesDirSafe = true;
|
|
2452
|
+
}
|
|
2453
|
+
if (!namespacesDirSafe) {
|
|
2454
|
+
skipped.push({ token: "namespaces", reason: "symlink", detail: namespacesDir });
|
|
2455
|
+
} else {
|
|
2456
|
+
try {
|
|
2457
|
+
entries = await readdir(namespacesDir, { withFileTypes: true });
|
|
2458
|
+
} catch {
|
|
2459
|
+
entries = [];
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
const scannedFromTokenized = /* @__PURE__ */ new Set();
|
|
2463
|
+
for (const entry of entries) {
|
|
2464
|
+
const token = entry.name;
|
|
2465
|
+
const fullPath = path2.join(namespacesDir, token);
|
|
2466
|
+
let stat3;
|
|
2467
|
+
try {
|
|
2468
|
+
stat3 = await lstat(fullPath);
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2473
|
+
if (stat3.isSymbolicLink()) {
|
|
2474
|
+
skipped.push({ token, reason: "symlink", detail: fullPath });
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
if (!stat3.isDirectory()) continue;
|
|
2478
|
+
try {
|
|
2479
|
+
const real = await realpath(fullPath);
|
|
2480
|
+
if (memoryReal && !isPathInside(memoryReal, real)) {
|
|
2481
|
+
skipped.push({ token, reason: "escape", detail: real });
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
const literalRecord = existing.get(token);
|
|
2489
|
+
const literalOwnsRoot = configured.has(token) || literalRecord !== void 0 && path2.resolve(literalRecord.storageDir) === path2.resolve(fullPath);
|
|
2490
|
+
const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);
|
|
2491
|
+
const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;
|
|
2492
|
+
const decoded = normalizeNamespaceIdentity(rawDecoded);
|
|
2493
|
+
if (decoded.length === 0 || rawDecoded !== decoded) {
|
|
2494
|
+
skipped.push({ token, reason: "unsafe", detail: rawDecoded });
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
if (decoded !== defaultNs && !isSafeRouteNamespace(decoded)) {
|
|
2498
|
+
skipped.push({ token, reason: "unsafe", detail: decoded });
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
const memoryData = await inspectMemoryDataRoot(fullPath);
|
|
2502
|
+
if (memoryData.invalidMarker) {
|
|
2503
|
+
skipped.push({
|
|
2504
|
+
token,
|
|
2505
|
+
reason: "unsafe",
|
|
2506
|
+
detail: `invalid memory marker: ${memoryData.invalidMarker}`
|
|
2507
|
+
});
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
if (!memoryData.hasData) continue;
|
|
2511
|
+
if (decoded === defaultNs) {
|
|
2512
|
+
const def = rebuilt.get(defaultNs);
|
|
2513
|
+
if (def) {
|
|
2514
|
+
def.storageDir = defaultStorageDir;
|
|
2515
|
+
def.kind = "default";
|
|
2516
|
+
}
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
const isTokenizedEntry = token === namespaceIdentityToken(decoded);
|
|
2520
|
+
if (rebuilt.has(decoded) && scannedFromTokenized.has(decoded) && !isTokenizedEntry) {
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (isTokenizedEntry) scannedFromTokenized.add(decoded);
|
|
2524
|
+
const prior = existing.get(decoded);
|
|
2525
|
+
rebuilt.set(
|
|
2526
|
+
decoded,
|
|
2527
|
+
this.mergeForRebuild(prior, {
|
|
2528
|
+
namespace: decoded,
|
|
2529
|
+
identityToken: namespaceIdentityToken(decoded),
|
|
2530
|
+
kind: inferKind(decoded, this.config),
|
|
2531
|
+
createdAt: prior?.createdAt ?? nowIso,
|
|
2532
|
+
storageDir: fullPath,
|
|
2533
|
+
// Configured-and-present namespaces keep config provenance; purely
|
|
2534
|
+
// discovered ones are scan.
|
|
2535
|
+
discoveredBy: configured.has(decoded) ? "config" : prior?.discoveredBy ?? "scan"
|
|
2536
|
+
})
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
if (legacyDefaultHasData && defaultStorageDir === this.memoryDir) {
|
|
2540
|
+
const def = rebuilt.get(defaultNs);
|
|
2541
|
+
if (def) def.kind = "default";
|
|
2542
|
+
}
|
|
2543
|
+
if (!wantMutate) {
|
|
2544
|
+
return this.finishRebuild(rebuilt, skipped, dryRun, false, memoryReal, nowIso);
|
|
2545
|
+
}
|
|
2546
|
+
if (this.onRebuildAfterScanForTest) {
|
|
2547
|
+
await this.onRebuildAfterScanForTest();
|
|
2548
|
+
}
|
|
2549
|
+
return this.withHeldCatalogLock(
|
|
2550
|
+
(acquired) => this.finishRebuild(rebuilt, skipped, dryRun, acquired, memoryReal, nowIso)
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Final load→merge→rename window of a rebuild, factored out so the caller can
|
|
2555
|
+
* run it WITHIN the cross-process file lock (NFgCT, codex P2) without holding
|
|
2556
|
+
* that lock across the preceding disk scan. Re-reads the latest on-disk state,
|
|
2557
|
+
* folds concurrent touches, then (when `canMutate`) atomically rewrites the log.
|
|
2558
|
+
*
|
|
2559
|
+
* `canMutate` records that the cross-process lock was actually held. The
|
|
2560
|
+
* re-merge + rewrite run only when it is true — a dry-run, or an unlocked apply
|
|
2561
|
+
* (lock-acquisition timeout), computes records but does NOT rename, so it can
|
|
2562
|
+
* never clobber a concurrent lock holder's window. `applied` mirrors `canMutate`.
|
|
2563
|
+
*/
|
|
2564
|
+
async finishRebuild(rebuilt, skipped, dryRun, canMutate, memoryReal, nowIso) {
|
|
2565
|
+
if (canMutate) {
|
|
2566
|
+
const latest = await this.loadCompacted();
|
|
2567
|
+
for (const [ns, fresh] of latest) {
|
|
2568
|
+
const current = rebuilt.get(ns);
|
|
2569
|
+
if (!current) {
|
|
2570
|
+
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
if (await this.liveStorageRootExistsForRebuild(ns, memoryReal)) {
|
|
2574
|
+
const safeDir = await this.resolveSafeStorageDir(ns);
|
|
2575
|
+
const resolvedSafe = path2.resolve(safeDir);
|
|
2576
|
+
let owningNamespace = null;
|
|
2577
|
+
for (const [otherNs, otherRec] of rebuilt) {
|
|
2578
|
+
if (otherNs !== ns && path2.resolve(otherRec.storageDir) === resolvedSafe) {
|
|
2579
|
+
owningNamespace = otherNs;
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (owningNamespace) {
|
|
2584
|
+
const owner = rebuilt.get(owningNamespace);
|
|
2585
|
+
if (owner) rebuilt.set(owningNamespace, mergeNewerTouchFields(owner, fresh));
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
rebuilt.set(ns, {
|
|
2589
|
+
...fresh,
|
|
2590
|
+
storageDir: safeDir,
|
|
2591
|
+
identityToken: namespaceIdentityToken(ns),
|
|
2592
|
+
kind: fresh.kind ?? inferKind(ns, this.config),
|
|
2593
|
+
createdAt: fresh.createdAt ?? nowIso
|
|
2594
|
+
});
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
rebuilt.set(ns, mergeNewerTouchFields(current, fresh));
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const records = [...rebuilt.values()].sort((a, b) => {
|
|
2603
|
+
const byName = a.namespace.localeCompare(b.namespace);
|
|
2604
|
+
if (byName !== 0) return byName;
|
|
2605
|
+
return a.identityToken.localeCompare(b.identityToken);
|
|
2606
|
+
});
|
|
2607
|
+
if (canMutate) {
|
|
2608
|
+
if (this.onRebuildBeforeRenameForTest) {
|
|
2609
|
+
await this.onRebuildBeforeRenameForTest();
|
|
2610
|
+
}
|
|
2611
|
+
await this.rewriteUnchained(records);
|
|
2612
|
+
}
|
|
2613
|
+
return { dryRun, records, skipped, applied: canMutate };
|
|
2614
|
+
}
|
|
2615
|
+
// ── Cross-process catalog write lock (held mutex) ────────────────────────
|
|
2616
|
+
/**
|
|
2617
|
+
* Run `fn` while HOLDING the shared cross-process advisory lock (round 5, codex
|
|
2618
|
+
* P2; generalized round 7 — NEZkA). This is the SINGLE mutex shared by BOTH the
|
|
2619
|
+
* touch read→merge→append window AND the rebuild final load→merge→rename window,
|
|
2620
|
+
* so a touch and a rebuild in different processes are mutually exclusive over
|
|
2621
|
+
* their respective critical sections — closing the check-then-append gap where a
|
|
2622
|
+
* polled-only touch could append into a rebuild's load→rename window.
|
|
2623
|
+
*
|
|
2624
|
+
* Acquisition is atomic via `open(..., "wx")`. A lock older than
|
|
2625
|
+
* `REBUILD_LOCK_STALE_MS` is treated as a crashed holder and broken. After
|
|
2626
|
+
* `REBUILD_LOCK_MAX_WAIT_MS` of contention we proceed best-effort WITHOUT the
|
|
2627
|
+
* lock rather than block forever. The lock is always released in `finally`.
|
|
2628
|
+
*
|
|
2629
|
+
* IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the
|
|
2630
|
+
* per-process `queueCritical` chain, which serializes all catalog mutations in
|
|
2631
|
+
* THIS process. So within one process only one logical holder attempts OS-lock
|
|
2632
|
+
* acquisition at a time — the file lock is never self-contended in-process, and
|
|
2633
|
+
* the lock is acquired and released within a single in-process turn. The file
|
|
2634
|
+
* lock adds only the missing CROSS-process exclusion.
|
|
2635
|
+
*
|
|
2636
|
+
* HEARTBEAT (round 5, cursor/codex Medium/P2): while WE hold the lock a timer
|
|
2637
|
+
* refreshes its mtime every `REBUILD_LOCK_HEARTBEAT_MS`, so a legitimately long
|
|
2638
|
+
* holder (> `REBUILD_LOCK_STALE_MS`) is not treated as a crashed holder and
|
|
2639
|
+
* unlinked by another process — which would let overlapping windows lose
|
|
2640
|
+
* appends. Heartbeat failures are swallowed; the timer is always cleared in
|
|
2641
|
+
* `finally`.
|
|
2642
|
+
*
|
|
2643
|
+
* ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE
|
|
2644
|
+
* actually hold the lock. When acquisition TIMED OUT (another holder is active),
|
|
2645
|
+
* a MUTATING rebuild must NOT perform its load/rename window unlocked, and a
|
|
2646
|
+
* touch must NOT append unlocked — both would recreate the lost-append race. The
|
|
2647
|
+
* caller uses `acquired` to run compute-only (rebuild) or DROP the append
|
|
2648
|
+
* (touch) when unlocked.
|
|
2649
|
+
*/
|
|
2650
|
+
async withHeldCatalogLock(fn) {
|
|
2651
|
+
const acquired = await this.acquireRebuildLock();
|
|
2652
|
+
let heartbeat;
|
|
2653
|
+
if (acquired) {
|
|
2654
|
+
heartbeat = setInterval(() => {
|
|
2655
|
+
const now = /* @__PURE__ */ new Date();
|
|
2656
|
+
utimes(this.rebuildLockPath, now, now).catch(() => void 0);
|
|
2657
|
+
}, REBUILD_LOCK_HEARTBEAT_MS);
|
|
2658
|
+
heartbeat.unref?.();
|
|
2659
|
+
}
|
|
2660
|
+
try {
|
|
2661
|
+
return await fn(acquired);
|
|
2662
|
+
} finally {
|
|
2663
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
2664
|
+
if (acquired) {
|
|
2665
|
+
try {
|
|
2666
|
+
if (await this.rebuildLockHeldBySelf()) {
|
|
2667
|
+
await unlink(this.rebuildLockPath);
|
|
2668
|
+
}
|
|
2669
|
+
} catch {
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
/** Try to acquire the rebuild lock; returns true if WE created it. */
|
|
2675
|
+
async acquireRebuildLock() {
|
|
2676
|
+
const deadline = Date.now() + REBUILD_LOCK_MAX_WAIT_MS;
|
|
2677
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2678
|
+
for (; ; ) {
|
|
2679
|
+
try {
|
|
2680
|
+
const handle = await open(this.rebuildLockPath, "wx");
|
|
2681
|
+
try {
|
|
2682
|
+
await handle.writeFile(
|
|
2683
|
+
`${process.pid} ${this.lockOwnerId} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2684
|
+
`,
|
|
2685
|
+
"utf8"
|
|
2686
|
+
);
|
|
2687
|
+
} catch {
|
|
2688
|
+
} finally {
|
|
2689
|
+
await handle.close();
|
|
2690
|
+
}
|
|
2691
|
+
return true;
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
if (err?.code !== "EEXIST") {
|
|
2694
|
+
return false;
|
|
2695
|
+
}
|
|
2696
|
+
await this.breakStaleRebuildLock();
|
|
2697
|
+
if (Date.now() >= deadline) return false;
|
|
2698
|
+
await new Promise((r) => setTimeout(r, REBUILD_LOCK_POLL_MS));
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Remove the lock file if its mtime is older than the stale threshold.
|
|
2704
|
+
*
|
|
2705
|
+
* REPLACEMENT-SAFE (NG7Bg, codex P2): a plain `stat` → `unlink` has a TOCTOU
|
|
2706
|
+
* window — two processes can both observe the SAME stale lock; one removes it and
|
|
2707
|
+
* creates a FRESH lock, and the other's later `unlink` then deletes that fresh
|
|
2708
|
+
* holder's ACTIVE lock based on the stale identity it read earlier, leaving the
|
|
2709
|
+
* fresh holder running its critical section with no visible lock and reopening the
|
|
2710
|
+
* lost-update race the mutex prevents. We therefore capture the lock's IDENTITY
|
|
2711
|
+
* (its full content line: `<pid> <owner-uuid> <iso>`) when we judge it stale, then
|
|
2712
|
+
* RE-READ immediately before unlinking and only remove it when the content is
|
|
2713
|
+
* byte-identical AND still stale. A replacement lock has a different owner id /
|
|
2714
|
+
* timestamp, so its content differs and we leave it untouched. We never unlink a
|
|
2715
|
+
* lock whose mtime is now fresh (a heartbeat refreshed it) or whose identity
|
|
2716
|
+
* changed (a replacement was created). This is best-effort: any mismatch/vanish
|
|
2717
|
+
* simply skips the break and the caller polls again.
|
|
2718
|
+
*/
|
|
2719
|
+
async breakStaleRebuildLock() {
|
|
2720
|
+
let staleIdentity;
|
|
2721
|
+
try {
|
|
2722
|
+
const info = await stat(this.rebuildLockPath);
|
|
2723
|
+
if (Date.now() - info.mtimeMs <= REBUILD_LOCK_STALE_MS) {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
staleIdentity = await readFile2(this.rebuildLockPath, "utf8");
|
|
2727
|
+
} catch {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (this.onBeforeBreakStaleUnlinkForTest) {
|
|
2731
|
+
await this.onBeforeBreakStaleUnlinkForTest();
|
|
2732
|
+
}
|
|
2733
|
+
try {
|
|
2734
|
+
const current = await readFile2(this.rebuildLockPath, "utf8");
|
|
2735
|
+
if (current !== staleIdentity) return;
|
|
2736
|
+
const recheck = await stat(this.rebuildLockPath);
|
|
2737
|
+
if (Date.now() - recheck.mtimeMs <= REBUILD_LOCK_STALE_MS) return;
|
|
2738
|
+
await unlink(this.rebuildLockPath).catch(() => void 0);
|
|
2739
|
+
} catch {
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Whether the rebuild lock file was written by THIS instance (round 6, codex
|
|
2744
|
+
* P2 — NBsGP). Matches the per-instance owner id, NOT just `process.pid`: two
|
|
2745
|
+
* NamespaceCatalog instances in the same process share a PID, so a PID-only
|
|
2746
|
+
* check would wrongly treat instance A's lock as self-held by instance B and
|
|
2747
|
+
* let B's touch skip the wait and append into A's rebuild window. Falls back to
|
|
2748
|
+
* the legacy PID-only form for lock files written before owner ids existed.
|
|
2749
|
+
*/
|
|
2750
|
+
async rebuildLockHeldBySelf() {
|
|
2751
|
+
try {
|
|
2752
|
+
const body = await readFile2(this.rebuildLockPath, "utf8");
|
|
2753
|
+
const parts = body.trim().split(/\s+/);
|
|
2754
|
+
const pid = Number.parseInt(parts[0] ?? "", 10);
|
|
2755
|
+
const ownerId = parts[1];
|
|
2756
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2757
|
+
if (ownerId && UUID_RE.test(ownerId)) {
|
|
2758
|
+
return ownerId === this.lockOwnerId;
|
|
2759
|
+
}
|
|
2760
|
+
return Number.isFinite(pid) && pid === process.pid;
|
|
2761
|
+
} catch {
|
|
2762
|
+
return false;
|
|
1498
2763
|
}
|
|
1499
|
-
rules.push(rule);
|
|
1500
|
-
await saveCorrectionsFile(storage.dir, rules);
|
|
1501
2764
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2765
|
+
/**
|
|
2766
|
+
* Merge a prior record's preserved metadata (timestamps, principal hints)
|
|
2767
|
+
* onto a freshly-discovered record. Disk-derived fields (storageDir, kind)
|
|
2768
|
+
* take precedence from the new record.
|
|
2769
|
+
*
|
|
2770
|
+
* PROVENANCE (round 3, cursor Low): `discoveredBy` and `createdAt` are
|
|
2771
|
+
* CREATION-ONLY — identical to the touch path's invariant. A rebuild must NOT
|
|
2772
|
+
* reset a namespace first seen via a `write`/`read` touch back to `config`
|
|
2773
|
+
* just because it is also listed in policies. So when a prior record exists we
|
|
2774
|
+
* carry its `discoveredBy` forward; only brand-new records keep the fresh
|
|
2775
|
+
* (config/scan) provenance.
|
|
2776
|
+
*/
|
|
2777
|
+
mergeForRebuild(prior, fresh) {
|
|
2778
|
+
if (!prior) return fresh;
|
|
2779
|
+
const merged = {
|
|
2780
|
+
...fresh,
|
|
2781
|
+
createdAt: prior.createdAt ?? fresh.createdAt,
|
|
2782
|
+
discoveredBy: prior.discoveredBy ?? fresh.discoveredBy
|
|
2783
|
+
};
|
|
2784
|
+
if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;
|
|
2785
|
+
if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;
|
|
2786
|
+
if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };
|
|
2787
|
+
if (prior.principal !== void 0) merged.principal = prior.principal;
|
|
2788
|
+
if (prior.projectId !== void 0) merged.projectId = prior.projectId;
|
|
2789
|
+
if (prior.branch !== void 0) merged.branch = prior.branch;
|
|
2790
|
+
if (prior.parentNamespace !== void 0) merged.parentNamespace = prior.parentNamespace;
|
|
2791
|
+
return merged;
|
|
2792
|
+
}
|
|
2793
|
+
// ── Persistence ──────────────────────────────────────────────────────────
|
|
2794
|
+
/** Load the JSONL log and fold it into current state (last-record-wins). */
|
|
2795
|
+
async loadCompacted() {
|
|
2796
|
+
const records = /* @__PURE__ */ new Map();
|
|
2797
|
+
let raw;
|
|
2798
|
+
try {
|
|
2799
|
+
raw = await readFile2(this.catalogPath, "utf8");
|
|
2800
|
+
} catch {
|
|
2801
|
+
return records;
|
|
1505
2802
|
}
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2803
|
+
for (const line of raw.split("\n")) {
|
|
2804
|
+
const trimmed = line.trim();
|
|
2805
|
+
if (trimmed.length === 0) continue;
|
|
2806
|
+
let parsed;
|
|
2807
|
+
try {
|
|
2808
|
+
parsed = JSON.parse(trimmed);
|
|
2809
|
+
} catch {
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
const record = coerceRecord(parsed);
|
|
2813
|
+
if (!record) continue;
|
|
2814
|
+
const prior = records.get(record.namespace);
|
|
2815
|
+
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
1512
2816
|
}
|
|
1513
|
-
|
|
1514
|
-
await saveCorrectionsFile(storage.dir, rules);
|
|
1515
|
-
return removed;
|
|
2817
|
+
return records;
|
|
1516
2818
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
2819
|
+
/**
|
|
2820
|
+
* Serialize an arbitrary read-modify-write critical section through the single
|
|
2821
|
+
* write chain. Every catalog mutation (touch read+merge+append, full rewrite)
|
|
2822
|
+
* runs through this so they are mutually exclusive: a touch always reads the
|
|
2823
|
+
* latest persisted state before appending, and a rebuild rewrite cannot
|
|
2824
|
+
* interleave with a touch's append. The chain recovers from rejection
|
|
2825
|
+
* (CLAUDE.md rule #40) — one failed section never poisons subsequent ones —
|
|
2826
|
+
* while still surfacing the error to that section's awaited promise.
|
|
2827
|
+
*/
|
|
2828
|
+
queueCritical(fn) {
|
|
2829
|
+
const run = this.writeChain.then(fn);
|
|
2830
|
+
this.writeChain = run.then(
|
|
2831
|
+
() => void 0,
|
|
2832
|
+
() => void 0
|
|
1523
2833
|
);
|
|
2834
|
+
return run;
|
|
1524
2835
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
2836
|
+
/**
|
|
2837
|
+
* Append a single record to the JSONL log WITHOUT re-serializing through the
|
|
2838
|
+
* write chain. MUST only be called from inside a `queueCritical(...)` section
|
|
2839
|
+
* (which already holds the serialized turn); calling it directly would bypass
|
|
2840
|
+
* the read-before-append ordering that prevents lost-field races.
|
|
2841
|
+
*/
|
|
2842
|
+
async appendUnchained(record) {
|
|
2843
|
+
const line = serializeRecord(record) + "\n";
|
|
2844
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2845
|
+
await appendFile(this.catalogPath, line, "utf8");
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Atomic temp-file + rename rewrite (CLAUDE.md rule #54: write temp, then
|
|
2849
|
+
* rename — never delete-before-write) WITHOUT re-entering the write chain.
|
|
2850
|
+
* MUST only be called from inside a `queueCritical(...)` turn (e.g. the
|
|
2851
|
+
* rebuild critical section, which already holds the serialized turn so its
|
|
2852
|
+
* load and rewrite are atomic against concurrent touches). Re-entering the
|
|
2853
|
+
* chain from within a held turn would deadlock.
|
|
2854
|
+
*/
|
|
2855
|
+
async rewriteUnchained(records) {
|
|
2856
|
+
const body = records.map((r) => serializeRecord(r)).join("\n") + (records.length > 0 ? "\n" : "");
|
|
2857
|
+
await mkdir2(this.stateDir, { recursive: true });
|
|
2858
|
+
const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2859
|
+
await writeFile2(tmp, body, "utf8");
|
|
2860
|
+
await rename(tmp, this.catalogPath);
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
function isPathInside(root, child) {
|
|
2864
|
+
const relative = path2.relative(root, child);
|
|
2865
|
+
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
1542
2866
|
}
|
|
1543
2867
|
|
|
1544
2868
|
// src/orchestrator.ts
|
|
@@ -1652,7 +2976,7 @@ async function raceRecallAbort(promise, signal, message = "recall aborted") {
|
|
|
1652
2976
|
}
|
|
1653
2977
|
}
|
|
1654
2978
|
function qmdCollectionPathParts(resultPath) {
|
|
1655
|
-
if (!resultPath ||
|
|
2979
|
+
if (!resultPath || path3.isAbsolute(resultPath)) return null;
|
|
1656
2980
|
const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1657
2981
|
const slashIndex = normalized.indexOf("/");
|
|
1658
2982
|
if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
|
|
@@ -1665,9 +2989,9 @@ function qmdCollectionPathParts(resultPath) {
|
|
|
1665
2989
|
}
|
|
1666
2990
|
function qmdResultPathCandidates(storageDir, resultPath) {
|
|
1667
2991
|
const candidates = /* @__PURE__ */ new Set();
|
|
1668
|
-
const storageRoot =
|
|
2992
|
+
const storageRoot = path3.resolve(storageDir);
|
|
1669
2993
|
const addCandidate = (candidate) => {
|
|
1670
|
-
const resolved =
|
|
2994
|
+
const resolved = path3.resolve(candidate);
|
|
1671
2995
|
if (isPathInsideStorageRoot(storageRoot, resolved)) {
|
|
1672
2996
|
candidates.add(resolved);
|
|
1673
2997
|
}
|
|
@@ -1675,12 +2999,12 @@ function qmdResultPathCandidates(storageDir, resultPath) {
|
|
|
1675
2999
|
const addRelativeCandidates = (relativePath) => {
|
|
1676
3000
|
const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1677
3001
|
if (!normalized) return;
|
|
1678
|
-
addCandidate(
|
|
3002
|
+
addCandidate(path3.join(storageRoot, normalized));
|
|
1679
3003
|
if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
|
|
1680
|
-
addCandidate(
|
|
3004
|
+
addCandidate(path3.join(storageRoot, "facts", normalized));
|
|
1681
3005
|
}
|
|
1682
3006
|
};
|
|
1683
|
-
if (
|
|
3007
|
+
if (path3.isAbsolute(resultPath)) {
|
|
1684
3008
|
addCandidate(resultPath);
|
|
1685
3009
|
} else {
|
|
1686
3010
|
addRelativeCandidates(resultPath);
|
|
@@ -1797,7 +3121,7 @@ async function qmdStartupCollectionCheckWithTimeout(promise, controller, label)
|
|
|
1797
3121
|
return await Promise.race([checkedPromise, timeoutPromise]);
|
|
1798
3122
|
}
|
|
1799
3123
|
function defaultWorkspaceDir() {
|
|
1800
|
-
return
|
|
3124
|
+
return path3.join(os.homedir(), ".openclaw", "workspace");
|
|
1801
3125
|
}
|
|
1802
3126
|
function sanitizeSessionKeyForFilename(sessionKey) {
|
|
1803
3127
|
const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -2102,11 +3426,11 @@ function mergeGraphExpandedResults(primary, expanded) {
|
|
|
2102
3426
|
return Array.from(mergedByPath.values());
|
|
2103
3427
|
}
|
|
2104
3428
|
function graphPathRelativeToStorage(storageDir, candidatePath) {
|
|
2105
|
-
const absolutePath =
|
|
2106
|
-
const rel =
|
|
3429
|
+
const absolutePath = path3.isAbsolute(candidatePath) ? candidatePath : path3.resolve(storageDir, candidatePath);
|
|
3430
|
+
const rel = path3.relative(storageDir, absolutePath);
|
|
2107
3431
|
if (!rel || rel === ".") return null;
|
|
2108
3432
|
if (rel.startsWith("..")) return null;
|
|
2109
|
-
return rel.split(
|
|
3433
|
+
return rel.split(path3.sep).join("/");
|
|
2110
3434
|
}
|
|
2111
3435
|
function normalizeGraphActivationScore(score) {
|
|
2112
3436
|
const bounded = Number.isFinite(score) && score > 0 ? score : 0;
|
|
@@ -2248,7 +3572,7 @@ function buildMemoryPathById(allMemsForGraph, storageDir) {
|
|
|
2248
3572
|
for (const mem of allMemsForGraph ?? []) {
|
|
2249
3573
|
const id = mem.frontmatter.id;
|
|
2250
3574
|
if (!id) continue;
|
|
2251
|
-
pathById.set(id,
|
|
3575
|
+
pathById.set(id, path3.relative(storageDir, mem.path));
|
|
2252
3576
|
}
|
|
2253
3577
|
return pathById;
|
|
2254
3578
|
}
|
|
@@ -2256,7 +3580,7 @@ function appendMemoryToGraphContext(options) {
|
|
|
2256
3580
|
if (!Array.isArray(options.allMemsForGraph)) return;
|
|
2257
3581
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2258
3582
|
options.allMemsForGraph.push({
|
|
2259
|
-
path:
|
|
3583
|
+
path: path3.join(options.storageDir, options.memoryRelPath),
|
|
2260
3584
|
content: options.content,
|
|
2261
3585
|
frontmatter: {
|
|
2262
3586
|
id: options.memoryId,
|
|
@@ -2276,20 +3600,24 @@ function resolvePersistedMemoryRelativePath(options) {
|
|
|
2276
3600
|
const persisted = options.pathById.get(options.memoryId);
|
|
2277
3601
|
if (persisted) return persisted;
|
|
2278
3602
|
if (options.category === "correction") {
|
|
2279
|
-
return
|
|
3603
|
+
return path3.join("corrections", `${options.memoryId}.md`);
|
|
2280
3604
|
}
|
|
2281
3605
|
const subtree = options.category === "procedure" ? "procedures" : options.category === "reasoning_trace" ? "reasoning-traces" : "facts";
|
|
2282
3606
|
const idParts = options.memoryId.split("-");
|
|
2283
3607
|
const maybeTimestamp = Number(idParts[1]);
|
|
2284
3608
|
if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
|
|
2285
3609
|
const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
|
|
2286
|
-
return
|
|
3610
|
+
return path3.join(subtree, day, `${options.memoryId}.md`);
|
|
2287
3611
|
}
|
|
2288
|
-
return
|
|
3612
|
+
return path3.join(subtree, `${options.memoryId}.md`);
|
|
2289
3613
|
}
|
|
2290
3614
|
var Orchestrator = class _Orchestrator {
|
|
2291
3615
|
storage;
|
|
2292
3616
|
storageRouter;
|
|
3617
|
+
/** Rebuildable namespace catalog (issue #1499). Inert unless namespaces enabled. */
|
|
3618
|
+
namespaceCatalog;
|
|
3619
|
+
namespaceStorageDirHints = /* @__PURE__ */ new Map();
|
|
3620
|
+
namespaceStorageDirHintsLoaded = false;
|
|
2293
3621
|
namespaceSearchRouter;
|
|
2294
3622
|
qmd;
|
|
2295
3623
|
conversationQmd;
|
|
@@ -2703,6 +4031,172 @@ var Orchestrator = class _Orchestrator {
|
|
|
2703
4031
|
)
|
|
2704
4032
|
);
|
|
2705
4033
|
}
|
|
4034
|
+
rememberNamespaceStorageDirHint(namespace, storageDir) {
|
|
4035
|
+
if (!this.config.namespacesEnabled || !storageDir) return;
|
|
4036
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
4037
|
+
if (!ns) return;
|
|
4038
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4039
|
+
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
|
|
4040
|
+
if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
|
|
4041
|
+
const resolvedStorageDir = path3.resolve(storageDir);
|
|
4042
|
+
let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
4043
|
+
if (!hints) {
|
|
4044
|
+
hints = /* @__PURE__ */ new Set();
|
|
4045
|
+
this.namespaceStorageDirHints.set(resolvedStorageDir, hints);
|
|
4046
|
+
}
|
|
4047
|
+
hints.add(ns);
|
|
4048
|
+
}
|
|
4049
|
+
storageDirMatchesNamespaceHint(namespace, storageDir) {
|
|
4050
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
4051
|
+
if (!ns) return false;
|
|
4052
|
+
const resolvedStorageDir = path3.resolve(storageDir);
|
|
4053
|
+
const resolvedMemoryDir = path3.resolve(this.config.memoryDir);
|
|
4054
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
4055
|
+
if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
|
|
4056
|
+
const resolvedNamespacesDir = path3.join(resolvedMemoryDir, "namespaces");
|
|
4057
|
+
if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
|
|
4058
|
+
const rawRoot = path3.resolve(resolvedNamespacesDir, ns);
|
|
4059
|
+
const tokenRoot = path3.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
|
|
4060
|
+
return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
|
|
4061
|
+
}
|
|
4062
|
+
namespaceStorageDirHintOwnershipRank(record, resolvedStorageDir, configured) {
|
|
4063
|
+
if (resolvedStorageDir === path3.resolve(this.config.memoryDir)) {
|
|
4064
|
+
return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace) ? 0 : 3;
|
|
4065
|
+
}
|
|
4066
|
+
const leaf = path3.basename(resolvedStorageDir);
|
|
4067
|
+
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
4068
|
+
if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
|
|
4069
|
+
if (record.namespace === leaf) return 1;
|
|
4070
|
+
if (tokenOwnsRoot) return 2;
|
|
4071
|
+
return 3;
|
|
4072
|
+
}
|
|
4073
|
+
preferNamespaceStorageDirHintOwner(current, candidate, resolvedStorageDir, configured) {
|
|
4074
|
+
const currentRank = this.namespaceStorageDirHintOwnershipRank(
|
|
4075
|
+
current,
|
|
4076
|
+
resolvedStorageDir,
|
|
4077
|
+
configured
|
|
4078
|
+
);
|
|
4079
|
+
const candidateRank = this.namespaceStorageDirHintOwnershipRank(
|
|
4080
|
+
candidate,
|
|
4081
|
+
resolvedStorageDir,
|
|
4082
|
+
configured
|
|
4083
|
+
);
|
|
4084
|
+
if (candidateRank < currentRank) return candidate;
|
|
4085
|
+
if (candidateRank > currentRank) return current;
|
|
4086
|
+
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
4087
|
+
if (byName < 0) return candidate;
|
|
4088
|
+
if (byName > 0) return current;
|
|
4089
|
+
return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
|
|
4090
|
+
}
|
|
4091
|
+
loadNamespaceStorageDirHintsFromCatalog() {
|
|
4092
|
+
if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
|
|
4093
|
+
this.namespaceStorageDirHintsLoaded = true;
|
|
4094
|
+
const catalogPath = path3.join(this.config.memoryDir, "state", "namespaces.jsonl");
|
|
4095
|
+
if (!existsSync(catalogPath)) return;
|
|
4096
|
+
let body;
|
|
4097
|
+
try {
|
|
4098
|
+
body = readFileSync(catalogPath, "utf8");
|
|
4099
|
+
} catch {
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
const compactedByNamespace = /* @__PURE__ */ new Map();
|
|
4103
|
+
for (const line of body.split(/\r?\n/)) {
|
|
4104
|
+
const trimmed = line.trim();
|
|
4105
|
+
if (!trimmed) continue;
|
|
4106
|
+
try {
|
|
4107
|
+
const parsed = JSON.parse(trimmed);
|
|
4108
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
4109
|
+
const record = parsed;
|
|
4110
|
+
if (typeof record.namespace !== "string" || typeof record.storageDir !== "string" || typeof record.identityToken !== "string") {
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
4113
|
+
const namespace = normalizeNamespaceIdentity(record.namespace);
|
|
4114
|
+
if (!namespace || record.identityToken !== namespaceIdentityToken(namespace)) continue;
|
|
4115
|
+
compactedByNamespace.set(namespace, {
|
|
4116
|
+
namespace,
|
|
4117
|
+
identityToken: record.identityToken,
|
|
4118
|
+
storageDir: record.storageDir
|
|
4119
|
+
});
|
|
4120
|
+
} catch {
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
const configured = new Set(
|
|
4124
|
+
this.configuredNamespaces().map((namespace) => normalizeNamespaceIdentity(namespace))
|
|
4125
|
+
);
|
|
4126
|
+
const preferredByStorageDir = /* @__PURE__ */ new Map();
|
|
4127
|
+
for (const record of compactedByNamespace.values()) {
|
|
4128
|
+
if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
const resolvedStorageDir = path3.resolve(record.storageDir);
|
|
4132
|
+
const current = preferredByStorageDir.get(resolvedStorageDir);
|
|
4133
|
+
preferredByStorageDir.set(
|
|
4134
|
+
resolvedStorageDir,
|
|
4135
|
+
current ? this.preferNamespaceStorageDirHintOwner(
|
|
4136
|
+
current,
|
|
4137
|
+
record,
|
|
4138
|
+
resolvedStorageDir,
|
|
4139
|
+
configured
|
|
4140
|
+
) : record
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
for (const record of preferredByStorageDir.values()) {
|
|
4144
|
+
this.rememberNamespaceStorageDirHint(record.namespace, record.storageDir);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Namespaces that QMD maintenance (update/embed) must cover: the CONFIGURED set
|
|
4149
|
+
* PLUS every dynamic namespace recorded in the catalog (NGnei, codex P2). An
|
|
4150
|
+
* extraction that writes to a coding-scoped/dynamic namespace (not in
|
|
4151
|
+
* defaultNamespace/sharedNamespace/namespacePolicies) is only made discoverable
|
|
4152
|
+
* via the catalog; if maintenance embeds only `configuredNamespaces()`, that
|
|
4153
|
+
* namespace's QMD collection is never updated/embedded after writes and
|
|
4154
|
+
* recall/search stays stale or empty until it is manually configured. We union in
|
|
4155
|
+
* the catalog's namespaces so maintenance keeps dynamic namespaces fresh.
|
|
4156
|
+
* `updateNamespaces`/`embedNamespaces` already trim, dedup, and skip
|
|
4157
|
+
* unavailable/missing collections, so extra names are filtered safely. A catalog
|
|
4158
|
+
* read failure must never break maintenance — fall back to the configured set.
|
|
4159
|
+
*/
|
|
4160
|
+
async maintenanceNamespaces() {
|
|
4161
|
+
const configured = this.configuredNamespaces();
|
|
4162
|
+
if (!this.namespaceCatalog.enabled) return configured;
|
|
4163
|
+
const configuredSet = new Set(configured);
|
|
4164
|
+
let cataloged = [];
|
|
4165
|
+
try {
|
|
4166
|
+
const records = await this.namespaceCatalog.listNamespaces();
|
|
4167
|
+
const safeRecords = await Promise.all(
|
|
4168
|
+
records.map(async (record) => {
|
|
4169
|
+
const namespace = record.namespace.trim();
|
|
4170
|
+
if (!namespace || configuredSet.has(namespace)) return null;
|
|
4171
|
+
return await this.isCatalogedMaintenanceRootLive(record) ? namespace : null;
|
|
4172
|
+
})
|
|
4173
|
+
);
|
|
4174
|
+
cataloged = safeRecords.filter(
|
|
4175
|
+
(namespace) => namespace !== null
|
|
4176
|
+
);
|
|
4177
|
+
} catch {
|
|
4178
|
+
cataloged = [];
|
|
4179
|
+
}
|
|
4180
|
+
return Array.from(
|
|
4181
|
+
new Set(
|
|
4182
|
+
[...configured, ...cataloged].map((value) => value.trim()).filter(Boolean)
|
|
4183
|
+
)
|
|
4184
|
+
);
|
|
4185
|
+
}
|
|
4186
|
+
async isCatalogedMaintenanceRootLive(record) {
|
|
4187
|
+
if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
|
|
4188
|
+
return false;
|
|
4189
|
+
}
|
|
4190
|
+
try {
|
|
4191
|
+
const liveRoot = await resolveNamespaceStorageRoot(this.config, record.namespace);
|
|
4192
|
+
if (path3.resolve(liveRoot) !== path3.resolve(record.storageDir)) {
|
|
4193
|
+
return false;
|
|
4194
|
+
}
|
|
4195
|
+
return hasMemoryData(liveRoot);
|
|
4196
|
+
} catch {
|
|
4197
|
+
return false;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
2706
4200
|
buildConfiguredQmdSearchOptions(queryText) {
|
|
2707
4201
|
const intentHint = this.config.qmdIntentHintsEnabled ? buildQmdIntentHint(inferIntentFromText(queryText)) : void 0;
|
|
2708
4202
|
const explain = this.config.qmdExplainEnabled === true;
|
|
@@ -2777,10 +4271,20 @@ var Orchestrator = class _Orchestrator {
|
|
|
2777
4271
|
this.config = config;
|
|
2778
4272
|
this.profiler = new ProfilingCollector({
|
|
2779
4273
|
enabled: config.profilingEnabled,
|
|
2780
|
-
storageDir: config.profilingStorageDir ||
|
|
4274
|
+
storageDir: config.profilingStorageDir || path3.join(config.memoryDir, "profiling"),
|
|
2781
4275
|
maxTraces: config.profilingMaxTraces
|
|
2782
4276
|
});
|
|
2783
|
-
this.
|
|
4277
|
+
this.namespaceCatalog = new NamespaceCatalog(config);
|
|
4278
|
+
this.storageRouter = new NamespaceStorageRouter(config, {
|
|
4279
|
+
// Return the registration promise (round 6, codex P2 — NEFoX) so the
|
|
4280
|
+
// router's resolve-hook dedup only marks a namespace notified when the
|
|
4281
|
+
// catalog actually APPENDED. A dropped append (rebuild-lock timeout) or a
|
|
4282
|
+
// failure resolves to `false`/rejects, so the next `storageFor` retries.
|
|
4283
|
+
onResolve: (namespace, storageDir) => {
|
|
4284
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
4285
|
+
return this.namespaceCatalog.registerResolved(namespace, storageDir);
|
|
4286
|
+
}
|
|
4287
|
+
});
|
|
2784
4288
|
this.namespaceSearchRouter = new NamespaceSearchRouter(
|
|
2785
4289
|
config,
|
|
2786
4290
|
this.storageRouter
|
|
@@ -2812,7 +4316,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2812
4316
|
this.compounding = config.compoundingEnabled ? new CompoundingEngine(config, this.storage) : void 0;
|
|
2813
4317
|
this.buffer = new SmartBuffer(config, this.storage);
|
|
2814
4318
|
this.transcript = new TranscriptManager(config);
|
|
2815
|
-
this.conversationIndexDir =
|
|
4319
|
+
this.conversationIndexDir = path3.join(
|
|
2816
4320
|
config.memoryDir,
|
|
2817
4321
|
"conversation-index",
|
|
2818
4322
|
"chunks"
|
|
@@ -2869,7 +4373,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
2869
4373
|
this.modelRegistry
|
|
2870
4374
|
);
|
|
2871
4375
|
this.threading = new ThreadingManager(
|
|
2872
|
-
|
|
4376
|
+
path3.join(config.memoryDir, "threads"),
|
|
2873
4377
|
config.threadingGapMinutes
|
|
2874
4378
|
);
|
|
2875
4379
|
this.tmtBuilder = new TmtBuilder(config.memoryDir, {
|
|
@@ -3129,6 +4633,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3129
4633
|
await sm.ensureDirectories();
|
|
3130
4634
|
await sm.loadAliases().catch(() => void 0);
|
|
3131
4635
|
}
|
|
4636
|
+
await this.namespaceCatalog.registerConfiguredNamespaces().catch(() => void 0);
|
|
3132
4637
|
}
|
|
3133
4638
|
await this.relevance.load();
|
|
3134
4639
|
await this.negatives.load();
|
|
@@ -3167,13 +4672,13 @@ var Orchestrator = class _Orchestrator {
|
|
|
3167
4672
|
if (this.config.compactionResetEnabled) {
|
|
3168
4673
|
try {
|
|
3169
4674
|
const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
|
|
3170
|
-
const files = await
|
|
4675
|
+
const files = await readdir2(wsDir).catch(() => []);
|
|
3171
4676
|
for (const f of files) {
|
|
3172
4677
|
if (!f.startsWith(".compaction-reset-signal-")) continue;
|
|
3173
|
-
const fp =
|
|
3174
|
-
const s = await
|
|
4678
|
+
const fp = path3.join(wsDir, f);
|
|
4679
|
+
const s = await stat2(fp).catch(() => null);
|
|
3175
4680
|
if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
|
|
3176
|
-
await
|
|
4681
|
+
await unlink2(fp).catch(() => {
|
|
3177
4682
|
});
|
|
3178
4683
|
log.debug(`initialize: removed stale compaction signal ${f}`);
|
|
3179
4684
|
}
|
|
@@ -3186,7 +4691,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3186
4691
|
const available = await this.qmd.probe();
|
|
3187
4692
|
if (available) {
|
|
3188
4693
|
log.info(`Search backend: available ${this.qmd.debugStatus()}`);
|
|
3189
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
4694
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
3190
4695
|
const states = await Promise.all(
|
|
3191
4696
|
namespaces.map(async (namespace) => {
|
|
3192
4697
|
const collectionCheckAbort = new AbortController();
|
|
@@ -3270,7 +4775,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3270
4775
|
log.info("QMD startup sync: updating index to match current disk state");
|
|
3271
4776
|
if (this.config.namespacesEnabled) {
|
|
3272
4777
|
await this.namespaceSearchRouter.updateNamespaces(
|
|
3273
|
-
this.
|
|
4778
|
+
await this.maintenanceNamespaces(),
|
|
3274
4779
|
{ signal }
|
|
3275
4780
|
);
|
|
3276
4781
|
} else {
|
|
@@ -3482,7 +4987,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3482
4987
|
if (this.config.namespacesEnabled) {
|
|
3483
4988
|
this.namespaceSearchRouter.clearCache();
|
|
3484
4989
|
}
|
|
3485
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
4990
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
3486
4991
|
const states = await Promise.all(
|
|
3487
4992
|
namespaces.map(async (namespace) => ({
|
|
3488
4993
|
namespace,
|
|
@@ -3564,7 +5069,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3564
5069
|
*/
|
|
3565
5070
|
async autoRegisterDaySummaryCron() {
|
|
3566
5071
|
const home = resolveHomeDir();
|
|
3567
|
-
const jobsPath =
|
|
5072
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3568
5073
|
try {
|
|
3569
5074
|
if (!existsSync(jobsPath)) {
|
|
3570
5075
|
log.debug(
|
|
@@ -3617,7 +5122,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3617
5122
|
}
|
|
3618
5123
|
async autoRegisterNightlyGovernanceCron() {
|
|
3619
5124
|
const home = resolveHomeDir();
|
|
3620
|
-
const jobsPath =
|
|
5125
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3621
5126
|
try {
|
|
3622
5127
|
if (!existsSync(jobsPath)) {
|
|
3623
5128
|
log.debug("nightly governance cron: jobs.json not found, skipping auto-register");
|
|
@@ -3639,7 +5144,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3639
5144
|
}
|
|
3640
5145
|
async autoRegisterProceduralMiningCron() {
|
|
3641
5146
|
const home = resolveHomeDir();
|
|
3642
|
-
const jobsPath =
|
|
5147
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3643
5148
|
try {
|
|
3644
5149
|
if (!existsSync(jobsPath)) {
|
|
3645
5150
|
log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
|
|
@@ -3659,7 +5164,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3659
5164
|
}
|
|
3660
5165
|
async autoRegisterContradictionScanCron() {
|
|
3661
5166
|
const home = resolveHomeDir();
|
|
3662
|
-
const jobsPath =
|
|
5167
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3663
5168
|
try {
|
|
3664
5169
|
if (!existsSync(jobsPath)) {
|
|
3665
5170
|
log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
|
|
@@ -3679,7 +5184,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3679
5184
|
}
|
|
3680
5185
|
async autoRegisterPatternReinforcementCron() {
|
|
3681
5186
|
const home = resolveHomeDir();
|
|
3682
|
-
const jobsPath =
|
|
5187
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3683
5188
|
try {
|
|
3684
5189
|
if (!existsSync(jobsPath)) {
|
|
3685
5190
|
log.debug("pattern reinforcement cron: jobs.json not found, skipping auto-register");
|
|
@@ -3741,7 +5246,7 @@ var Orchestrator = class _Orchestrator {
|
|
|
3741
5246
|
}
|
|
3742
5247
|
async autoRegisterGraphEdgeDecayCron() {
|
|
3743
5248
|
const home = resolveHomeDir();
|
|
3744
|
-
const jobsPath =
|
|
5249
|
+
const jobsPath = path3.join(home, ".openclaw", "cron", "jobs.json");
|
|
3745
5250
|
try {
|
|
3746
5251
|
if (!existsSync(jobsPath)) {
|
|
3747
5252
|
log.debug("graph edge decay cron: jobs.json not found, skipping auto-register");
|
|
@@ -3798,15 +5303,15 @@ ${doc.content}` : doc.content,
|
|
|
3798
5303
|
this.lastFileHygieneRunAtMs = now;
|
|
3799
5304
|
if (hygiene.rotateEnabled) {
|
|
3800
5305
|
for (const rel of hygiene.rotatePaths) {
|
|
3801
|
-
const abs =
|
|
5306
|
+
const abs = path3.isAbsolute(rel) ? rel : path3.join(this.config.workspaceDir, rel);
|
|
3802
5307
|
try {
|
|
3803
|
-
const raw = await
|
|
5308
|
+
const raw = await readFile3(abs, "utf-8");
|
|
3804
5309
|
if (raw.length > hygiene.rotateMaxBytes) {
|
|
3805
|
-
const archiveDir =
|
|
5310
|
+
const archiveDir = path3.join(
|
|
3806
5311
|
this.config.workspaceDir,
|
|
3807
5312
|
hygiene.archiveDir
|
|
3808
5313
|
);
|
|
3809
|
-
const base =
|
|
5314
|
+
const base = path3.basename(abs);
|
|
3810
5315
|
const prefix = base.toUpperCase().replace(/\.MD$/i, "").replace(/[^A-Z0-9]+/g, "-") || "FILE";
|
|
3811
5316
|
const { newContent } = await rotateMarkdownFileToArchive({
|
|
3812
5317
|
filePath: abs,
|
|
@@ -3814,7 +5319,7 @@ ${doc.content}` : doc.content,
|
|
|
3814
5319
|
archivePrefix: prefix,
|
|
3815
5320
|
keepTailChars: hygiene.rotateKeepTailChars
|
|
3816
5321
|
});
|
|
3817
|
-
await
|
|
5322
|
+
await writeFile3(abs, newContent, "utf-8");
|
|
3818
5323
|
}
|
|
3819
5324
|
} catch {
|
|
3820
5325
|
}
|
|
@@ -3831,8 +5336,8 @@ ${doc.content}` : doc.content,
|
|
|
3831
5336
|
log.warn(w.message);
|
|
3832
5337
|
}
|
|
3833
5338
|
if (hygiene.warningsLogEnabled && warnings.length > 0) {
|
|
3834
|
-
const fp =
|
|
3835
|
-
await
|
|
5339
|
+
const fp = path3.join(this.config.memoryDir, hygiene.warningsLogPath);
|
|
5340
|
+
await mkdir3(path3.dirname(fp), { recursive: true });
|
|
3836
5341
|
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3837
5342
|
const block = `
|
|
3838
5343
|
|
|
@@ -3841,11 +5346,11 @@ ${doc.content}` : doc.content,
|
|
|
3841
5346
|
` + warnings.map((w) => `- ${w.message}`).join("\n") + "\n";
|
|
3842
5347
|
let existing = "";
|
|
3843
5348
|
try {
|
|
3844
|
-
existing = await
|
|
5349
|
+
existing = await readFile3(fp, "utf-8");
|
|
3845
5350
|
} catch {
|
|
3846
5351
|
existing = "# Engram File Hygiene Warnings\n";
|
|
3847
5352
|
}
|
|
3848
|
-
await
|
|
5353
|
+
await writeFile3(fp, existing + block, "utf-8");
|
|
3849
5354
|
}
|
|
3850
5355
|
}
|
|
3851
5356
|
}
|
|
@@ -3952,6 +5457,7 @@ ${doc.content}` : doc.content,
|
|
|
3952
5457
|
log.warn(`[semantic-consolidation] extension discovery failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
3953
5458
|
}
|
|
3954
5459
|
for (const cluster of clusters) {
|
|
5460
|
+
let canonicalWriteCompleted = false;
|
|
3955
5461
|
try {
|
|
3956
5462
|
const operatorAwareEnabled = this.config.operatorAwareConsolidationEnabled === true;
|
|
3957
5463
|
let prompt = operatorAwareEnabled ? buildOperatorAwareConsolidationPrompt(cluster) : buildConsolidationPrompt(cluster);
|
|
@@ -4030,6 +5536,7 @@ ${doc.content}` : doc.content,
|
|
|
4030
5536
|
derivedVia: operator
|
|
4031
5537
|
}
|
|
4032
5538
|
);
|
|
5539
|
+
canonicalWriteCompleted = true;
|
|
4033
5540
|
result.memoriesConsolidated++;
|
|
4034
5541
|
for (const m of cluster.memories) {
|
|
4035
5542
|
const archiveResult = await targetStorage.archiveMemory(m, {
|
|
@@ -4048,13 +5555,19 @@ ${doc.content}` : doc.content,
|
|
|
4048
5555
|
this.contentHashIndex.remove(m.content);
|
|
4049
5556
|
}
|
|
4050
5557
|
}
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
5558
|
+
try {
|
|
5559
|
+
await this.embeddingFallback.removeFromIndex(m.frontmatter.id);
|
|
5560
|
+
if (this.config.queryAwareIndexingEnabled && m.path && m.frontmatter?.created) {
|
|
5561
|
+
deindexMemory(
|
|
5562
|
+
targetStorage.dir,
|
|
5563
|
+
m.path,
|
|
5564
|
+
m.frontmatter.created,
|
|
5565
|
+
m.frontmatter.tags ?? []
|
|
5566
|
+
);
|
|
5567
|
+
}
|
|
5568
|
+
} catch (cleanupErr) {
|
|
5569
|
+
log.warn(
|
|
5570
|
+
`[semantic-consolidation] index cleanup failed (non-fatal): ${cleanupErr}`
|
|
4058
5571
|
);
|
|
4059
5572
|
}
|
|
4060
5573
|
result.memoriesArchived++;
|
|
@@ -4068,6 +5581,13 @@ ${doc.content}` : doc.content,
|
|
|
4068
5581
|
`[semantic-consolidation] cluster processing failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4069
5582
|
);
|
|
4070
5583
|
result.errors++;
|
|
5584
|
+
} finally {
|
|
5585
|
+
if (canonicalWriteCompleted) {
|
|
5586
|
+
this.markCatalogWrite(
|
|
5587
|
+
this.namespaceFromStorageDir(targetStorage.dir),
|
|
5588
|
+
targetStorage.dir
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
4071
5591
|
}
|
|
4072
5592
|
}
|
|
4073
5593
|
if (result.memoriesArchived > 0 && this.contentHashIndex) {
|
|
@@ -4300,18 +5820,18 @@ ${evidenceText}`
|
|
|
4300
5820
|
const now = options.now instanceof Date && Number.isFinite(options.now.getTime()) ? options.now : /* @__PURE__ */ new Date();
|
|
4301
5821
|
const targetLocalDate = formatDateInTimeZone(now, timeZone);
|
|
4302
5822
|
const datesToScan = utcDateKeysForLocalDay(now, timeZone);
|
|
4303
|
-
const factsBaseDir =
|
|
5823
|
+
const factsBaseDir = path3.join(storage.dir, "facts");
|
|
4304
5824
|
const MAX_CHARS = 1e5;
|
|
4305
5825
|
const facts = [];
|
|
4306
5826
|
for (const date of datesToScan) {
|
|
4307
|
-
const factsDir =
|
|
5827
|
+
const factsDir = path3.join(factsBaseDir, date);
|
|
4308
5828
|
try {
|
|
4309
|
-
const entries = await
|
|
5829
|
+
const entries = await readdir2(factsDir, { withFileTypes: true });
|
|
4310
5830
|
for (const entry of entries) {
|
|
4311
5831
|
if (!entry.name.endsWith(".md")) continue;
|
|
4312
|
-
const fullPath =
|
|
5832
|
+
const fullPath = path3.join(factsDir, entry.name);
|
|
4313
5833
|
try {
|
|
4314
|
-
const raw = await
|
|
5834
|
+
const raw = await readFile3(fullPath, "utf-8");
|
|
4315
5835
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
4316
5836
|
if (!fmMatch) continue;
|
|
4317
5837
|
const fmBlock = fmMatch[1];
|
|
@@ -4330,7 +5850,7 @@ ${evidenceText}`
|
|
|
4330
5850
|
facts.push({
|
|
4331
5851
|
path: fullPath,
|
|
4332
5852
|
frontmatter: {
|
|
4333
|
-
id: fm.id ||
|
|
5853
|
+
id: fm.id || path3.basename(entry.name, ".md"),
|
|
4334
5854
|
category: fm.category || "fact",
|
|
4335
5855
|
created,
|
|
4336
5856
|
updated: fm.updated || created,
|
|
@@ -4352,15 +5872,15 @@ ${evidenceText}`
|
|
|
4352
5872
|
return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
|
|
4353
5873
|
});
|
|
4354
5874
|
const hourlySummaries = [];
|
|
4355
|
-
const hourlyBaseDir =
|
|
5875
|
+
const hourlyBaseDir = path3.join(storage.dir, "summaries", "hourly");
|
|
4356
5876
|
try {
|
|
4357
|
-
const sessionKeys = await
|
|
5877
|
+
const sessionKeys = await readdir2(hourlyBaseDir, { withFileTypes: true });
|
|
4358
5878
|
for (const sk of sessionKeys) {
|
|
4359
5879
|
if (!sk.isDirectory()) continue;
|
|
4360
5880
|
for (const date of datesToScan) {
|
|
4361
|
-
const summaryFile =
|
|
5881
|
+
const summaryFile = path3.join(hourlyBaseDir, sk.name, `${date}.md`);
|
|
4362
5882
|
try {
|
|
4363
|
-
const raw = await
|
|
5883
|
+
const raw = await readFile3(summaryFile, "utf-8");
|
|
4364
5884
|
const filtered = filterHourlySummaryMarkdownForLocalDay(
|
|
4365
5885
|
raw,
|
|
4366
5886
|
date,
|
|
@@ -4462,13 +5982,13 @@ ${evidenceText}`
|
|
|
4462
5982
|
}
|
|
4463
5983
|
async getLastGraphRecallSnapshot(namespace) {
|
|
4464
5984
|
const storage = await this.getStorage(namespace);
|
|
4465
|
-
const snapshotPath =
|
|
5985
|
+
const snapshotPath = path3.join(
|
|
4466
5986
|
storage.dir,
|
|
4467
5987
|
"state",
|
|
4468
5988
|
"last_graph_recall.json"
|
|
4469
5989
|
);
|
|
4470
5990
|
try {
|
|
4471
|
-
const raw = await
|
|
5991
|
+
const raw = await readFile3(snapshotPath, "utf-8");
|
|
4472
5992
|
const parsed = JSON.parse(raw);
|
|
4473
5993
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4474
5994
|
return {
|
|
@@ -4501,9 +6021,9 @@ ${evidenceText}`
|
|
|
4501
6021
|
}
|
|
4502
6022
|
async getLastIntentSnapshot(namespace) {
|
|
4503
6023
|
const storage = await this.getStorage(namespace);
|
|
4504
|
-
const snapshotPath =
|
|
6024
|
+
const snapshotPath = path3.join(storage.dir, "state", "last_intent.json");
|
|
4505
6025
|
try {
|
|
4506
|
-
const raw = await
|
|
6026
|
+
const raw = await readFile3(snapshotPath, "utf-8");
|
|
4507
6027
|
const parsed = JSON.parse(raw);
|
|
4508
6028
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4509
6029
|
const graphDecision = parsed.graphDecision && typeof parsed.graphDecision === "object" ? parsed.graphDecision : void 0;
|
|
@@ -4534,13 +6054,13 @@ ${evidenceText}`
|
|
|
4534
6054
|
}
|
|
4535
6055
|
async getLastQmdRecallSnapshot(namespace) {
|
|
4536
6056
|
const storage = await this.getStorage(namespace);
|
|
4537
|
-
const snapshotPath =
|
|
6057
|
+
const snapshotPath = path3.join(
|
|
4538
6058
|
storage.dir,
|
|
4539
6059
|
"state",
|
|
4540
6060
|
"last_qmd_recall.json"
|
|
4541
6061
|
);
|
|
4542
6062
|
try {
|
|
4543
|
-
const raw = await
|
|
6063
|
+
const raw = await readFile3(snapshotPath, "utf-8");
|
|
4544
6064
|
const parsed = JSON.parse(raw);
|
|
4545
6065
|
if (!parsed || typeof parsed !== "object") return null;
|
|
4546
6066
|
return {
|
|
@@ -4684,10 +6204,10 @@ ${r.snippet.trim()}
|
|
|
4684
6204
|
}
|
|
4685
6205
|
async countConversationChunkDocs(dir) {
|
|
4686
6206
|
try {
|
|
4687
|
-
const entries = await
|
|
6207
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
4688
6208
|
let total = 0;
|
|
4689
6209
|
for (const entry of entries) {
|
|
4690
|
-
const fullPath =
|
|
6210
|
+
const fullPath = path3.join(dir, entry.name);
|
|
4691
6211
|
if (entry.isDirectory()) {
|
|
4692
6212
|
total += await this.countConversationChunkDocs(fullPath);
|
|
4693
6213
|
continue;
|
|
@@ -5586,7 +7106,7 @@ ${r.snippet.trim()}
|
|
|
5586
7106
|
resolvedPath = resolvedCold.result.path;
|
|
5587
7107
|
resolvedResult = resolvedCold.result;
|
|
5588
7108
|
}
|
|
5589
|
-
if (!
|
|
7109
|
+
if (!path3.isAbsolute(resolvedPath)) {
|
|
5590
7110
|
resolvedAmbiguousSeeds.set(result.path, null);
|
|
5591
7111
|
return null;
|
|
5592
7112
|
}
|
|
@@ -5611,7 +7131,7 @@ ${r.snippet.trim()}
|
|
|
5611
7131
|
}
|
|
5612
7132
|
continue;
|
|
5613
7133
|
}
|
|
5614
|
-
if (
|
|
7134
|
+
if (path3.isAbsolute(result.path)) {
|
|
5615
7135
|
const resolved = await resolveAmbiguousSeedOwner(result, null);
|
|
5616
7136
|
if (resolved) {
|
|
5617
7137
|
addResultForNamespace(resolved.namespace, resolved.result);
|
|
@@ -5654,7 +7174,7 @@ ${r.snippet.trim()}
|
|
|
5654
7174
|
0
|
|
5655
7175
|
);
|
|
5656
7176
|
seedPaths.push(
|
|
5657
|
-
...seedRelativePaths.map((rel) =>
|
|
7177
|
+
...seedRelativePaths.map((rel) => path3.join(storage.dir, rel))
|
|
5658
7178
|
);
|
|
5659
7179
|
const seedSet = new Set(seedRelativePaths);
|
|
5660
7180
|
const expanded = await this.graphIndexFor(storage).spreadingActivation(
|
|
@@ -5670,7 +7190,7 @@ ${r.snippet.trim()}
|
|
|
5670
7190
|
for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
|
|
5671
7191
|
if (deadlineExpired()) break;
|
|
5672
7192
|
if (seedSet.has(candidate.path)) continue;
|
|
5673
|
-
const memoryPath =
|
|
7193
|
+
const memoryPath = path3.resolve(storage.dir, candidate.path);
|
|
5674
7194
|
const memory = await storage.readMemoryByPath(memoryPath);
|
|
5675
7195
|
if (deadlineExpired()) break;
|
|
5676
7196
|
if (!memory) continue;
|
|
@@ -5695,7 +7215,7 @@ ${r.snippet.trim()}
|
|
|
5695
7215
|
path: memory.path,
|
|
5696
7216
|
score,
|
|
5697
7217
|
namespace,
|
|
5698
|
-
seed:
|
|
7218
|
+
seed: path3.resolve(storage.dir, candidate.seed),
|
|
5699
7219
|
hopDepth: candidate.hopDepth,
|
|
5700
7220
|
decayedWeight: candidate.decayedWeight,
|
|
5701
7221
|
graphType: candidate.graphType,
|
|
@@ -5716,12 +7236,12 @@ ${r.snippet.trim()}
|
|
|
5716
7236
|
}
|
|
5717
7237
|
async recordLastGraphRecallSnapshot(options) {
|
|
5718
7238
|
try {
|
|
5719
|
-
const snapshotPath =
|
|
7239
|
+
const snapshotPath = path3.join(
|
|
5720
7240
|
options.storage.dir,
|
|
5721
7241
|
"state",
|
|
5722
7242
|
"last_graph_recall.json"
|
|
5723
7243
|
);
|
|
5724
|
-
await
|
|
7244
|
+
await mkdir3(path3.dirname(snapshotPath), { recursive: true });
|
|
5725
7245
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5726
7246
|
const totalSeedCount = options.seedPaths.length;
|
|
5727
7247
|
const totalExpandedCount = options.expandedPaths.length;
|
|
@@ -5748,20 +7268,20 @@ ${r.snippet.trim()}
|
|
|
5748
7268
|
finalResults: (options.finalResults ?? []).slice(0, 64),
|
|
5749
7269
|
shadowComparison: options.shadowComparison
|
|
5750
7270
|
};
|
|
5751
|
-
await
|
|
7271
|
+
await writeFile3(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
5752
7272
|
} catch (err) {
|
|
5753
7273
|
log.debug(`last graph recall write failed: ${err}`);
|
|
5754
7274
|
}
|
|
5755
7275
|
}
|
|
5756
7276
|
async recordLastIntentSnapshot(options) {
|
|
5757
7277
|
try {
|
|
5758
|
-
const snapshotPath =
|
|
7278
|
+
const snapshotPath = path3.join(
|
|
5759
7279
|
options.storage.dir,
|
|
5760
7280
|
"state",
|
|
5761
7281
|
"last_intent.json"
|
|
5762
7282
|
);
|
|
5763
|
-
await
|
|
5764
|
-
await
|
|
7283
|
+
await mkdir3(path3.dirname(snapshotPath), { recursive: true });
|
|
7284
|
+
await writeFile3(
|
|
5765
7285
|
snapshotPath,
|
|
5766
7286
|
JSON.stringify(options.snapshot, null, 2),
|
|
5767
7287
|
"utf-8"
|
|
@@ -5772,13 +7292,13 @@ ${r.snippet.trim()}
|
|
|
5772
7292
|
}
|
|
5773
7293
|
async recordLastQmdRecallSnapshot(options) {
|
|
5774
7294
|
try {
|
|
5775
|
-
const snapshotPath =
|
|
7295
|
+
const snapshotPath = path3.join(
|
|
5776
7296
|
options.storage.dir,
|
|
5777
7297
|
"state",
|
|
5778
7298
|
"last_qmd_recall.json"
|
|
5779
7299
|
);
|
|
5780
|
-
await
|
|
5781
|
-
await
|
|
7300
|
+
await mkdir3(path3.dirname(snapshotPath), { recursive: true });
|
|
7301
|
+
await writeFile3(
|
|
5782
7302
|
snapshotPath,
|
|
5783
7303
|
JSON.stringify(options.snapshot, null, 2),
|
|
5784
7304
|
"utf-8"
|
|
@@ -5792,9 +7312,9 @@ ${r.snippet.trim()}
|
|
|
5792
7312
|
const stateDir = await this.resolveStateDirForNamespace(
|
|
5793
7313
|
options.namespace
|
|
5794
7314
|
);
|
|
5795
|
-
const snapshotPath =
|
|
5796
|
-
await
|
|
5797
|
-
await
|
|
7315
|
+
const snapshotPath = path3.join(stateDir, "last_intent.json");
|
|
7316
|
+
await mkdir3(path3.dirname(snapshotPath), { recursive: true });
|
|
7317
|
+
await writeFile3(
|
|
5798
7318
|
snapshotPath,
|
|
5799
7319
|
JSON.stringify(options.snapshot, null, 2),
|
|
5800
7320
|
"utf-8"
|
|
@@ -5805,24 +7325,24 @@ ${r.snippet.trim()}
|
|
|
5805
7325
|
}
|
|
5806
7326
|
async resolveStateDirForNamespace(namespace) {
|
|
5807
7327
|
if (!this.config.namespacesEnabled) {
|
|
5808
|
-
return
|
|
7328
|
+
return path3.join(this.config.memoryDir, "state");
|
|
5809
7329
|
}
|
|
5810
7330
|
if (namespace !== this.config.defaultNamespace) {
|
|
5811
|
-
return
|
|
7331
|
+
return path3.join(this.config.memoryDir, "namespaces", namespace, "state");
|
|
5812
7332
|
}
|
|
5813
|
-
const candidate =
|
|
7333
|
+
const candidate = path3.join(
|
|
5814
7334
|
this.config.memoryDir,
|
|
5815
7335
|
"namespaces",
|
|
5816
7336
|
this.config.defaultNamespace
|
|
5817
7337
|
);
|
|
5818
7338
|
try {
|
|
5819
|
-
const candidateStat = await
|
|
7339
|
+
const candidateStat = await stat2(candidate);
|
|
5820
7340
|
if (candidateStat.isDirectory()) {
|
|
5821
|
-
return
|
|
7341
|
+
return path3.join(candidate, "state");
|
|
5822
7342
|
}
|
|
5823
7343
|
} catch {
|
|
5824
7344
|
}
|
|
5825
|
-
return
|
|
7345
|
+
return path3.join(this.config.memoryDir, "state");
|
|
5826
7346
|
}
|
|
5827
7347
|
buildGraphRecallRankedResults(results, sourceLabelResolver, limit = 64) {
|
|
5828
7348
|
return results.slice(0, limit).map((result) => ({
|
|
@@ -6232,7 +7752,7 @@ ${r.snippet.trim()}
|
|
|
6232
7752
|
const graphExpandedResultPaths = /* @__PURE__ */ new Set();
|
|
6233
7753
|
const graphSourceLabelsForPath = (resultPath) => {
|
|
6234
7754
|
const labels = [];
|
|
6235
|
-
const normalizedPath = resultPath.split(
|
|
7755
|
+
const normalizedPath = resultPath.split(path3.sep).join("/");
|
|
6236
7756
|
const isEntityPath = normalizedPath.startsWith("entities/") || normalizedPath.includes("/entities/");
|
|
6237
7757
|
if (graphBaselinePaths.has(resultPath)) labels.push("baseline");
|
|
6238
7758
|
if (graphExpandedResultPaths.has(resultPath))
|
|
@@ -6379,6 +7899,9 @@ ${r.snippet.trim()}
|
|
|
6379
7899
|
}
|
|
6380
7900
|
const profileStorage = await this.storageRouter.storageFor(selfNamespace);
|
|
6381
7901
|
throwIfRecallAborted(options.abortSignal);
|
|
7902
|
+
if (this.namespaceCatalog.enabled && recallResultLimit > 0 && !options.abortSignal?.aborted) {
|
|
7903
|
+
for (const ns of recallNamespaces) this.markCatalogRead(ns);
|
|
7904
|
+
}
|
|
6382
7905
|
const sharedContextPromise = (async () => {
|
|
6383
7906
|
if (!this.isRecallSectionEnabled(
|
|
6384
7907
|
"shared-context",
|
|
@@ -7547,16 +9070,16 @@ ${formatted}`;
|
|
|
7547
9070
|
if (!this.config.compactionResetEnabled) return null;
|
|
7548
9071
|
const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
|
|
7549
9072
|
const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
|
|
7550
|
-
const signalPath =
|
|
9073
|
+
const signalPath = path3.join(
|
|
7551
9074
|
workspaceDir,
|
|
7552
9075
|
`.compaction-reset-signal-${safeSessionKey}`
|
|
7553
9076
|
);
|
|
7554
|
-
const bootPath =
|
|
9077
|
+
const bootPath = path3.join(workspaceDir, "BOOT.md");
|
|
7555
9078
|
try {
|
|
7556
|
-
const signalStat = await
|
|
9079
|
+
const signalStat = await stat2(signalPath).catch(() => null);
|
|
7557
9080
|
if (!signalStat) return null;
|
|
7558
9081
|
const signalAge = Date.now() - signalStat.mtimeMs;
|
|
7559
|
-
const signalData = JSON.parse(await
|
|
9082
|
+
const signalData = JSON.parse(await readFile3(signalPath, "utf-8"));
|
|
7560
9083
|
if (signalData.sessionKey !== effectiveSessionKey) {
|
|
7561
9084
|
log.debug(
|
|
7562
9085
|
`recall: compaction signal is for ${signalData.sessionKey}, not ${effectiveSessionKey} \u2014 skipping`
|
|
@@ -7567,7 +9090,7 @@ ${formatted}`;
|
|
|
7567
9090
|
log.debug(
|
|
7568
9091
|
`recall: stale compaction signal (${Math.round(signalAge / 1e3)}s old), skipping`
|
|
7569
9092
|
);
|
|
7570
|
-
await
|
|
9093
|
+
await unlink2(signalPath).catch(() => {
|
|
7571
9094
|
});
|
|
7572
9095
|
return null;
|
|
7573
9096
|
}
|
|
@@ -7576,7 +9099,7 @@ ${formatted}`;
|
|
|
7576
9099
|
|
|
7577
9100
|
`;
|
|
7578
9101
|
try {
|
|
7579
|
-
const bootContent = await
|
|
9102
|
+
const bootContent = await readFile3(bootPath, "utf-8");
|
|
7580
9103
|
section += "### BOOT.md (working state before compaction)\n\n";
|
|
7581
9104
|
section += bootContent + "\n";
|
|
7582
9105
|
} catch {
|
|
@@ -7587,12 +9110,12 @@ ${formatted}`;
|
|
|
7587
9110
|
log.info(
|
|
7588
9111
|
`recall: injected compaction reset context for ${effectiveSessionKey}`
|
|
7589
9112
|
);
|
|
7590
|
-
await
|
|
9113
|
+
await unlink2(signalPath).catch(() => {
|
|
7591
9114
|
});
|
|
7592
9115
|
return section;
|
|
7593
9116
|
} catch (err) {
|
|
7594
9117
|
log.debug("recall: compaction signal check failed:", err);
|
|
7595
|
-
await
|
|
9118
|
+
await unlink2(signalPath).catch(() => {
|
|
7596
9119
|
});
|
|
7597
9120
|
return null;
|
|
7598
9121
|
}
|
|
@@ -10211,7 +11734,10 @@ ${normalized}`).digest("hex");
|
|
|
10211
11734
|
result,
|
|
10212
11735
|
storage,
|
|
10213
11736
|
threadIdForExtraction,
|
|
10214
|
-
{ sessionKey, principal, validAt: sourceValidAt }
|
|
11737
|
+
{ sessionKey, principal, validAt: sourceValidAt },
|
|
11738
|
+
// Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
|
|
11739
|
+
// real namespace rather than a guess decoded from the storage dir.
|
|
11740
|
+
selfNamespace
|
|
10215
11741
|
);
|
|
10216
11742
|
let postPersistMetadataFailed = false;
|
|
10217
11743
|
meta ??= await storage.loadMeta();
|
|
@@ -10425,7 +11951,7 @@ ${normalized}`).digest("hex");
|
|
|
10425
11951
|
);
|
|
10426
11952
|
this.tierMigrationInFlight = true;
|
|
10427
11953
|
try {
|
|
10428
|
-
const coldStorage = new StorageManager(
|
|
11954
|
+
const coldStorage = new StorageManager(path3.join(storage.dir, "cold"));
|
|
10429
11955
|
const [hotMemories, coldMemories] = await Promise.all([
|
|
10430
11956
|
storage.readAllMemories(),
|
|
10431
11957
|
coldStorage.readAllMemories()
|
|
@@ -10569,22 +12095,20 @@ ${normalized}`).digest("hex");
|
|
|
10569
12095
|
this.qmdMaintenancePending = false;
|
|
10570
12096
|
try {
|
|
10571
12097
|
if (this.config.namespacesEnabled) {
|
|
10572
|
-
await this.
|
|
10573
|
-
|
|
10574
|
-
);
|
|
12098
|
+
const maintenanceNamespaces = await this.maintenanceNamespaces();
|
|
12099
|
+
await this.namespaceSearchRouter.updateNamespaces(maintenanceNamespaces);
|
|
12100
|
+
const now = Date.now();
|
|
12101
|
+
if (this.config.qmdAutoEmbedEnabled && now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs) {
|
|
12102
|
+
await this.namespaceSearchRouter.embedNamespaces(maintenanceNamespaces);
|
|
12103
|
+
this.lastQmdEmbedAtMs = now;
|
|
12104
|
+
}
|
|
10575
12105
|
} else {
|
|
10576
12106
|
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 {
|
|
12107
|
+
const now = Date.now();
|
|
12108
|
+
if (this.config.qmdAutoEmbedEnabled && now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs) {
|
|
10585
12109
|
await this.qmd.embed();
|
|
12110
|
+
this.lastQmdEmbedAtMs = now;
|
|
10586
12111
|
}
|
|
10587
|
-
this.lastQmdEmbedAtMs = now;
|
|
10588
12112
|
}
|
|
10589
12113
|
} finally {
|
|
10590
12114
|
this.qmdMaintenanceInFlight = false;
|
|
@@ -10593,7 +12117,7 @@ ${normalized}`).digest("hex");
|
|
|
10593
12117
|
}
|
|
10594
12118
|
}
|
|
10595
12119
|
}
|
|
10596
|
-
async persistExtraction(result, storage, threadIdForExtraction, sourceContext) {
|
|
12120
|
+
async persistExtraction(result, storage, threadIdForExtraction, sourceContext, baseNamespace) {
|
|
10597
12121
|
const citationEnabled = this.config.inlineSourceAttributionEnabled === true;
|
|
10598
12122
|
const citationTemplate = this.config.inlineSourceAttributionFormat;
|
|
10599
12123
|
const citationContextBase = citationEnabled ? {
|
|
@@ -10700,7 +12224,7 @@ ${normalized}`).digest("hex");
|
|
|
10700
12224
|
});
|
|
10701
12225
|
hashDedupLookupComplete = true;
|
|
10702
12226
|
if (hashDedupMatchingFact) {
|
|
10703
|
-
await applyTemporalSupersession({
|
|
12227
|
+
const hashDedupSupersession = await applyTemporalSupersession({
|
|
10704
12228
|
storage: sharedStorage,
|
|
10705
12229
|
newMemoryId: hashDedupMatchingFact.frontmatter.id,
|
|
10706
12230
|
entityRef: options.entityRef,
|
|
@@ -10709,6 +12233,9 @@ ${normalized}`).digest("hex");
|
|
|
10709
12233
|
enabled: true,
|
|
10710
12234
|
useCallerTimestamp: true
|
|
10711
12235
|
});
|
|
12236
|
+
if (hashDedupSupersession.supersededIds.length > 0) {
|
|
12237
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
12238
|
+
}
|
|
10712
12239
|
return;
|
|
10713
12240
|
}
|
|
10714
12241
|
log.debug(
|
|
@@ -10769,6 +12296,7 @@ ${normalized}`).digest("hex");
|
|
|
10769
12296
|
);
|
|
10770
12297
|
}
|
|
10771
12298
|
}
|
|
12299
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
10772
12300
|
trackPersistedId(sharedStorage, promotedId, {
|
|
10773
12301
|
includeReturnedIds: false
|
|
10774
12302
|
});
|
|
@@ -10995,6 +12523,7 @@ ${normalized}`).digest("hex");
|
|
|
10995
12523
|
fact.confidence = typeof fact.confidence === "number" ? fact.confidence : 0.7;
|
|
10996
12524
|
let writeCategory = fact.category;
|
|
10997
12525
|
let targetStorage = storage;
|
|
12526
|
+
let targetNamespaceName = baseNamespace && baseNamespace.length > 0 ? baseNamespace : this.namespaceFromStorageDir(targetStorage.dir);
|
|
10998
12527
|
let routedRuleId;
|
|
10999
12528
|
let routedNamespaceExplicit = false;
|
|
11000
12529
|
if (routeRules.length > 0) {
|
|
@@ -11011,6 +12540,7 @@ ${normalized}`).digest("hex");
|
|
|
11011
12540
|
targetStorage = await this.storageRouter.storageFor(
|
|
11012
12541
|
selected.target.namespace
|
|
11013
12542
|
);
|
|
12543
|
+
targetNamespaceName = selected.target.namespace;
|
|
11014
12544
|
}
|
|
11015
12545
|
}
|
|
11016
12546
|
} catch (err) {
|
|
@@ -11026,6 +12556,7 @@ ${normalized}`).digest("hex");
|
|
|
11026
12556
|
targetStorage = await this.storageRouter.storageFor(
|
|
11027
12557
|
this.config.sharedNamespace
|
|
11028
12558
|
);
|
|
12559
|
+
targetNamespaceName = this.config.sharedNamespace;
|
|
11029
12560
|
log.debug(
|
|
11030
12561
|
`scope-routing: fact "${fact.content.slice(0, 60)}\u2026" routed to shared namespace (scope=global)`
|
|
11031
12562
|
);
|
|
@@ -11227,34 +12758,38 @@ ${normalized}`).digest("hex");
|
|
|
11227
12758
|
contentHashSource: rawChunkedContent
|
|
11228
12759
|
}
|
|
11229
12760
|
);
|
|
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
|
-
|
|
12761
|
+
try {
|
|
12762
|
+
for (const chunk of chunkResult.chunks) {
|
|
12763
|
+
const chunkImportance = scoreImportance(
|
|
12764
|
+
chunk.content,
|
|
12765
|
+
writeCategory,
|
|
12766
|
+
fact.tags
|
|
12767
|
+
);
|
|
12768
|
+
const chunkWriteSource = fact.source === "proactive" ? "chunking-proactive" : "chunking";
|
|
12769
|
+
await targetStorage.writeChunk(
|
|
12770
|
+
parentId,
|
|
12771
|
+
chunk.index,
|
|
12772
|
+
chunkResult.chunks.length,
|
|
12773
|
+
writeCategory,
|
|
12774
|
+
// Each chunk carries its own inline citation so provenance
|
|
12775
|
+
// survives when a single chunk is quoted in isolation.
|
|
12776
|
+
applyInlineCitation(chunk.content),
|
|
12777
|
+
{
|
|
12778
|
+
confidence: fact.confidence,
|
|
12779
|
+
tags: fact.tags,
|
|
12780
|
+
entityRef: fact.entityRef,
|
|
12781
|
+
source: chunkWriteSource,
|
|
12782
|
+
importance: chunkImportance,
|
|
12783
|
+
intentGoal: inferredIntent?.goal,
|
|
12784
|
+
intentActionType: inferredIntent?.actionType,
|
|
12785
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
12786
|
+
memoryKind: memoryKind2,
|
|
12787
|
+
validAt: sourceContext?.validAt
|
|
12788
|
+
}
|
|
12789
|
+
);
|
|
12790
|
+
}
|
|
12791
|
+
} finally {
|
|
12792
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11258
12793
|
}
|
|
11259
12794
|
if (routedRuleId) {
|
|
11260
12795
|
log.debug(
|
|
@@ -11307,51 +12842,55 @@ ${normalized}`).digest("hex");
|
|
|
11307
12842
|
const chunkId = `${parentId}-chunk-${chunk.index}`;
|
|
11308
12843
|
await this.indexPersistedMemory(targetStorage, chunkId);
|
|
11309
12844
|
}
|
|
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
|
|
12845
|
+
try {
|
|
12846
|
+
if (this.config.verbatimArtifactsEnabled && this.config.verbatimArtifactCategories.includes(writeCategory) && fact.confidence >= this.config.verbatimArtifactsMinConfidence) {
|
|
12847
|
+
await targetStorage.writeArtifact(citedChunkedContent, {
|
|
12848
|
+
confidence: fact.confidence,
|
|
12849
|
+
tags: [...fact.tags, "artifact", "chunked-parent"],
|
|
12850
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
12851
|
+
sourceMemoryId: parentId,
|
|
12852
|
+
intentGoal: inferredIntent?.goal,
|
|
12853
|
+
intentActionType: inferredIntent?.actionType,
|
|
12854
|
+
intentEntityTypes: inferredIntent?.entityTypes
|
|
11339
12855
|
});
|
|
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
12856
|
}
|
|
12857
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
12858
|
+
try {
|
|
12859
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
12860
|
+
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
12861
|
+
const parentRelPath = resolvePersistedMemoryRelativePath({
|
|
12862
|
+
memoryId: parentId,
|
|
12863
|
+
pathById: graphContext.memoryPathById,
|
|
12864
|
+
category: writeCategory
|
|
12865
|
+
});
|
|
12866
|
+
graphContext.memoryPathById.set(parentId, parentRelPath);
|
|
12867
|
+
appendMemoryToGraphContext({
|
|
12868
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
12869
|
+
storageDir: targetStorage.dir,
|
|
12870
|
+
memoryRelPath: parentRelPath,
|
|
12871
|
+
memoryId: parentId,
|
|
12872
|
+
category: writeCategory,
|
|
12873
|
+
content: fact.content ?? "",
|
|
12874
|
+
entityRef
|
|
12875
|
+
});
|
|
12876
|
+
await this.buildGraphEdge(
|
|
12877
|
+
targetStorage,
|
|
12878
|
+
parentRelPath,
|
|
12879
|
+
entityRef,
|
|
12880
|
+
parentId,
|
|
12881
|
+
fact.content ?? "",
|
|
12882
|
+
graphContext.allMemsForGraph,
|
|
12883
|
+
graphContext.memoryPathById,
|
|
12884
|
+
threadIdForExtraction ?? void 0,
|
|
12885
|
+
threadEpisodeIdsForGraph,
|
|
12886
|
+
graphContext.previousPersistedRelPath
|
|
12887
|
+
);
|
|
12888
|
+
graphContext.previousPersistedRelPath = parentRelPath;
|
|
12889
|
+
} catch {
|
|
12890
|
+
}
|
|
12891
|
+
}
|
|
12892
|
+
} finally {
|
|
12893
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11355
12894
|
}
|
|
11356
12895
|
trackBehaviorSignals(
|
|
11357
12896
|
targetStorage,
|
|
@@ -11419,91 +12958,107 @@ ${normalized}`).digest("hex");
|
|
|
11419
12958
|
} catch (err) {
|
|
11420
12959
|
log.warn(`temporal-supersession: unexpected error: ${err}`);
|
|
11421
12960
|
}
|
|
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,
|
|
12961
|
+
try {
|
|
12962
|
+
trackBehaviorSignals(
|
|
12963
|
+
targetStorage,
|
|
12964
|
+
buildBehaviorSignalsForMemory({
|
|
11469
12965
|
memoryId,
|
|
11470
12966
|
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 {
|
|
12967
|
+
content: fact.content,
|
|
12968
|
+
namespace: this.namespaceFromStorageDir(targetStorage.dir),
|
|
12969
|
+
confidence: fact.confidence,
|
|
12970
|
+
source: "extraction"
|
|
12971
|
+
})
|
|
12972
|
+
);
|
|
12973
|
+
trackPersistedId(targetStorage, memoryId);
|
|
12974
|
+
if (threadEpisodeIdsForGraph && !threadEpisodeIdsForGraph.includes(memoryId)) {
|
|
12975
|
+
threadEpisodeIdsForGraph.push(memoryId);
|
|
11488
12976
|
}
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
12977
|
+
await this.indexPersistedMemory(targetStorage, memoryId);
|
|
12978
|
+
await promoteMemoryToShared({
|
|
12979
|
+
sourceStorage: targetStorage,
|
|
12980
|
+
category: writeCategory,
|
|
12981
|
+
content: fact.content,
|
|
11492
12982
|
confidence: fact.confidence,
|
|
11493
|
-
tags:
|
|
11494
|
-
|
|
12983
|
+
tags: fact.tags,
|
|
12984
|
+
entityRef: typeof fact.entityRef === "string" ? fact.entityRef : void 0,
|
|
12985
|
+
structuredAttributes: fact.structuredAttributes,
|
|
11495
12986
|
sourceMemoryId: memoryId,
|
|
12987
|
+
importance,
|
|
11496
12988
|
intentGoal: inferredIntent?.goal,
|
|
11497
12989
|
intentActionType: inferredIntent?.actionType,
|
|
11498
|
-
intentEntityTypes: inferredIntent?.entityTypes
|
|
12990
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
12991
|
+
memoryKind,
|
|
12992
|
+
validAt: sourceContext?.validAt,
|
|
12993
|
+
source: extractionWriteSource
|
|
11499
12994
|
});
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
12995
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
12996
|
+
try {
|
|
12997
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
12998
|
+
const entityRef = typeof fact.entityRef === "string" ? fact.entityRef : void 0;
|
|
12999
|
+
const memoryRelPath = resolvePersistedMemoryRelativePath({
|
|
13000
|
+
memoryId,
|
|
13001
|
+
pathById: graphContext.memoryPathById,
|
|
13002
|
+
category: writeCategory
|
|
13003
|
+
});
|
|
13004
|
+
graphContext.memoryPathById.set(memoryId, memoryRelPath);
|
|
13005
|
+
appendMemoryToGraphContext({
|
|
13006
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
13007
|
+
storageDir: targetStorage.dir,
|
|
13008
|
+
memoryRelPath,
|
|
13009
|
+
memoryId,
|
|
13010
|
+
category: writeCategory,
|
|
13011
|
+
content: fact.content ?? "",
|
|
13012
|
+
entityRef
|
|
13013
|
+
});
|
|
13014
|
+
await this.buildGraphEdge(
|
|
13015
|
+
targetStorage,
|
|
13016
|
+
memoryRelPath,
|
|
13017
|
+
entityRef,
|
|
13018
|
+
memoryId,
|
|
13019
|
+
fact.content ?? "",
|
|
13020
|
+
graphContext.allMemsForGraph,
|
|
13021
|
+
graphContext.memoryPathById,
|
|
13022
|
+
threadIdForExtraction ?? void 0,
|
|
13023
|
+
threadEpisodeIdsForGraph,
|
|
13024
|
+
graphContext.previousPersistedRelPath
|
|
13025
|
+
);
|
|
13026
|
+
graphContext.previousPersistedRelPath = memoryRelPath;
|
|
13027
|
+
} catch {
|
|
13028
|
+
}
|
|
13029
|
+
}
|
|
13030
|
+
if (this.config.verbatimArtifactsEnabled && this.config.verbatimArtifactCategories.includes(writeCategory) && fact.confidence >= this.config.verbatimArtifactsMinConfidence) {
|
|
13031
|
+
await targetStorage.writeArtifact(citedFactContent, {
|
|
13032
|
+
confidence: fact.confidence,
|
|
13033
|
+
tags: [...fact.tags, "artifact"],
|
|
13034
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
13035
|
+
sourceMemoryId: memoryId,
|
|
13036
|
+
intentGoal: inferredIntent?.goal,
|
|
13037
|
+
intentActionType: inferredIntent?.actionType,
|
|
13038
|
+
intentEntityTypes: inferredIntent?.entityTypes
|
|
13039
|
+
});
|
|
13040
|
+
}
|
|
13041
|
+
if (this.contentHashIndex) {
|
|
13042
|
+
const canonicalFactContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
|
|
13043
|
+
const hashRegisterKey = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : canonicalFactContent;
|
|
13044
|
+
this.contentHashIndex.add(hashRegisterKey);
|
|
13045
|
+
}
|
|
13046
|
+
} finally {
|
|
13047
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
11505
13048
|
}
|
|
11506
13049
|
}
|
|
13050
|
+
let durableNonFactWritten = false;
|
|
13051
|
+
let durableNonFactTouchRecorded = false;
|
|
13052
|
+
const touchBaseNonFactNamespace = () => {
|
|
13053
|
+
const baseTouchNamespace = baseNamespace && baseNamespace.length > 0 ? baseNamespace : this.namespaceFromStorageDir(storage.dir);
|
|
13054
|
+
this.markCatalogWrite(baseTouchNamespace, storage.dir);
|
|
13055
|
+
};
|
|
13056
|
+
const recordDurableNonFactWrite = () => {
|
|
13057
|
+
durableNonFactWritten = true;
|
|
13058
|
+
if (durableNonFactTouchRecorded) return;
|
|
13059
|
+
durableNonFactTouchRecorded = true;
|
|
13060
|
+
touchBaseNonFactNamespace();
|
|
13061
|
+
};
|
|
11507
13062
|
for (const entity of entities) {
|
|
11508
13063
|
try {
|
|
11509
13064
|
const name = entity?.name;
|
|
@@ -11519,7 +13074,10 @@ ${normalized}`).digest("hex");
|
|
|
11519
13074
|
principal: sourceContext?.principal,
|
|
11520
13075
|
structuredSections: Array.isArray(entity?.structuredSections) ? entity.structuredSections : void 0
|
|
11521
13076
|
});
|
|
11522
|
-
if (id)
|
|
13077
|
+
if (id) {
|
|
13078
|
+
trackPersistedId(storage, id);
|
|
13079
|
+
recordDurableNonFactWrite();
|
|
13080
|
+
}
|
|
11523
13081
|
} catch (err) {
|
|
11524
13082
|
log.warn(`persistExtraction: entity write failed: ${err}`);
|
|
11525
13083
|
}
|
|
@@ -11532,10 +13090,12 @@ ${normalized}`).digest("hex");
|
|
|
11532
13090
|
target: rel.target,
|
|
11533
13091
|
label: rel.label
|
|
11534
13092
|
});
|
|
13093
|
+
recordDurableNonFactWrite();
|
|
11535
13094
|
await storage.addEntityRelationship(rel.target, {
|
|
11536
13095
|
target: rel.source,
|
|
11537
13096
|
label: `${rel.label} (reverse)`
|
|
11538
13097
|
});
|
|
13098
|
+
recordDurableNonFactWrite();
|
|
11539
13099
|
} catch (err) {
|
|
11540
13100
|
log.debug(`relationship persist failed: ${err}`);
|
|
11541
13101
|
}
|
|
@@ -11561,18 +13121,26 @@ ${normalized}`).digest("hex");
|
|
|
11561
13121
|
}
|
|
11562
13122
|
if (profileUpdates.length > 0) {
|
|
11563
13123
|
await storage.appendToProfile(profileUpdates);
|
|
13124
|
+
recordDurableNonFactWrite();
|
|
11564
13125
|
}
|
|
11565
13126
|
for (const q of questions) {
|
|
11566
13127
|
const id = await storage.writeQuestion(q.question, q.context, q.priority);
|
|
11567
|
-
if (id)
|
|
13128
|
+
if (id) {
|
|
13129
|
+
trackPersistedId(storage, id);
|
|
13130
|
+
recordDurableNonFactWrite();
|
|
13131
|
+
}
|
|
11568
13132
|
}
|
|
11569
13133
|
if (this.config.identityEnabled && result.identityReflection) {
|
|
11570
13134
|
try {
|
|
11571
13135
|
await storage.appendIdentityReflection(result.identityReflection);
|
|
13136
|
+
recordDurableNonFactWrite();
|
|
11572
13137
|
} catch (err) {
|
|
11573
13138
|
log.debug(`identity reflection write failed: ${err}`);
|
|
11574
13139
|
}
|
|
11575
13140
|
}
|
|
13141
|
+
if (durableNonFactWritten) {
|
|
13142
|
+
touchBaseNonFactNamespace();
|
|
13143
|
+
}
|
|
11576
13144
|
if (this.contentHashIndex) {
|
|
11577
13145
|
await this.contentHashIndex.save().catch((err) => log.warn(`content-hash index save failed: ${err}`));
|
|
11578
13146
|
}
|
|
@@ -11628,7 +13196,7 @@ ${normalized}`).digest("hex");
|
|
|
11628
13196
|
const allMems = allMemsForGraph ?? [];
|
|
11629
13197
|
for (const m of allMems) {
|
|
11630
13198
|
if (m.frontmatter.entityRef === entityRef) {
|
|
11631
|
-
const rel =
|
|
13199
|
+
const rel = path3.relative(storage.dir, m.path);
|
|
11632
13200
|
if (rel !== memoryRelPath) entitySiblings.push(rel);
|
|
11633
13201
|
}
|
|
11634
13202
|
}
|
|
@@ -11730,6 +13298,7 @@ ${normalized}`).digest("hex");
|
|
|
11730
13298
|
log.info("running consolidation pass");
|
|
11731
13299
|
let merged = 0;
|
|
11732
13300
|
let invalidated = 0;
|
|
13301
|
+
let memoryItemMutated = false;
|
|
11733
13302
|
if (this.accessTrackingBuffer.size > 0) {
|
|
11734
13303
|
await this.flushAccessTracking();
|
|
11735
13304
|
}
|
|
@@ -11752,6 +13321,7 @@ ${normalized}`).digest("hex");
|
|
|
11752
13321
|
const toInvalidate = this.config.queryAwareIndexingEnabled ? memoryLookup?.get(item.existingId) ?? null : null;
|
|
11753
13322
|
if (await this.storage.invalidateMemory(item.existingId)) {
|
|
11754
13323
|
invalidated += 1;
|
|
13324
|
+
memoryItemMutated = true;
|
|
11755
13325
|
await this.embeddingFallback.removeFromIndex(item.existingId);
|
|
11756
13326
|
if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
|
|
11757
13327
|
deindexMemory(
|
|
@@ -11773,6 +13343,7 @@ ${normalized}`).digest("hex");
|
|
|
11773
13343
|
lineage: [item.existingId]
|
|
11774
13344
|
}
|
|
11775
13345
|
);
|
|
13346
|
+
memoryItemMutated = true;
|
|
11776
13347
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
11777
13348
|
}
|
|
11778
13349
|
break;
|
|
@@ -11786,6 +13357,7 @@ ${normalized}`).digest("hex");
|
|
|
11786
13357
|
lineage: [item.existingId, item.mergeWith]
|
|
11787
13358
|
}
|
|
11788
13359
|
);
|
|
13360
|
+
memoryItemMutated = true;
|
|
11789
13361
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
11790
13362
|
const toMergeInvalidate = this.config.queryAwareIndexingEnabled ? memoryLookup?.get(item.mergeWith) ?? null : null;
|
|
11791
13363
|
if (await this.storage.invalidateMemory(item.mergeWith)) {
|
|
@@ -11815,8 +13387,12 @@ ${normalized}`).digest("hex");
|
|
|
11815
13387
|
structuredSections: Array.isArray(entity?.structuredSections) ? entity.structuredSections : void 0
|
|
11816
13388
|
});
|
|
11817
13389
|
}
|
|
13390
|
+
if (result.profileUpdates.length > 0 || result.entityUpdates.length > 0) {
|
|
13391
|
+
memoryItemMutated = true;
|
|
13392
|
+
}
|
|
11818
13393
|
const entitiesMerged = await this.storage.mergeFragmentedEntities();
|
|
11819
13394
|
if (entitiesMerged > 0) {
|
|
13395
|
+
memoryItemMutated = true;
|
|
11820
13396
|
log.info(`merged ${entitiesMerged} fragmented entity files`);
|
|
11821
13397
|
}
|
|
11822
13398
|
if (this.config.entitySummaryEnabled) {
|
|
@@ -11826,6 +13402,7 @@ ${normalized}`).digest("hex");
|
|
|
11826
13402
|
5
|
|
11827
13403
|
);
|
|
11828
13404
|
if (synthesized > 0) {
|
|
13405
|
+
memoryItemMutated = true;
|
|
11829
13406
|
log.info(`refreshed ${synthesized} entity syntheses`);
|
|
11830
13407
|
}
|
|
11831
13408
|
} catch (err) {
|
|
@@ -11836,6 +13413,7 @@ ${normalized}`).digest("hex");
|
|
|
11836
13413
|
this.config.commitmentDecayDays
|
|
11837
13414
|
);
|
|
11838
13415
|
if (deletedCommitments.length > 0) {
|
|
13416
|
+
memoryItemMutated = true;
|
|
11839
13417
|
log.info(`cleaned ${deletedCommitments.length} expired commitments`);
|
|
11840
13418
|
if (this.config.queryAwareIndexingEnabled) {
|
|
11841
13419
|
for (const m of deletedCommitments) {
|
|
@@ -11857,6 +13435,7 @@ ${normalized}`).digest("hex");
|
|
|
11857
13435
|
decayDays: this.config.commitmentDecayDays
|
|
11858
13436
|
});
|
|
11859
13437
|
if (lifecycle.transitionedToExpired.length > 0 || lifecycle.deletedResolved.length > 0) {
|
|
13438
|
+
memoryItemMutated = true;
|
|
11860
13439
|
log.info(
|
|
11861
13440
|
`commitment ledger lifecycle: expired ${lifecycle.transitionedToExpired.length}, cleaned ${lifecycle.deletedResolved.length}`
|
|
11862
13441
|
);
|
|
@@ -11867,6 +13446,7 @@ ${normalized}`).digest("hex");
|
|
|
11867
13446
|
}
|
|
11868
13447
|
const deletedTTL = await this.storage.cleanExpiredTTL();
|
|
11869
13448
|
if (deletedTTL.length > 0) {
|
|
13449
|
+
memoryItemMutated = true;
|
|
11870
13450
|
log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
|
|
11871
13451
|
if (this.config.queryAwareIndexingEnabled) {
|
|
11872
13452
|
for (const m of deletedTTL) {
|
|
@@ -11883,7 +13463,9 @@ ${normalized}`).digest("hex");
|
|
|
11883
13463
|
try {
|
|
11884
13464
|
const lightSleepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11885
13465
|
const lifecycleCorpus = await this.storage.readAllMemories();
|
|
11886
|
-
await this.runLifecyclePolicyPass(lifecycleCorpus)
|
|
13466
|
+
if (await this.runLifecyclePolicyPass(lifecycleCorpus) > 0) {
|
|
13467
|
+
memoryItemMutated = true;
|
|
13468
|
+
}
|
|
11887
13469
|
await this.recordScheduledDreamsPhaseRun(
|
|
11888
13470
|
"lightSleep",
|
|
11889
13471
|
lifecycleCorpus.length,
|
|
@@ -11900,11 +13482,13 @@ ${normalized}`).digest("hex");
|
|
|
11900
13482
|
await this.runCompressionGuidelineLearningPass();
|
|
11901
13483
|
try {
|
|
11902
13484
|
const deepSleepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11903
|
-
await this.runTierMigrationCycle(this.storage, "maintenance");
|
|
13485
|
+
const tierMigration = await this.runTierMigrationCycle(this.storage, "maintenance");
|
|
13486
|
+
if (tierMigration.migrated > 0) memoryItemMutated = true;
|
|
11904
13487
|
allMemories = await this.storage.readAllMemories();
|
|
11905
13488
|
if (this.config.factArchivalEnabled) {
|
|
11906
13489
|
const archived = await this.runFactArchival(allMemories);
|
|
11907
13490
|
if (archived > 0) {
|
|
13491
|
+
memoryItemMutated = true;
|
|
11908
13492
|
log.info(`archived ${archived} old low-importance facts`);
|
|
11909
13493
|
}
|
|
11910
13494
|
}
|
|
@@ -11928,14 +13512,14 @@ ${normalized}`).digest("hex");
|
|
|
11928
13512
|
}
|
|
11929
13513
|
if (this.config.semanticConsolidationEnabled) {
|
|
11930
13514
|
try {
|
|
11931
|
-
const stateFilePath =
|
|
13515
|
+
const stateFilePath = path3.join(
|
|
11932
13516
|
this.config.memoryDir,
|
|
11933
13517
|
"state",
|
|
11934
13518
|
"semantic-consolidation-last-run.json"
|
|
11935
13519
|
);
|
|
11936
13520
|
let shouldRun = true;
|
|
11937
13521
|
try {
|
|
11938
|
-
const stateRaw = await
|
|
13522
|
+
const stateRaw = await readFile3(stateFilePath, "utf-8");
|
|
11939
13523
|
const stateData = JSON.parse(stateRaw);
|
|
11940
13524
|
if (stateData.lastRunAt) {
|
|
11941
13525
|
const lastRunMs = new Date(stateData.lastRunAt).getTime();
|
|
@@ -11976,9 +13560,9 @@ ${normalized}`).digest("hex");
|
|
|
11976
13560
|
);
|
|
11977
13561
|
}
|
|
11978
13562
|
if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
|
|
11979
|
-
const stateDir =
|
|
11980
|
-
await
|
|
11981
|
-
await
|
|
13563
|
+
const stateDir = path3.join(this.config.memoryDir, "state");
|
|
13564
|
+
await mkdir3(stateDir, { recursive: true });
|
|
13565
|
+
await writeFile3(
|
|
11982
13566
|
stateFilePath,
|
|
11983
13567
|
JSON.stringify({ lastRunAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
11984
13568
|
"utf-8"
|
|
@@ -12009,6 +13593,7 @@ ${normalized}`).digest("hex");
|
|
|
12009
13593
|
);
|
|
12010
13594
|
if (profileResult) {
|
|
12011
13595
|
await this.storage.writeProfile(profileResult.consolidatedProfile);
|
|
13596
|
+
memoryItemMutated = true;
|
|
12012
13597
|
log.info(
|
|
12013
13598
|
`profile.md consolidated: removed ${profileResult.removedCount} items \u2014 ${profileResult.summary}`
|
|
12014
13599
|
);
|
|
@@ -12080,6 +13665,12 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12080
13665
|
}
|
|
12081
13666
|
}
|
|
12082
13667
|
}
|
|
13668
|
+
if (memoryItemMutated) {
|
|
13669
|
+
this.markCatalogWrite(
|
|
13670
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
13671
|
+
this.storage.dir
|
|
13672
|
+
);
|
|
13673
|
+
}
|
|
12083
13674
|
log.info("consolidation complete");
|
|
12084
13675
|
return { memoriesProcessed: allMemories.length, merged, invalidated };
|
|
12085
13676
|
}
|
|
@@ -12398,7 +13989,9 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12398
13989
|
}
|
|
12399
13990
|
async runLifecyclePolicyNow(storage = this.storage) {
|
|
12400
13991
|
const lifecycleCorpus = await storage.readAllMemories();
|
|
12401
|
-
await this.runLifecyclePolicyPass(lifecycleCorpus, storage)
|
|
13992
|
+
if (await this.runLifecyclePolicyPass(lifecycleCorpus, storage) > 0) {
|
|
13993
|
+
this.markCatalogWrite(this.namespaceFromStorageDir(storage.dir), storage.dir);
|
|
13994
|
+
}
|
|
12402
13995
|
return { memoriesAssessed: lifecycleCorpus.length };
|
|
12403
13996
|
}
|
|
12404
13997
|
async runLifecyclePolicyPass(allMemories, storage = this.storage) {
|
|
@@ -12454,7 +14047,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12454
14047
|
});
|
|
12455
14048
|
if (wrote) updatedCount += 1;
|
|
12456
14049
|
}
|
|
12457
|
-
if (!this.config.lifecycleMetricsEnabled) return;
|
|
14050
|
+
if (!this.config.lifecycleMetricsEnabled) return updatedCount;
|
|
12458
14051
|
const total = evaluatedCount;
|
|
12459
14052
|
const metrics = {
|
|
12460
14053
|
generatedAt: nowIso,
|
|
@@ -12471,13 +14064,14 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12471
14064
|
protectedCategories: this.config.lifecycleProtectedCategories
|
|
12472
14065
|
}
|
|
12473
14066
|
};
|
|
12474
|
-
const metricsPath =
|
|
14067
|
+
const metricsPath = path3.join(
|
|
12475
14068
|
storage.dir,
|
|
12476
14069
|
"state",
|
|
12477
14070
|
"lifecycle-metrics.json"
|
|
12478
14071
|
);
|
|
12479
|
-
await
|
|
12480
|
-
await
|
|
14072
|
+
await mkdir3(path3.dirname(metricsPath), { recursive: true });
|
|
14073
|
+
await writeFile3(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
|
|
14074
|
+
return updatedCount;
|
|
12481
14075
|
}
|
|
12482
14076
|
/**
|
|
12483
14077
|
* Archive old, low-importance, rarely-accessed facts (v6.0).
|
|
@@ -12549,8 +14143,8 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12549
14143
|
const sorted = activeMemories.sort(
|
|
12550
14144
|
(a, b) => new Date(a.frontmatter.created).getTime() - new Date(b.frontmatter.created).getTime()
|
|
12551
14145
|
);
|
|
12552
|
-
const
|
|
12553
|
-
const toSummarize = sorted.slice(0, -
|
|
14146
|
+
const recentToKeep = Math.max(0, this.config.summarizationRecentToKeep);
|
|
14147
|
+
const toSummarize = recentToKeep > 0 ? sorted.slice(0, -recentToKeep) : sorted;
|
|
12554
14148
|
const candidates = toSummarize.filter((m) => {
|
|
12555
14149
|
if (m.frontmatter.entityRef) return false;
|
|
12556
14150
|
const protectedTags = this.config.summarizationProtectedTags;
|
|
@@ -12593,6 +14187,10 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12593
14187
|
batch.map((m) => m.frontmatter.id),
|
|
12594
14188
|
summary.id
|
|
12595
14189
|
);
|
|
14190
|
+
this.markCatalogWrite(
|
|
14191
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
14192
|
+
this.storage.dir
|
|
14193
|
+
);
|
|
12596
14194
|
log.info(
|
|
12597
14195
|
`created summary ${summary.id} from ${batch.length} memories, archived ${archived}`
|
|
12598
14196
|
);
|
|
@@ -12618,7 +14216,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
|
|
|
12618
14216
|
/** Threshold (bytes) at which IDENTITY.md reflections get auto-consolidated */
|
|
12619
14217
|
static IDENTITY_CONSOLIDATE_THRESHOLD = 8e3;
|
|
12620
14218
|
async autoConsolidateIdentity() {
|
|
12621
|
-
const namespaces = this.config.namespacesEnabled ? this.
|
|
14219
|
+
const namespaces = this.config.namespacesEnabled ? await this.maintenanceNamespaces() : [this.config.defaultNamespace];
|
|
12622
14220
|
for (const namespace of namespaces) {
|
|
12623
14221
|
const storage = await this.storageRouter.storageFor(namespace);
|
|
12624
14222
|
const identityNamespace = this.config.namespacesEnabled && namespace !== this.config.defaultNamespace ? namespace : void 0;
|
|
@@ -12661,6 +14259,7 @@ ${reflectionsContent.trim()}
|
|
|
12661
14259
|
identityNamespace
|
|
12662
14260
|
);
|
|
12663
14261
|
await storage.writeIdentityReflections("");
|
|
14262
|
+
this.markCatalogWrite(namespace, storage.dir);
|
|
12664
14263
|
log.info(
|
|
12665
14264
|
`IDENTITY(${namespace}) consolidated: ${identityContent.length} \u2192 ${newContent.length} chars, ${result.learnedPatterns.length} patterns`
|
|
12666
14265
|
);
|
|
@@ -13004,7 +14603,7 @@ ${lines.join("\n\n")}`;
|
|
|
13004
14603
|
const seenStorageDirs = /* @__PURE__ */ new Set();
|
|
13005
14604
|
const addStorage = (storage) => {
|
|
13006
14605
|
const storageDir = storageDirFor(storage);
|
|
13007
|
-
const storageKey = storageDir ?
|
|
14606
|
+
const storageKey = storageDir ? path3.resolve(storageDir) : `storage-without-dir-${storages.length}`;
|
|
13008
14607
|
if (seenStorageDirs.has(storageKey)) return;
|
|
13009
14608
|
seenStorageDirs.add(storageKey);
|
|
13010
14609
|
storages.push(storage);
|
|
@@ -13034,7 +14633,7 @@ ${lines.join("\n\n")}`;
|
|
|
13034
14633
|
continue;
|
|
13035
14634
|
}
|
|
13036
14635
|
try {
|
|
13037
|
-
const coldRoot =
|
|
14636
|
+
const coldRoot = path3.join(storageDir, "cold");
|
|
13038
14637
|
for (const candidate of qmdResultPathCandidates(
|
|
13039
14638
|
coldRoot,
|
|
13040
14639
|
parts.relativePath
|
|
@@ -13075,7 +14674,7 @@ ${lines.join("\n\n")}`;
|
|
|
13075
14674
|
return null;
|
|
13076
14675
|
}
|
|
13077
14676
|
}
|
|
13078
|
-
if (
|
|
14677
|
+
if (path3.isAbsolute(resultPath)) {
|
|
13079
14678
|
if (!fallbackStorageDir) {
|
|
13080
14679
|
return await fallbackStorage.readMemoryByPath(resultPath);
|
|
13081
14680
|
}
|
|
@@ -13114,7 +14713,7 @@ ${lines.join("\n\n")}`;
|
|
|
13114
14713
|
);
|
|
13115
14714
|
if (!memory) return null;
|
|
13116
14715
|
let ownerNamespace = null;
|
|
13117
|
-
if (
|
|
14716
|
+
if (path3.isAbsolute(memory.path)) {
|
|
13118
14717
|
const ownerStorage = await this.storageForAbsoluteQmdResultPath(
|
|
13119
14718
|
memory.path,
|
|
13120
14719
|
fallbackStorage,
|
|
@@ -13138,16 +14737,16 @@ ${lines.join("\n\n")}`;
|
|
|
13138
14737
|
};
|
|
13139
14738
|
}
|
|
13140
14739
|
async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
|
|
13141
|
-
const resolvedPath =
|
|
13142
|
-
const memoryRoot =
|
|
13143
|
-
const namespacesRoot =
|
|
14740
|
+
const resolvedPath = path3.resolve(resultPath);
|
|
14741
|
+
const memoryRoot = path3.resolve(this.config.memoryDir);
|
|
14742
|
+
const namespacesRoot = path3.join(memoryRoot, "namespaces");
|
|
13144
14743
|
const fallbackStorageDir = typeof fallbackStorage.dir === "string" && fallbackStorage.dir ? fallbackStorage.dir : null;
|
|
13145
14744
|
const matches = [];
|
|
13146
14745
|
const seenDirs = /* @__PURE__ */ new Set();
|
|
13147
14746
|
const maybeAddStorage = (storage, namespace) => {
|
|
13148
14747
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
13149
14748
|
if (!storageDir) return;
|
|
13150
|
-
const candidateRoot =
|
|
14749
|
+
const candidateRoot = path3.resolve(storageDir);
|
|
13151
14750
|
if (seenDirs.has(candidateRoot)) return;
|
|
13152
14751
|
if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
|
|
13153
14752
|
if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
@@ -13165,7 +14764,7 @@ ${lines.join("\n\n")}`;
|
|
|
13165
14764
|
candidateNamespaces.add(ns);
|
|
13166
14765
|
}
|
|
13167
14766
|
if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
|
|
13168
|
-
const relativeToNamespaces =
|
|
14767
|
+
const relativeToNamespaces = path3.relative(namespacesRoot, resolvedPath);
|
|
13169
14768
|
const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
|
|
13170
14769
|
if (namespaceSegment) {
|
|
13171
14770
|
candidateNamespaces.add(
|
|
@@ -13210,7 +14809,7 @@ ${lines.join("\n\n")}`;
|
|
|
13210
14809
|
nsMap = buildMemoryWorthCounterMap(memories);
|
|
13211
14810
|
this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
|
|
13212
14811
|
}
|
|
13213
|
-
for (const [
|
|
14812
|
+
for (const [path4, c] of nsMap) counters.set(path4, c);
|
|
13214
14813
|
} catch (err) {
|
|
13215
14814
|
log.debug("memory-worth: failed to read namespace, skipping", {
|
|
13216
14815
|
namespace: ns,
|
|
@@ -13381,12 +14980,12 @@ ${lines.join("\n\n")}`;
|
|
|
13381
14980
|
*/
|
|
13382
14981
|
semanticDedupScopeFor(targetStorage) {
|
|
13383
14982
|
if (!this.config.namespacesEnabled) return {};
|
|
13384
|
-
const memoryDir =
|
|
13385
|
-
const storageDir =
|
|
14983
|
+
const memoryDir = path3.resolve(this.config.memoryDir);
|
|
14984
|
+
const storageDir = path3.resolve(targetStorage.dir);
|
|
13386
14985
|
if (storageDir === memoryDir) {
|
|
13387
14986
|
return { pathExcludePrefixes: ["namespaces/"] };
|
|
13388
14987
|
}
|
|
13389
|
-
let rel =
|
|
14988
|
+
let rel = path3.relative(memoryDir, storageDir);
|
|
13390
14989
|
if (!rel || rel.startsWith("..")) {
|
|
13391
14990
|
log.debug(
|
|
13392
14991
|
`semantic dedup: target storage dir ${storageDir} is outside memoryDir ${memoryDir}; scoping lookup to absolute path prefix`
|
|
@@ -13405,7 +15004,7 @@ ${lines.join("\n\n")}`;
|
|
|
13405
15004
|
if (hits.length === 0) return [];
|
|
13406
15005
|
const results = [];
|
|
13407
15006
|
for (const hit of hits) {
|
|
13408
|
-
const fullPath =
|
|
15007
|
+
const fullPath = path3.isAbsolute(hit.path) ? hit.path : path3.join(this.config.memoryDir, hit.path);
|
|
13409
15008
|
const memory = await this.storage.readMemoryByPath(fullPath);
|
|
13410
15009
|
if (!memory) continue;
|
|
13411
15010
|
results.push({
|
|
@@ -13580,7 +15179,7 @@ ${lines.join("\n\n")}`;
|
|
|
13580
15179
|
const storage = await this.storageRouter.storageFor(namespace);
|
|
13581
15180
|
const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
|
|
13582
15181
|
if (!storageDir) continue;
|
|
13583
|
-
const recallRoot =
|
|
15182
|
+
const recallRoot = path3.resolve(storageDir);
|
|
13584
15183
|
if (seenRecallRoots.has(recallRoot)) continue;
|
|
13585
15184
|
seenRecallRoots.add(recallRoot);
|
|
13586
15185
|
recallRoots.push(recallRoot);
|
|
@@ -13604,8 +15203,8 @@ ${lines.join("\n\n")}`;
|
|
|
13604
15203
|
if (resolvedCold) scopedResults.push(resolvedCold.result);
|
|
13605
15204
|
continue;
|
|
13606
15205
|
}
|
|
13607
|
-
if (
|
|
13608
|
-
const resolvedPath =
|
|
15206
|
+
if (path3.isAbsolute(result.path)) {
|
|
15207
|
+
const resolvedPath = path3.resolve(result.path);
|
|
13609
15208
|
if (recallRoots.some(
|
|
13610
15209
|
(recallRoot) => isPathInsideStorageRoot(recallRoot, resolvedPath)
|
|
13611
15210
|
)) {
|
|
@@ -14346,13 +15945,65 @@ ${lines.join("\n\n")}`;
|
|
|
14346
15945
|
}
|
|
14347
15946
|
namespaceFromStorageDir(storageDir) {
|
|
14348
15947
|
if (!this.config.namespacesEnabled) return this.config.defaultNamespace;
|
|
14349
|
-
const resolvedStorageDir =
|
|
14350
|
-
const resolvedMemoryDir =
|
|
15948
|
+
const resolvedStorageDir = path3.resolve(storageDir);
|
|
15949
|
+
const resolvedMemoryDir = path3.resolve(this.config.memoryDir);
|
|
14351
15950
|
if (resolvedStorageDir === resolvedMemoryDir)
|
|
14352
15951
|
return this.config.defaultNamespace;
|
|
14353
15952
|
const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
|
|
14354
15953
|
if (!m?.[1]) return this.config.defaultNamespace;
|
|
14355
|
-
|
|
15954
|
+
const dirName = m[1];
|
|
15955
|
+
if (this.configuredNamespaces().includes(dirName)) {
|
|
15956
|
+
return dirName;
|
|
15957
|
+
}
|
|
15958
|
+
this.loadNamespaceStorageDirHintsFromCatalog();
|
|
15959
|
+
const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
15960
|
+
if (hintedNamespaces?.has(dirName)) {
|
|
15961
|
+
return dirName;
|
|
15962
|
+
}
|
|
15963
|
+
if (hintedNamespaces?.size === 1) {
|
|
15964
|
+
const [hintedNamespace] = hintedNamespaces;
|
|
15965
|
+
if (hintedNamespace) return hintedNamespace;
|
|
15966
|
+
}
|
|
15967
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
15968
|
+
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
15969
|
+
return decoded;
|
|
15970
|
+
}
|
|
15971
|
+
return dirName;
|
|
15972
|
+
}
|
|
15973
|
+
/**
|
|
15974
|
+
* Record a namespace write in the catalog (issue #1499). Best-effort and
|
|
15975
|
+
* failure-tolerant: a catalog write error MUST NOT crash the primary memory
|
|
15976
|
+
* write (CLAUDE.md gotcha #13, rule #40). Fire-and-forget by design.
|
|
15977
|
+
*/
|
|
15978
|
+
markCatalogWrite(namespace, storageDir) {
|
|
15979
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
15980
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
15981
|
+
void this.namespaceCatalog.markWrite(namespace, { discoveredBy: "write", storageDir }).catch(() => void 0);
|
|
15982
|
+
}
|
|
15983
|
+
/**
|
|
15984
|
+
* Public best-effort catalog write touch (issue #1499). User-facing explicit
|
|
15985
|
+
* captures (`memory_store`) and review-queue approvals persist via
|
|
15986
|
+
* `persistExplicitCapture()` → `storage.writeMemory()`, which bypasses the
|
|
15987
|
+
* extraction write path that calls `markCatalogWrite`. Without this their
|
|
15988
|
+
* namespaces never record `lastWriteAt`, so the catalog under-reports write
|
|
15989
|
+
* recency (round 5, codex P2). Fire-and-forget and failure-tolerant — a
|
|
15990
|
+
* catalog error must never affect the explicit write (gotcha #13, rule #40).
|
|
15991
|
+
*
|
|
15992
|
+
* An undefined/empty `namespace` means the write targeted the DEFAULT namespace
|
|
15993
|
+
* (`getStorage(undefined)` routes there), so we record it under the configured
|
|
15994
|
+
* default rather than skipping it (round 6, codex P2 — default `memory_store`
|
|
15995
|
+
* and inline-note writes were missing from `writtenSince`/maintenance).
|
|
15996
|
+
*/
|
|
15997
|
+
recordCatalogWrite(namespace, storageDir) {
|
|
15998
|
+
const ns = namespace && namespace.trim().length > 0 ? namespace : this.config.defaultNamespace;
|
|
15999
|
+
if (!ns) return;
|
|
16000
|
+
this.markCatalogWrite(ns, storageDir);
|
|
16001
|
+
}
|
|
16002
|
+
/** Record a namespace read in the catalog. Best-effort, failure-tolerant. */
|
|
16003
|
+
markCatalogRead(namespace, storageDir) {
|
|
16004
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
16005
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
16006
|
+
void this.namespaceCatalog.markRead(namespace, { discoveredBy: "read", storageDir }).catch(() => void 0);
|
|
14356
16007
|
}
|
|
14357
16008
|
async readAllMemoriesForNamespaces(namespaces) {
|
|
14358
16009
|
const uniq = Array.from(new Set(namespaces.filter(Boolean)));
|
|
@@ -14394,6 +16045,7 @@ export {
|
|
|
14394
16045
|
ensureBuiltInWearableConnectors,
|
|
14395
16046
|
WearablesService,
|
|
14396
16047
|
locateTranscriptPath,
|
|
16048
|
+
NamespaceCatalog,
|
|
14397
16049
|
BulkImportBatchPartialFailureError,
|
|
14398
16050
|
dedupeEntitySynthesisEvidenceEntries,
|
|
14399
16051
|
defaultWorkspaceDir,
|
|
@@ -14424,4 +16076,4 @@ export {
|
|
|
14424
16076
|
resolvePersistedMemoryRelativePath,
|
|
14425
16077
|
Orchestrator
|
|
14426
16078
|
};
|
|
14427
|
-
//# sourceMappingURL=chunk-
|
|
16079
|
+
//# sourceMappingURL=chunk-OL2364SB.js.map
|