@remnic/core 9.3.699 → 9.3.701
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +14 -10
- package/dist/access-cli.js +30 -28
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +18 -14
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +17 -13
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +16 -12
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DftqtNUy.d.ts → access-service-CGVWK6lZ.d.ts} +2 -2
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +13 -9
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/action-confidence.js +2 -2
- 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 +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -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/capabilities.d.ts +1 -1
- package/dist/{catalog-D7YDNNF7.d.ts → catalog-DBIghceA.d.ts} +27 -53
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +8 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-E6GOVHHJ.js → chunk-27LQPUMZ.js} +3 -3
- package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
- package/dist/chunk-3FAMU5TX.js.map +1 -0
- package/dist/chunk-3JJWNZTT.js +446 -0
- package/dist/chunk-3JJWNZTT.js.map +1 -0
- package/dist/{chunk-JYVD2XZ4.js → chunk-4HIAWLA2.js} +58 -25
- package/dist/chunk-4HIAWLA2.js.map +1 -0
- package/dist/{chunk-A62RAIBN.js → chunk-6W2D6FGG.js} +2 -2
- package/dist/{chunk-COEZR6F5.js → chunk-7TAQEPLE.js} +2 -2
- package/dist/{chunk-TIVZ4MCG.js → chunk-D75JXBV4.js} +3 -3
- package/dist/{chunk-YY2HGKWR.js → chunk-DEDQXIDL.js} +2 -2
- package/dist/{chunk-XFG3PVZE.js → chunk-FUCJAZ25.js} +8 -8
- package/dist/{chunk-EZWQZYLK.js → chunk-HF4N43Q7.js} +2 -2
- package/dist/{chunk-HZ5DRA4Q.js → chunk-HXHKLVAS.js} +20 -20
- package/dist/{chunk-AC5LO7IU.js → chunk-I4WBNM3M.js} +7 -1
- package/dist/chunk-I4WBNM3M.js.map +1 -0
- package/dist/{chunk-QVTSLRKT.js → chunk-IJEZMWKA.js} +3 -3
- package/dist/{chunk-ZXUOAFUG.js → chunk-IKNQAGBV.js} +1 -1
- package/dist/chunk-IKNQAGBV.js.map +1 -0
- package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
- package/dist/{chunk-2ULWWAQH.js → chunk-ISLJ5WIM.js} +2 -2
- package/dist/{chunk-2KJG6ZZK.js → chunk-IYOPIG3E.js} +2 -2
- package/dist/{chunk-M4DQWUKX.js → chunk-JKOKX3PS.js} +62 -163
- package/dist/chunk-JKOKX3PS.js.map +1 -0
- package/dist/{chunk-SJQ4HY3E.js → chunk-JO3E5VGS.js} +2 -2
- package/dist/{chunk-3LSSFCB4.js → chunk-K4DWSPMW.js} +11 -1
- package/dist/chunk-K4DWSPMW.js.map +1 -0
- package/dist/{chunk-IZ5E6ZTK.js → chunk-KF4TXW7Z.js} +6 -6
- package/dist/{chunk-CQ4PGFMC.js → chunk-KS7WQ4BZ.js} +4 -4
- package/dist/chunk-LTJAMRGI.js +286 -0
- package/dist/chunk-LTJAMRGI.js.map +1 -0
- package/dist/{chunk-PJIHCOGV.js → chunk-MNU5G4TK.js} +2 -2
- package/dist/{chunk-2K64VH66.js → chunk-ODTWHSY2.js} +69 -45
- package/dist/chunk-ODTWHSY2.js.map +1 -0
- package/dist/{chunk-LIWU4QI6.js → chunk-OLOYQZFB.js} +5 -5
- package/dist/{chunk-MRFVMSTR.js → chunk-OMNTQPZO.js} +20 -1
- package/dist/{chunk-MRFVMSTR.js.map → chunk-OMNTQPZO.js.map} +1 -1
- package/dist/{chunk-JI6HWBYL.js → chunk-Q4O3ET6F.js} +2 -2
- package/dist/{chunk-N55RJT4N.js → chunk-QP37KL5H.js} +2 -2
- package/dist/{chunk-LPIWEKZ3.js → chunk-SDPDU2PM.js} +3 -3
- package/dist/{chunk-RIC5U67B.js → chunk-SFMRLXIV.js} +232 -4
- package/dist/chunk-SFMRLXIV.js.map +1 -0
- package/dist/{chunk-RTN2BLZM.js → chunk-T5QAZIBO.js} +2 -2
- package/dist/{chunk-ANZLT74L.js → chunk-TFVVONWD.js} +2 -2
- package/dist/{chunk-SIDSEXUG.js → chunk-WFEZUGU5.js} +2 -2
- package/dist/{chunk-FSEQXHEZ.js → chunk-XTIRCSIH.js} +2 -2
- package/dist/{chunk-XL5RSHZP.js → chunk-YO4MBK3I.js} +2 -2
- package/dist/{chunk-AH2JUU6X.js → chunk-Z2YFLXHU.js} +2 -2
- package/dist/{chunk-MRX6S22R.js → chunk-ZT7B64BE.js} +2 -2
- package/dist/{chunk-UDDSC6PO.js → chunk-ZYNMX6IU.js} +54 -6
- package/dist/chunk-ZYNMX6IU.js.map +1 -0
- package/dist/{cli-DUMkkdLl.d.ts → cli-D3XeenwN.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +32 -28
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -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 +7 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +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 +7 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{forget-PLR6J5DN.js → forget-6SOIPUMQ.js} +32 -1
- package/dist/forget-6SOIPUMQ.js.map +1 -0
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +65 -43
- 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 +7 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -3
- package/dist/maintenance/rebuild-memory-projection.js +8 -4
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-provenance.js +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 +2 -2
- package/dist/namespaces/migrate.js +8 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +32 -3
- package/dist/namespaces/storage.js +7 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +19 -2
- package/dist/operator-toolkit.js +17 -11
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +9 -5
- package/dist/{orchestrator-Dv8JQu3-.d.ts → orchestrator-BzMCZlKn.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +22 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +4 -4
- 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 +5 -5
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +4 -4
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +3 -3
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +28 -28
- 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-CgREi9E0.d.ts → semantic-consolidation-BtUfv-AL.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +8 -4
- package/dist/semantic-rule-promotion.js +7 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +7 -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 +306 -2
- package/dist/storage.js +6 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -2
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/summary-snapshot.js +2 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-supersession.js +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-CeZYSC-E.d.ts → types-PuiPZ9iE.d.ts} +34 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/utils/serialize-mutations.js +5 -280
- package/dist/utils/serialize-mutations.js.map +1 -1
- package/dist/verified-recall.js +7 -3
- package/package.json +2 -2
- package/src/config.ts +10 -0
- package/src/lifecycle/tombstones.test.ts +355 -0
- package/src/lifecycle/tombstones.ts +807 -0
- package/src/maintenance/forget.ts +41 -0
- package/src/maintenance/pattern-reinforcement.ts +47 -0
- package/src/memory-provenance.ts +12 -0
- package/src/namespaces/catalog.test.ts +222 -14
- package/src/namespaces/catalog.ts +54 -187
- package/src/namespaces/storage.ts +120 -80
- package/src/operator-toolkit.ts +77 -0
- package/src/orchestrator.ts +16 -0
- package/src/review/index.ts +48 -3
- package/src/storage.ts +329 -2
- package/src/summary-snapshot.test.ts +63 -1
- package/src/summary-snapshot.ts +61 -80
- package/src/temporal-supersession.ts +38 -0
- package/src/types.ts +34 -0
- package/src/utils/serialize-mutations.ts +10 -6
- package/dist/chunk-2K64VH66.js.map +0 -1
- package/dist/chunk-3LSSFCB4.js.map +0 -1
- package/dist/chunk-AC5LO7IU.js.map +0 -1
- package/dist/chunk-JYVD2XZ4.js.map +0 -1
- package/dist/chunk-M4DQWUKX.js.map +0 -1
- package/dist/chunk-RIC5U67B.js.map +0 -1
- package/dist/chunk-UDDSC6PO.js.map +0 -1
- package/dist/chunk-XJNBEDFE.js.map +0 -1
- package/dist/chunk-ZXUOAFUG.js.map +0 -1
- package/dist/forget-PLR6J5DN.js.map +0 -1
- /package/dist/{chunk-E6GOVHHJ.js.map → chunk-27LQPUMZ.js.map} +0 -0
- /package/dist/{chunk-A62RAIBN.js.map → chunk-6W2D6FGG.js.map} +0 -0
- /package/dist/{chunk-COEZR6F5.js.map → chunk-7TAQEPLE.js.map} +0 -0
- /package/dist/{chunk-TIVZ4MCG.js.map → chunk-D75JXBV4.js.map} +0 -0
- /package/dist/{chunk-YY2HGKWR.js.map → chunk-DEDQXIDL.js.map} +0 -0
- /package/dist/{chunk-XFG3PVZE.js.map → chunk-FUCJAZ25.js.map} +0 -0
- /package/dist/{chunk-EZWQZYLK.js.map → chunk-HF4N43Q7.js.map} +0 -0
- /package/dist/{chunk-HZ5DRA4Q.js.map → chunk-HXHKLVAS.js.map} +0 -0
- /package/dist/{chunk-QVTSLRKT.js.map → chunk-IJEZMWKA.js.map} +0 -0
- /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
- /package/dist/{chunk-2ULWWAQH.js.map → chunk-ISLJ5WIM.js.map} +0 -0
- /package/dist/{chunk-2KJG6ZZK.js.map → chunk-IYOPIG3E.js.map} +0 -0
- /package/dist/{chunk-SJQ4HY3E.js.map → chunk-JO3E5VGS.js.map} +0 -0
- /package/dist/{chunk-IZ5E6ZTK.js.map → chunk-KF4TXW7Z.js.map} +0 -0
- /package/dist/{chunk-CQ4PGFMC.js.map → chunk-KS7WQ4BZ.js.map} +0 -0
- /package/dist/{chunk-PJIHCOGV.js.map → chunk-MNU5G4TK.js.map} +0 -0
- /package/dist/{chunk-LIWU4QI6.js.map → chunk-OLOYQZFB.js.map} +0 -0
- /package/dist/{chunk-JI6HWBYL.js.map → chunk-Q4O3ET6F.js.map} +0 -0
- /package/dist/{chunk-N55RJT4N.js.map → chunk-QP37KL5H.js.map} +0 -0
- /package/dist/{chunk-LPIWEKZ3.js.map → chunk-SDPDU2PM.js.map} +0 -0
- /package/dist/{chunk-RTN2BLZM.js.map → chunk-T5QAZIBO.js.map} +0 -0
- /package/dist/{chunk-ANZLT74L.js.map → chunk-TFVVONWD.js.map} +0 -0
- /package/dist/{chunk-SIDSEXUG.js.map → chunk-WFEZUGU5.js.map} +0 -0
- /package/dist/{chunk-FSEQXHEZ.js.map → chunk-XTIRCSIH.js.map} +0 -0
- /package/dist/{chunk-XL5RSHZP.js.map → chunk-YO4MBK3I.js.map} +0 -0
- /package/dist/{chunk-AH2JUU6X.js.map → chunk-Z2YFLXHU.js.map} +0 -0
- /package/dist/{chunk-MRX6S22R.js.map → chunk-ZT7B64BE.js.map} +0 -0
package/src/summary-snapshot.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
mkdir,
|
|
3
|
-
open,
|
|
4
|
-
readFile,
|
|
5
|
-
stat,
|
|
6
|
-
unlink,
|
|
7
|
-
utimes,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
2
|
import path from "node:path";
|
|
11
3
|
import { z } from "zod";
|
|
12
4
|
import {
|
|
13
5
|
encodeStoragePathSegment,
|
|
14
6
|
resolvePathInsideStorageRoot,
|
|
15
7
|
} from "./storage-paths.js";
|
|
8
|
+
import {
|
|
9
|
+
serializeMutations,
|
|
10
|
+
withHeldFileLock,
|
|
11
|
+
} from "./utils/serialize-mutations.js";
|
|
16
12
|
import type { HourlySummary } from "./types.js";
|
|
17
13
|
|
|
18
14
|
const summarySnapshotSchemaVersion = 1;
|
|
@@ -34,7 +30,9 @@ const SummarySnapshotSchema = z.object({
|
|
|
34
30
|
|
|
35
31
|
type SummarySnapshot = z.infer<typeof SummarySnapshotSchema>;
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
// Lock timings for the cross-process summary-snapshot file lock. These are
|
|
34
|
+
// passed straight through to the shared `withHeldFileLock` utility (issue
|
|
35
|
+
// #1524 adoption); the previous bespoke lock used the same numbers.
|
|
38
36
|
const summarySnapshotLockTimeoutMs = 5_000;
|
|
39
37
|
const summarySnapshotLockStaleMs = 30_000;
|
|
40
38
|
const summarySnapshotLockHeartbeatMs = Math.max(
|
|
@@ -137,31 +135,65 @@ export async function writeSummarySnapshot(
|
|
|
137
135
|
await writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
138
136
|
}
|
|
139
137
|
|
|
138
|
+
// ── Concurrency primitives (issue #1524 adoption) ─────────────────────────
|
|
139
|
+
//
|
|
140
|
+
// Summary-snapshot upserts were previously guarded by TWO bespoke serializers:
|
|
141
|
+
// 1. an in-process `summarySnapshotUpserts` map keyed by sessionKey that
|
|
142
|
+
// chained each upsert off the prior one's release (mirroring
|
|
143
|
+
// `serializeMutations`), and
|
|
144
|
+
// 2. an on-disk `withExclusiveSummarySnapshotFileLock` that re-implemented
|
|
145
|
+
// `open(path, "wx")` acquire, mtime heartbeat, stale-break, and
|
|
146
|
+
// ownership-checked release.
|
|
147
|
+
// Both now delegate to the shared utility so there is ONE home for each
|
|
148
|
+
// primitive (`serializeMutations` for in-process, `withHeldFileLock` for
|
|
149
|
+
// cross-process). The behavior contract is preserved:
|
|
150
|
+
// - in-process upserts for the SAME sessionKey are strictly serialized
|
|
151
|
+
// (read-merge-write cannot interleave);
|
|
152
|
+
// - a cross-process holder blocks up to `summarySnapshotLockTimeoutMs`, then
|
|
153
|
+
// the upsert FAILS (a snapshot is advisory; we never race a read-merge-write
|
|
154
|
+
// unlocked). To preserve this strictness on top of a best-effort utility,
|
|
155
|
+
// the work callback throws when `acquired === false`.
|
|
156
|
+
|
|
140
157
|
async function withSummarySnapshotLock<T>(
|
|
141
158
|
memoryDir: string,
|
|
142
159
|
sessionKey: string,
|
|
143
160
|
work: () => Promise<T>,
|
|
144
161
|
): Promise<T> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
return await withExclusiveSummarySnapshotFileLock(
|
|
162
|
+
// In-process serialization (one upsert per sessionKey at a time). The
|
|
163
|
+
// serializer recovers from rejection, so a failed upsert never poisons the
|
|
164
|
+
// next one — matching the previous chain's `.then(noop, noop)` recovery.
|
|
165
|
+
return serializeMutations(`summary-snapshot:${sessionKey}`, () =>
|
|
166
|
+
// Cross-process mutex via the shared utility. The lock path, stale
|
|
167
|
+
// threshold, bounded wait, and heartbeat cadence all flow through; the
|
|
168
|
+
// utility's replacement-safe stale break (NG7Bg) and ownership-checked
|
|
169
|
+
// release are stronger than the bare `unlink` this module used before.
|
|
170
|
+
withHeldFileLock(
|
|
156
171
|
summarySnapshotLockPath(memoryDir, sessionKey),
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
{
|
|
173
|
+
staleMs: summarySnapshotLockStaleMs,
|
|
174
|
+
maxWaitMs: summarySnapshotLockTimeoutMs,
|
|
175
|
+
heartbeatMs: summarySnapshotLockHeartbeatMs,
|
|
176
|
+
},
|
|
177
|
+
async (acquired) => {
|
|
178
|
+
// Strict-fail when the lock could not be acquired: the upsert is a
|
|
179
|
+
// read-merge-write, so a best-effort unlocked run would clobber a
|
|
180
|
+
// concurrent writer. The utility's `acquired === false` covers BOTH a
|
|
181
|
+
// genuine contention timeout AND a filesystem acquire failure (lock-dir
|
|
182
|
+
// mkdir/open/permission errors — the advisory lock is best-effort, so
|
|
183
|
+
// the util degrades rather than throwing). The bespoke lock this
|
|
184
|
+
// replaced propagated fs errors verbatim and reserved the timeout
|
|
185
|
+
// message for contention; we no longer claim "timed out" for an fs
|
|
186
|
+
// failure (cursor Low 25143f4f) — the message names both causes so
|
|
187
|
+
// upstream fail-open (runHourly) is unchanged but debugging is honest.
|
|
188
|
+
if (!acquired) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"could not acquire summary snapshot lock (contention timeout or filesystem error)",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return work();
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
);
|
|
165
197
|
}
|
|
166
198
|
|
|
167
199
|
export async function upsertSummarySnapshot(
|
|
@@ -185,54 +217,3 @@ export async function upsertSummarySnapshot(
|
|
|
185
217
|
await writeSummarySnapshot(memoryDir, summary.sessionKey, next);
|
|
186
218
|
});
|
|
187
219
|
}
|
|
188
|
-
|
|
189
|
-
async function withExclusiveSummarySnapshotFileLock<T>(
|
|
190
|
-
lockPath: string,
|
|
191
|
-
callback: () => Promise<T>,
|
|
192
|
-
): Promise<T> {
|
|
193
|
-
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
194
|
-
const startedAt = Date.now();
|
|
195
|
-
|
|
196
|
-
while (true) {
|
|
197
|
-
try {
|
|
198
|
-
const handle = await open(lockPath, "wx");
|
|
199
|
-
let heartbeat: NodeJS.Timeout | null = null;
|
|
200
|
-
if (summarySnapshotLockHeartbeatMs > 0) {
|
|
201
|
-
heartbeat = setInterval(() => {
|
|
202
|
-
void utimes(lockPath, new Date(), new Date()).catch(() => undefined);
|
|
203
|
-
}, summarySnapshotLockHeartbeatMs);
|
|
204
|
-
heartbeat.unref?.();
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
return await callback();
|
|
208
|
-
} finally {
|
|
209
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
210
|
-
await handle.close().catch(() => undefined);
|
|
211
|
-
await unlink(lockPath).catch(() => undefined);
|
|
212
|
-
}
|
|
213
|
-
} catch (error) {
|
|
214
|
-
if (!isAlreadyExistsError(error)) throw error;
|
|
215
|
-
try {
|
|
216
|
-
const lockStat = await stat(lockPath);
|
|
217
|
-
if (Date.now() - lockStat.mtimeMs > summarySnapshotLockStaleMs) {
|
|
218
|
-
await unlink(lockPath).catch(() => undefined);
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
} catch {
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (Date.now() - startedAt > summarySnapshotLockTimeoutMs) {
|
|
225
|
-
throw new Error("timed out acquiring summary snapshot lock");
|
|
226
|
-
}
|
|
227
|
-
await sleep(10);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isAlreadyExistsError(error: unknown): boolean {
|
|
233
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function sleep(ms: number): Promise<void> {
|
|
237
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
238
|
-
}
|
|
@@ -471,6 +471,44 @@ export async function applyTemporalSupersession(args: {
|
|
|
471
471
|
supersededAt,
|
|
472
472
|
invalidAt: invalidAtPatch ?? fresh.frontmatter.invalid_at,
|
|
473
473
|
});
|
|
474
|
+
// Issue #1579 — emit a tombstone for the superseded memory so it
|
|
475
|
+
// cannot resurrect through re-extraction / import / consolidation /
|
|
476
|
+
// dreams / pattern-reinforcement. The supersession key is the first
|
|
477
|
+
// matched key (the write-time supersession identity). Best-effort:
|
|
478
|
+
// a tombstone append failure is logged but never fails supersession
|
|
479
|
+
// (gotcha #13).
|
|
480
|
+
// Issue #1579 thread ObteS: persist ALL matched supersession keys, not
|
|
481
|
+
// just the first. A later paraphrased re-observation may derive the
|
|
482
|
+
// same entity but place the tombstoned attribute at a different
|
|
483
|
+
// position in structuredAttributes; without an entry for every matched
|
|
484
|
+
// key, the keyed tier would miss and the fact could resurrect as
|
|
485
|
+
// active. Pass the canonical contentHash from the retired memory's
|
|
486
|
+
// frontmatter so the tombstone's exact tier matches re-extraction
|
|
487
|
+
// (review: citation-hash alignment). The rawContent is the stored body;
|
|
488
|
+
// the StorageManager.appendTombstone chokepoint strips citation
|
|
489
|
+
// annotations for the normalized-text tier.
|
|
490
|
+
const keysToTombstone =
|
|
491
|
+
decision.matchedKeys.length > 0 ? decision.matchedKeys : [undefined];
|
|
492
|
+
for (const key of keysToTombstone) {
|
|
493
|
+
try {
|
|
494
|
+
await args.storage.appendTombstone({
|
|
495
|
+
reason: "supersession",
|
|
496
|
+
createdBy: "supersession",
|
|
497
|
+
sourceMemoryId: fresh.frontmatter.id,
|
|
498
|
+
...(fresh.frontmatter.contentHash
|
|
499
|
+
? { contentHash: fresh.frontmatter.contentHash }
|
|
500
|
+
: {}),
|
|
501
|
+
rawContent: fresh.content,
|
|
502
|
+
...(fresh.frontmatter.entityRef ? { entityRef: fresh.frontmatter.entityRef } : {}),
|
|
503
|
+
...(key ? { supersessionKey: key } : {}),
|
|
504
|
+
createdAt: supersededAt,
|
|
505
|
+
});
|
|
506
|
+
} catch (tombErr) {
|
|
507
|
+
log.warn(
|
|
508
|
+
`temporal-supersession: tombstone emit failed for ${fresh.frontmatter.id}${key ? ` (key=${key})` : ""} : ${tombErr}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
474
512
|
}
|
|
475
513
|
} catch (err) {
|
|
476
514
|
log.warn(
|
package/src/types.ts
CHANGED
|
@@ -821,6 +821,29 @@ export interface PluginConfig {
|
|
|
821
821
|
* deployments prefer the older (always-inject) behavior. Default `false`.
|
|
822
822
|
*/
|
|
823
823
|
temporalExpiredInInjection: boolean;
|
|
824
|
+
// Tombstones — non-resurrection invariant (issue #1579).
|
|
825
|
+
/**
|
|
826
|
+
* Master gate for the tombstone non-resurrection invariant. When `true`
|
|
827
|
+
* (default), a fact that matches an active tombstone at the storage
|
|
828
|
+
* chokepoint is persisted as `status: "pending_review"` + `blockedBy`
|
|
829
|
+
* instead of becoming active — visible, never a silent drop (rule 34).
|
|
830
|
+
* When `false`, pre-feature behavior: the chokepoint check is a no-op and
|
|
831
|
+
* retired facts can resurrect through re-extraction / import / consolidation
|
|
832
|
+
* / dreams / pattern-reinforcement. Off = rollback safety (rule 30).
|
|
833
|
+
*/
|
|
834
|
+
tombstonesEnabled: boolean;
|
|
835
|
+
/**
|
|
836
|
+
* Tier-4 semantic tombstone matching (issue #1579). Off by default — ships
|
|
837
|
+
* dark until MemCorrect (#1584) shows an acceptable false-block rate
|
|
838
|
+
* (rule 48). When `true`, the tombstone lookup also checks embedding cosine
|
|
839
|
+
* similarity against tombstoned normalized text.
|
|
840
|
+
*/
|
|
841
|
+
tombstonesSemanticMatch: boolean;
|
|
842
|
+
/**
|
|
843
|
+
* Cosine threshold for tier-4 semantic tombstone matching. Default `0.9`.
|
|
844
|
+
* Only consulted when `tombstonesSemanticMatch` is `true`.
|
|
845
|
+
*/
|
|
846
|
+
tombstonesSemanticThreshold: number;
|
|
824
847
|
// Direct-answer retrieval tier (issue #518)
|
|
825
848
|
/**
|
|
826
849
|
* When true, recall checks whether a single validated memory in a
|
|
@@ -2441,6 +2464,17 @@ export interface MemoryFrontmatter {
|
|
|
2441
2464
|
lineage?: string[];
|
|
2442
2465
|
/** Memory status: active (default), pending_review, rejected, quarantined, superseded, archived, or forgotten */
|
|
2443
2466
|
status?: MemoryStatus;
|
|
2467
|
+
/**
|
|
2468
|
+
* Tombstone block marker (issue #1579). When a new fact matches an active
|
|
2469
|
+
* tombstone at the storage chokepoint, it is persisted with
|
|
2470
|
+
* `status: "pending_review"` and this field set to the tombstone id — making
|
|
2471
|
+
* the block VISIBLE (rule 34: never a silent drop) rather than silently
|
|
2472
|
+
* discarding the extraction. Approving the memory through the review queue
|
|
2473
|
+
* emits a revocation that re-allows the content.
|
|
2474
|
+
*/
|
|
2475
|
+
blockedBy?: string;
|
|
2476
|
+
/** Which tombstone tier matched (`exact`/`normalized`/`keyed`/`semantic`). */
|
|
2477
|
+
tombstoneBlockTier?: "exact" | "normalized" | "keyed" | "semantic";
|
|
2444
2478
|
/** ID of memory that superseded this one */
|
|
2445
2479
|
supersededBy?: string;
|
|
2446
2480
|
/** Timestamp when superseded */
|
|
@@ -670,10 +670,14 @@ async function lockHeldBySelf(held: HeldLock): Promise<boolean> {
|
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
function sleep(ms: number): Promise<void> {
|
|
673
|
-
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
673
|
+
// Manual deferred instead of Promise.withResolvers (ES2024) — plugin-openclaw's
|
|
674
|
+
// standalone tsconfig targets ES2022 lib and this module is reachable from its
|
|
675
|
+
// type graph, so withResolvers would TS2550 there (same fix as
|
|
676
|
+
// extraction-faithfulness.ts:467).
|
|
677
|
+
return new Promise<void>((resolve) => {
|
|
678
|
+
// NOT unref'd: this polls inside an awaited acquire loop, so the caller's
|
|
679
|
+
// await chain keeps the loop alive; unref would let Node exit mid-poll when
|
|
680
|
+
// nothing else is pending (the heartbeat interval IS unref'd separately).
|
|
681
|
+
setTimeout(resolve, ms);
|
|
682
|
+
});
|
|
679
683
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/namespaces/storage.ts"],"sourcesContent":["import path from \"node:path\";\nimport { access, lstat, readdir } from \"node:fs/promises\";\nimport { isSafeRouteNamespace } from \"../routing/engine.js\";\nimport { StorageManager } from \"../storage.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { ALL_CATEGORY_DIRS } from \"../utils/category-dir.js\";\nimport { namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\nimport type { NamespaceCatalog } from \"./catalog.js\";\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function hasStoredEntries(p: string): Promise<boolean> {\n try {\n const entry = await lstat(p);\n if (entry.isSymbolicLink()) return true;\n if (!entry.isDirectory()) return true;\n const children = await readdir(p, { withFileTypes: true });\n for (const child of children) {\n const childPath = path.join(p, child.name);\n if (child.isSymbolicLink() || child.isFile()) return true;\n if (child.isDirectory() && (await hasStoredEntries(childPath))) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\n// Build a per-namespace directory under `<memoryDir>/namespaces` and assert the\n// resolved path stays inside that base. Namespace identifiers can originate from\n// operator config (config.defaultNamespace) and request-derived routing, so this\n// containment check prevents directory traversal (CodeQL js/path-injection).\n// For safe segments this returns exactly `path.join(base, segment)`, so there is\n// no behavioral change for valid namespaces.\nfunction resolveNamespaceDir(memoryDir: string, segment: string): string {\n // Mirror isSafeRouteNamespace's separator/parent-ref rejection (without its\n // 64-char cap, so identity tokens still pass). Rejecting separators and \"..\"\n // up front keeps the value a single contained child of <memoryDir>/namespaces.\n if (\n segment.length === 0 ||\n segment.includes(\"/\") ||\n segment.includes(\"\\\\\") ||\n segment.includes(\"..\") ||\n path.isAbsolute(segment)\n ) {\n throw new Error(`unsafe namespace path segment: ${segment}`);\n }\n return path.join(memoryDir, \"namespaces\", segment);\n}\n\nconst LEGACY_NAMESPACE_CONTENT_CHILDREN = [\n ...ALL_CATEGORY_DIRS,\n \"entities\",\n \"artifacts\",\n \"identity\",\n \"config\",\n \"summaries\",\n \"profile.md\",\n] as const;\n\nconst LEGACY_NAMESPACE_RUNTIME_CHILDREN = [\"state\"] as const;\n\nasync function hasAnyLegacyData(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await hasStoredEntries(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\nasync function hasAnyNamespaceStorageMarker(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await exists(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\n/**\n * Storage routing for namespaces.\n *\n * Compatibility note:\n * - When namespaces are enabled, existing raw namespace roots are preserved.\n * New namespace roots use tokenized names under `memoryDir/namespaces/<token>`.\n * - The default namespace continues to use the legacy `memoryDir` root unless the caller\n * has created `memoryDir/namespaces/<defaultNamespace>` (in which case we use that).\n *\n * This avoids surprising \"lost memories\" when an install flips namespaces on without\n * migrating existing data.\n */\n/**\n * Optional hooks for the storage router. `onResolve` fires whenever a namespace's\n * storage is resolved/created, so a downstream consumer (e.g. the namespace\n * catalog, issue #1499) can register the namespace. The hook MUST NOT throw into\n * the router; the router invokes it defensively and a hook failure never affects\n * storage resolution.\n *\n * The hook MAY return (or resolve to) a boolean indicating whether the\n * registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves\n * to `false` (a dropped/no-op registration), the router does NOT mark the\n * (namespace, storageDir) pair as notified, so the next resolve RETRIES it\n * instead of suppressing it forever. A `void`/`undefined` result is treated as\n * success (legacy hooks).\n */\nexport interface NamespaceStorageRouterHooks {\n onResolve?: (\n namespace: string,\n storageDir: string,\n ) => void | boolean | Promise<void | boolean>;\n}\n\n/**\n * Resolve the runtime storage root for the configured DEFAULT namespace.\n *\n * Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)\n * and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two\n * can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through\n * the same logic). The contract is: while legacy memory data still lives\n * directly under `memoryDir`, the default root stays `memoryDir`; only once the\n * legacy root is empty and a `namespaces/<default|token>` dir holds data does\n * the default migrate into that tokenized/legacy-named dir.\n */\nexport async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {\n if (!config.namespacesEnabled) {\n return config.memoryDir;\n }\n\n // Build the legacy default root from the NORMALIZED (trimmed) name so a\n // whitespace-padded `defaultNamespace` still finds the live `namespaces/default`\n // root (NIabe). `storageFor()` classifies the trimmed value as the default, and\n // the on-disk legacy dir is created under the trimmed name; using the raw spaced\n // name here would look for `namespaces/<spaced>` and miss the real root, falling\n // back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes\n // internally, so the tokenized path is unaffected.\n const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);\n const tokenizedNsDir = resolveNamespaceDir(\n config.memoryDir,\n namespaceIdentityToken(config.defaultNamespace),\n );\n const tokenizedHasData =\n (await exists(tokenizedNsDir)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));\n const nsDir = tokenizedHasData\n ? tokenizedNsDir\n : (await exists(legacyNsDir))\n ? legacyNsDir\n : tokenizedNsDir;\n return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))\n ? nsDir\n : config.memoryDir;\n}\n\n/**\n * Resolve the runtime storage root for ANY namespace exactly as the live router\n * would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable\n * catalog records the SAME on-disk root the router routes to — a recall/read\n * touch must not guess `namespaces/<token>` when the router actually serves a\n * legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round\n * 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;\n * every other namespace prefers the tokenized root when it has a storage marker,\n * else a legacy raw-name dir when present, else the tokenized root.\n */\nexport async function resolveNamespaceStorageRoot(\n config: PluginConfig,\n namespace: string,\n): Promise<string> {\n if (!config.namespacesEnabled) return config.memoryDir;\n // Compare on NORMALIZED identity so a whitespace-padded configured default name\n // still routes to the default root rather than a tokenized non-default dir\n // (NH-FH). The catalog keys records by the same normalized identity.\n if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {\n return resolveDefaultNamespaceRoot(config);\n }\n const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);\n const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));\n if (\n (await exists(tokenizedRoot)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))\n ) {\n return tokenizedRoot;\n }\n return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;\n}\n\nexport class NamespaceStorageRouter {\n private readonly cache = new Map<string, StorageManager>();\n private defaultNsRootResolved: string | null = null;\n // Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction\n // call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +\n // append) on every cache hit grows `namespaces.jsonl` without bound between\n // rebuilds. We fire the hook only when the (namespace, storageDir) pair is new\n // or its dir changed, so a steady-state cache hit is a no-op for the catalog.\n private readonly notifiedResolved = new Map<string, string>();\n // In-flight resolve-hook dedup (NFJV-, codex P2). The catalog's `onResolve`\n // hook is ASYNC (it returns `registerResolved(...)`), so `notifiedResolved` is\n // only set after the hook's promise SETTLES. Without tracking the in-flight\n // window, a burst of `storageFor()` cache hits for the SAME namespace before\n // the first registration finishes would each pass the `notifiedResolved` guard\n // and fire their OWN `onResolve` — queueing N duplicate catalog touches + lock\n // acquisitions despite the once-per-namespace intent. We therefore record the\n // (namespace → storageDir) being registered BEFORE awaiting the hook so a\n // concurrent call for the same pair skips firing. On SUCCESS the pair is\n // promoted to `notifiedResolved` (future calls skip permanently); on `false`\n // (dropped touch — e.g. rebuild-lock timeout) OR rejection the in-flight marker\n // is CLEARED so a later `storageFor()` can RETRY the dropped registration. The\n // entry is always removed when the promise settles, so the map cannot grow\n // unbounded (one transient entry per concurrently-resolving namespace).\n private readonly inFlightResolved = new Map<string, string>();\n // Tracks every in-flight resolve-hook promise so callers can deterministically\n // await the fire-and-forget registrations that `storageFor()` kicks off (see\n // `whenResolveHooksSettled`). Entries are removed as each hook settles, so the\n // set holds at most one promise per concurrently-resolving namespace.\n private readonly pendingResolveHooks = new Set<Promise<unknown>>();\n // Pending post-write catalog touch promises (#1522). Like pendingResolveHooks,\n // lets tests await fire-and-forget write touches deterministically.\n private readonly pendingWriteTouches = new Set<Promise<unknown>>();\n\n // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`\n // normalizes its input, so default-namespace branches must compare against the\n // normalized config default too — otherwise a whitespace-padded configured\n // default name routes the default namespace to a tokenized non-default root.\n private readonly defaultNamespaceIdentity: string;\n\n constructor(\n private readonly config: PluginConfig,\n private readonly hooks: NamespaceStorageRouterHooks = {},\n /** Catalog reference for post-write/read touches (issue #1522 chokepoint). */\n private readonly catalog?: NamespaceCatalog,\n ) {\n this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n }\n\n private async defaultNamespaceRoot(): Promise<string> {\n this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);\n return this.defaultNsRootResolved;\n }\n\n private async namespaceRoot(namespace: string): Promise<string> {\n // NOTE: only used after defaultNamespaceRoot() resolution.\n if (!this.config.namespacesEnabled) return this.config.memoryDir;\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n return this.defaultNsRootResolved ?? this.config.memoryDir;\n }\n return resolveNamespaceStorageRoot(this.config, namespace);\n }\n\n async storageFor(namespace: string): Promise<StorageManager> {\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n throw new Error(`unsafe namespace: ${ns}`);\n }\n // Even when the default namespace is exempt from the check above, every\n // on-disk path is built through resolveNamespaceDir(), which rejects\n // traversal segments — so an unsafe configured default still cannot escape\n // <memoryDir>/namespaces (CodeQL js/path-injection).\n\n let root: string;\n if (ns === this.defaultNamespaceIdentity) {\n root = await this.defaultNamespaceRoot();\n const cached = this.cache.get(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n } else {\n const cached = this.cache.get(ns);\n root = await this.namespaceRoot(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n }\n\n const sm = new StorageManager(root, this.config.entitySchemas);\n // Propagate the inline-attribution template so that router-created storages\n // (used by extraction and shared-promotion paths) strip citations consistently,\n // matching the behaviour of the primary this.storage instance in the orchestrator.\n sm.citationTemplate = this.config.inlineSourceAttributionFormat;\n // #1522: install the post-write catalog touch at the chokepoint — every\n // successful write on this StorageManager records the namespace touch.\n this.bindCatalogWriteHook(sm, ns);\n this.cache.set(ns, sm);\n this.notifyResolved(ns, root);\n return sm;\n }\n\n /**\n * Fire the resolve hook defensively. A hook failure (e.g. a catalog write\n * error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.\n */\n private notifyResolved(namespace: string, storageDir: string): void {\n const hook = this.hooks.onResolve;\n if (!hook) return;\n // Skip when we've already SUCCESSFULLY notified this exact (namespace,\n // storageDir) — a steady-state cache hit must not re-append to the catalog\n // log (NCNL2). A changed dir (rare: migration/realignment) still re-fires\n // once. We mark the pair as notified ONLY AFTER the hook succeeds, and CLEAR\n // it on failure, so a dropped registration (e.g. rebuild-lock timeout) is\n // RETRIED on the next cache hit instead of being suppressed forever (round 6,\n // cursor Medium — ND3EJ).\n if (this.notifiedResolved.get(namespace) === storageDir) return;\n // In-flight dedup (NFJV-, codex P2): if a registration for this exact\n // (namespace, storageDir) is already AWAITING its async hook, do not fire a\n // second one. Without this, concurrent cache-hit bursts before the first\n // append settles each pass the `notifiedResolved` guard above and queue\n // duplicate catalog touches/lock acquisitions. A pair with a DIFFERENT\n // in-flight dir (rare mid-migration realignment) still fires once.\n if (this.inFlightResolved.get(namespace) === storageDir) return;\n try {\n // Handle BOTH synchronous throws and asynchronous rejections (round 6,\n // codex P2 — NDo8C). The hook may be `async`; its rejected promise would\n // bypass this try/catch and, where unhandled rejections are fatal, crash\n // storage resolution. Mark the dedup pair as notified ONLY when the hook\n // resolves to a PERSISTED result (round 6, codex P2 — NEFoX): a result of\n // `false` means the registration was dropped/no-op (e.g. rebuild-lock\n // timeout), so we must NOT suppress its retry. `void`/`undefined` is treated\n // as success for legacy hooks. On rejection we leave it un-notified to retry.\n //\n // Record the in-flight marker BEFORE awaiting so concurrent calls for the\n // same pair skip (NFJV-). It is always cleared once the promise settles, so\n // the map holds at most one transient entry per concurrently-resolving\n // namespace and cannot grow unbounded.\n this.inFlightResolved.set(namespace, storageDir);\n const hookResult = Promise.resolve(hook(namespace, storageDir));\n // Track the in-flight promise so `whenResolveHooksSettled()` can await it.\n this.pendingResolveHooks.add(hookResult);\n hookResult.then(\n (persisted) => {\n // Clear the in-flight marker ONLY if it is still ours (a newer resolve\n // for a different dir may have replaced it).\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (persisted !== false) {\n this.notifiedResolved.set(namespace, storageDir);\n }\n // On `false` (dropped touch) we intentionally do NOT mark notified, so\n // a later `storageFor()` retries the registration. Clearing the\n // in-flight marker above is what re-enables that retry.\n this.pendingResolveHooks.delete(hookResult);\n },\n () => {\n // Registration failed — clear in-flight AND do NOT mark as notified, so\n // it is retried on the next cache hit.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (this.notifiedResolved.get(namespace) === storageDir) {\n this.notifiedResolved.delete(namespace);\n }\n this.pendingResolveHooks.delete(hookResult);\n },\n );\n } catch {\n // Synchronous throw: clear any in-flight marker we just set and leave the\n // pair un-notified so a later resolve retries.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n }\n }\n\n /**\n * Install the post-write catalog touch hook on an externally-constructed\n * StorageManager (issue #1522). Used by the orchestrator for the legacy\n * default-namespace storage (`this.storage`) that bypasses the router.\n */\n bindCatalogWriteHook(sm: StorageManager, namespace: string): void {\n sm.onCatalogWrite = () => this.touchCatalogWrite(namespace, sm.dir);\n }\n\n /**\n * Post-write catalog touch (issue #1522 chokepoint). Called by every\n * StorageManager's post-write hook AFTER a successful write. Best-effort\n * and failure-tolerant — a catalog error MUST NOT affect the primary write\n * (gotcha #13, rule #40). Fire-and-forget by design.\n */\n private touchCatalogWrite(namespace: string, storageDir: string): void {\n if (!this.catalog) return;\n const touch = this.catalog\n .markWrite(namespace, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Record a namespace read in the catalog (issue #1522 chokepoint move).\n * Best-effort and failure-tolerant. Used by recall paths so the read touch\n * lives in the storage layer, not at the caller.\n */\n recordRead(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n void this.catalog\n .markRead(ns, { discoveredBy: \"read\", storageDir })\n .catch(() => undefined);\n }\n /**\n * Record a namespace write touch in the catalog (issue #1522). Best-effort\n * and failure-tolerant. Used by consolidation/cleanup passes that may mutate\n * the store via delete-only paths (e.g. entity-file merges, TTL cleanup) so\n * the namespace's lastWriteAt stays fresh even when no explicit write went\n * through the storage chokepoint. This is NOT a substitute for the chokepoint\n * — it's a belt-and-suspenders for paths the chokepoint doesn't cover.\n */\n recordWrite(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n const touch = this.catalog\n .markWrite(ns, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Resolve once every in-flight post-write catalog touch has settled (#1522).\n * Mirrors `whenResolveHooksSettled()`: the StorageManager's post-write hook\n * fires the catalog touch fire-and-forget, so tests asserting lastWriteAt\n * moved should await this instead of racing a timer.\n */\n async whenWriteTouchesSettled(): Promise<void> {\n while (this.pendingWriteTouches.size > 0) {\n await Promise.allSettled([...this.pendingWriteTouches]);\n }\n }\n\n /**\n * Resolve once every in-flight `onResolve` registration has settled.\n *\n * `storageFor()` fires the resolve hook fire-and-forget, so its catalog side\n * effect (e.g. `registerResolved(...)`) is not observable the moment\n * `storageFor()` returns. Callers that must act on that side effect — notably\n * tests asserting the catalog was updated — should await this instead of\n * racing a timer. Resolves immediately when no hook is registered or nothing\n * is in flight. The loop re-checks because a settling hook could, in\n * principle, trigger a follow-on resolution.\n */\n async whenResolveHooksSettled(): Promise<void> {\n while (this.pendingResolveHooks.size > 0) {\n await Promise.allSettled([...this.pendingResolveHooks]);\n }\n }\n}\n"],"mappings":"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","names":[]}
|