@remnic/core 9.43.0 → 9.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +42 -41
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +43 -42
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +42 -41
- package/dist/access-cli.js +100 -97
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +42 -41
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http-query.d.ts +8 -0
- package/dist/access-http-query.js +180 -0
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +51 -49
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +42 -41
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +42 -41
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +47 -46
- package/dist/access-namespace-preflight.js +42 -41
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +42 -41
- package/dist/access-operations-batch.js +44 -43
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +46 -45
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +42 -41
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +42 -41
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +42 -41
- package/dist/access-schema.d.ts +84 -84
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-D1BkxwSf.d.ts → access-service-DBo88Dmm.d.ts} +14 -4
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +42 -41
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +5 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +9 -8
- package/dist/buffer.d.ts +1 -1
- package/dist/buffer.js +6 -6
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +13 -12
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
- package/dist/{chunk-CMENJQ6I.js → chunk-2QR2RRH6.js} +2 -2
- package/dist/{chunk-ZERSIPFC.js → chunk-3HKQOFOV.js} +22 -20
- package/dist/{chunk-ZERSIPFC.js.map → chunk-3HKQOFOV.js.map} +1 -1
- package/dist/{chunk-4DYBLQZC.js → chunk-3HLCRYBV.js} +3 -3
- package/dist/{chunk-L2DPF4DF.js → chunk-3SHB4YAW.js} +4 -4
- package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
- package/dist/{chunk-GUQYMOPU.js → chunk-4LMA77JH.js} +5 -5
- package/dist/{chunk-Q53RLEW7.js → chunk-5SM35XZR.js} +2 -2
- package/dist/{chunk-YJHU5B3M.js → chunk-5YGO5VEH.js} +2 -2
- package/dist/{chunk-AX2J2LC4.js → chunk-5ZE7QHTK.js} +2 -2
- package/dist/{chunk-5QZEVMLY.js → chunk-65V4K4MT.js} +56 -38
- package/dist/chunk-65V4K4MT.js.map +1 -0
- package/dist/{chunk-YFHMW37V.js → chunk-6CZPDBJS.js} +5 -5
- package/dist/{chunk-BI6RE2ZS.js → chunk-6OLQJJYE.js} +2 -2
- package/dist/chunk-6UWB5PTR.js +413 -0
- package/dist/chunk-6UWB5PTR.js.map +1 -0
- package/dist/chunk-6ZYQ36IB.js +30 -0
- package/dist/chunk-6ZYQ36IB.js.map +1 -0
- package/dist/{chunk-GS5UVGQK.js → chunk-7JO6RVBI.js} +6 -6
- package/dist/{chunk-MBTPUVHZ.js → chunk-7PVFOFPJ.js} +2 -2
- package/dist/{chunk-KPJQTSRJ.js → chunk-C7EI7CA6.js} +2 -2
- package/dist/{chunk-NRV4Y3PM.js → chunk-CAGPP5NB.js} +1 -1
- package/dist/{chunk-NF3U3MPG.js → chunk-ECK75GDJ.js} +2 -2
- package/dist/{chunk-KDXD2AM6.js → chunk-FLBPATMZ.js} +44 -44
- package/dist/{chunk-OEXKYYVH.js → chunk-FVGQ5LIS.js} +6 -6
- package/dist/{chunk-AR2FSZBP.js → chunk-G24XIK6H.js} +2 -2
- package/dist/{chunk-5TOV3D6U.js → chunk-GBJJUPNV.js} +6 -6
- package/dist/{chunk-UP67AFRN.js → chunk-GOPWZYZH.js} +29 -29
- package/dist/chunk-GOPWZYZH.js.map +1 -0
- package/dist/{chunk-EPQ76W5T.js → chunk-GQBZ7EVF.js} +2 -2
- package/dist/{chunk-BGXHVOBI.js → chunk-HLPD7B7P.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-JDXDA5OO.js → chunk-J4OKD6QX.js} +2 -2
- package/dist/{chunk-LZ5JKQCS.js → chunk-JZ7SSAQ5.js} +3 -3
- package/dist/chunk-K6OTW3TP.js +24 -0
- package/dist/chunk-K6OTW3TP.js.map +1 -0
- package/dist/{chunk-EHKDORQN.js → chunk-NVRYW264.js} +9 -9
- package/dist/{chunk-MU7ZHSZE.js → chunk-PO5LSKQO.js} +2 -2
- package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
- package/dist/{chunk-WGCTFGLW.js → chunk-QQ3NDJOM.js} +3 -3
- package/dist/{chunk-BEQOZOYZ.js → chunk-RVLJEII5.js} +30 -511
- package/dist/chunk-RVLJEII5.js.map +1 -0
- package/dist/chunk-S5SLVSVY.js +517 -0
- package/dist/chunk-S5SLVSVY.js.map +1 -0
- package/dist/{chunk-QY66HJ4X.js → chunk-SS7YWVPF.js} +5 -5
- package/dist/{chunk-ZVVASRUT.js → chunk-U5G66ORD.js} +90 -88
- package/dist/chunk-U5G66ORD.js.map +1 -0
- package/dist/{chunk-JYIFGGEM.js → chunk-UZGJFIIG.js} +2 -2
- package/dist/{chunk-NHRNYJPJ.js → chunk-VCGPO2VD.js} +7 -7
- package/dist/{chunk-F2KMAFEB.js → chunk-VRYJSKOM.js} +2 -2
- package/dist/{chunk-HNZKVLAN.js → chunk-W6ROK2KU.js} +2 -2
- package/dist/{chunk-SEUSFKNS.js → chunk-WC4SPYHX.js} +14 -14
- package/dist/{chunk-MSXWUXFE.js → chunk-WG2PI44B.js} +2 -2
- package/dist/{chunk-4VXGRIH6.js → chunk-WUNIWKY4.js} +5 -5
- package/dist/{chunk-3C2TGBDB.js → chunk-XFQRFWVS.js} +2 -2
- package/dist/{chunk-5N26WBPQ.js → chunk-XMZV27S5.js} +5 -5
- package/dist/{chunk-JGXXPDM2.js → chunk-YQQK7W7T.js} +3 -3
- package/dist/{chunk-3F2LKUMU.js → chunk-ZAB5PG65.js} +2 -2
- package/dist/{chunk-MVW234LO.js → chunk-ZPJJE7MF.js} +2 -2
- package/dist/{chunk-6GIMMNVK.js → chunk-ZS5UZU5M.js} +4 -4
- package/dist/{cli-IUhV-lmk.d.ts → cli-DQ4VhjqX.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +69 -67
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +9 -8
- package/dist/connectors/codex-materialize-runner.js +9 -8
- package/dist/connectors/index.js +9 -8
- 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/convergence-refresh.d.ts +24 -0
- package/dist/convergence-refresh.js +75 -0
- package/dist/convergence-refresh.js.map +1 -0
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +20 -19
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +9 -8
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +4 -3
- package/dist/extraction.js +15 -14
- package/dist/{graph-edge-decay-Z6ENALU2.js → graph-edge-decay-GF3ZSGSP.js} +4 -4
- package/dist/graph-edge-decay-GF3ZSGSP.js.map +1 -0
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.d.ts +398 -398
- package/dist/index.js +161 -158
- package/dist/lcm/index.js +3 -3
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -8
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-DVBLXkFw.d.ts → maintenance-_RV38GlE.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +19 -18
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +9 -8
- package/dist/offline-sync-impression-drain.js +4 -4
- package/dist/operator-doctor-corpus.js +21 -20
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +30 -29
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +16 -15
- package/dist/{orchestrator-fYTncnLy.d.ts → orchestrator-B2ecFpRO.d.ts} +4 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +101 -98
- package/dist/qmd.js +4 -4
- package/dist/reconcile/manifest.d.ts +33 -0
- package/dist/reconcile/manifest.js +248 -0
- package/dist/reconcile/manifest.js.map +1 -0
- package/dist/reconcile/plan.d.ts +1 -1
- package/dist/reconcile/plan.js +8 -404
- package/dist/reconcile/plan.js.map +1 -1
- package/dist/resume-bundles.js +1 -1
- package/dist/schemas.d.ts +76 -76
- package/dist/search/factory.js +8 -8
- package/dist/search/index.js +14 -14
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +9 -8
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-CLuMO-pa.d.ts → service-DW06sY3P.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +14 -13
- package/dist/{storage-Blr71GpS.d.ts → storage-qRIQPA6F.d.ts} +12 -0
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +8 -7
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +10 -9
- package/package.json +12 -2
- package/src/access-http-query.ts +25 -0
- package/src/access-http.test.ts +136 -0
- package/src/access-http.ts +26 -26
- package/src/access-service-offline-file-content.test.ts +40 -0
- package/src/access-service.ts +34 -0
- package/src/access-surface-catalog.ts +5 -0
- package/src/convergence-refresh.ts +28 -0
- package/src/orchestrator-convergence-refresh.test.ts +114 -0
- package/src/orchestrator.ts +5 -4
- package/src/reconcile/manifest.test.ts +261 -0
- package/src/reconcile/manifest.ts +278 -0
- package/src/reconcile/plan.ts +1 -0
- package/src/storage/content-hash-index.ts +26 -0
- package/dist/chunk-5QZEVMLY.js.map +0 -1
- package/dist/chunk-BEQOZOYZ.js.map +0 -1
- package/dist/chunk-UP67AFRN.js.map +0 -1
- package/dist/chunk-ZVVASRUT.js.map +0 -1
- /package/dist/{graph-edge-decay-Z6ENALU2.js.map → access-http-query.js.map} +0 -0
- /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
- /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
- /package/dist/{chunk-CMENJQ6I.js.map → chunk-2QR2RRH6.js.map} +0 -0
- /package/dist/{chunk-4DYBLQZC.js.map → chunk-3HLCRYBV.js.map} +0 -0
- /package/dist/{chunk-L2DPF4DF.js.map → chunk-3SHB4YAW.js.map} +0 -0
- /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
- /package/dist/{chunk-GUQYMOPU.js.map → chunk-4LMA77JH.js.map} +0 -0
- /package/dist/{chunk-Q53RLEW7.js.map → chunk-5SM35XZR.js.map} +0 -0
- /package/dist/{chunk-YJHU5B3M.js.map → chunk-5YGO5VEH.js.map} +0 -0
- /package/dist/{chunk-AX2J2LC4.js.map → chunk-5ZE7QHTK.js.map} +0 -0
- /package/dist/{chunk-YFHMW37V.js.map → chunk-6CZPDBJS.js.map} +0 -0
- /package/dist/{chunk-BI6RE2ZS.js.map → chunk-6OLQJJYE.js.map} +0 -0
- /package/dist/{chunk-GS5UVGQK.js.map → chunk-7JO6RVBI.js.map} +0 -0
- /package/dist/{chunk-MBTPUVHZ.js.map → chunk-7PVFOFPJ.js.map} +0 -0
- /package/dist/{chunk-KPJQTSRJ.js.map → chunk-C7EI7CA6.js.map} +0 -0
- /package/dist/{chunk-NRV4Y3PM.js.map → chunk-CAGPP5NB.js.map} +0 -0
- /package/dist/{chunk-NF3U3MPG.js.map → chunk-ECK75GDJ.js.map} +0 -0
- /package/dist/{chunk-KDXD2AM6.js.map → chunk-FLBPATMZ.js.map} +0 -0
- /package/dist/{chunk-OEXKYYVH.js.map → chunk-FVGQ5LIS.js.map} +0 -0
- /package/dist/{chunk-AR2FSZBP.js.map → chunk-G24XIK6H.js.map} +0 -0
- /package/dist/{chunk-5TOV3D6U.js.map → chunk-GBJJUPNV.js.map} +0 -0
- /package/dist/{chunk-EPQ76W5T.js.map → chunk-GQBZ7EVF.js.map} +0 -0
- /package/dist/{chunk-BGXHVOBI.js.map → chunk-HLPD7B7P.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-JDXDA5OO.js.map → chunk-J4OKD6QX.js.map} +0 -0
- /package/dist/{chunk-LZ5JKQCS.js.map → chunk-JZ7SSAQ5.js.map} +0 -0
- /package/dist/{chunk-EHKDORQN.js.map → chunk-NVRYW264.js.map} +0 -0
- /package/dist/{chunk-MU7ZHSZE.js.map → chunk-PO5LSKQO.js.map} +0 -0
- /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
- /package/dist/{chunk-WGCTFGLW.js.map → chunk-QQ3NDJOM.js.map} +0 -0
- /package/dist/{chunk-QY66HJ4X.js.map → chunk-SS7YWVPF.js.map} +0 -0
- /package/dist/{chunk-JYIFGGEM.js.map → chunk-UZGJFIIG.js.map} +0 -0
- /package/dist/{chunk-NHRNYJPJ.js.map → chunk-VCGPO2VD.js.map} +0 -0
- /package/dist/{chunk-F2KMAFEB.js.map → chunk-VRYJSKOM.js.map} +0 -0
- /package/dist/{chunk-HNZKVLAN.js.map → chunk-W6ROK2KU.js.map} +0 -0
- /package/dist/{chunk-SEUSFKNS.js.map → chunk-WC4SPYHX.js.map} +0 -0
- /package/dist/{chunk-MSXWUXFE.js.map → chunk-WG2PI44B.js.map} +0 -0
- /package/dist/{chunk-4VXGRIH6.js.map → chunk-WUNIWKY4.js.map} +0 -0
- /package/dist/{chunk-3C2TGBDB.js.map → chunk-XFQRFWVS.js.map} +0 -0
- /package/dist/{chunk-5N26WBPQ.js.map → chunk-XMZV27S5.js.map} +0 -0
- /package/dist/{chunk-JGXXPDM2.js.map → chunk-YQQK7W7T.js.map} +0 -0
- /package/dist/{chunk-3F2LKUMU.js.map → chunk-ZAB5PG65.js.map} +0 -0
- /package/dist/{chunk-MVW234LO.js.map → chunk-ZPJJE7MF.js.map} +0 -0
- /package/dist/{chunk-6GIMMNVK.js.map → chunk-ZS5UZU5M.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
|
|
3
|
+
ContentHashIndex
|
|
4
|
+
} from "./chunk-S5SLVSVY.js";
|
|
1
5
|
import {
|
|
2
6
|
markProjectedMemoryPathInvalid
|
|
3
7
|
} from "./chunk-3CSX3ETE.js";
|
|
@@ -9,14 +13,7 @@ import {
|
|
|
9
13
|
withHeldFileLock
|
|
10
14
|
} from "./chunk-FFAZ2XFU.js";
|
|
11
15
|
import {
|
|
12
|
-
computeContentHash,
|
|
13
|
-
normalizeContent
|
|
14
|
-
} from "./chunk-W5H6ZRPT.js";
|
|
15
|
-
import {
|
|
16
|
-
SecureStoreLockedError,
|
|
17
|
-
readMaybeEncryptedFile,
|
|
18
16
|
readMaybeEncryptedFileFromChunks,
|
|
19
|
-
writeMaybeEncryptedFile,
|
|
20
17
|
writeMaybeEncryptedFileFromChunks
|
|
21
18
|
} from "./chunk-5TFP4CUL.js";
|
|
22
19
|
import {
|
|
@@ -29,486 +26,11 @@ import {
|
|
|
29
26
|
log
|
|
30
27
|
} from "./chunk-3WCASNQZ.js";
|
|
31
28
|
|
|
32
|
-
// src/storage/content-hash-index.ts
|
|
33
|
-
import { mkdir, stat } from "fs/promises";
|
|
34
|
-
import path from "path";
|
|
35
|
-
var CONTENT_HASH_INDEX_LOCK_STALE_MS = 3e4;
|
|
36
|
-
var CONTENT_HASH_INDEX_RETRY_MAX_ATTEMPTS = 5;
|
|
37
|
-
var CONTENT_HASH_INDEX_RETRY_BASE_MS = 500;
|
|
38
|
-
var CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS = 5e3;
|
|
39
|
-
var FactHashIndexNotAuthoritativeError = class extends Error {
|
|
40
|
-
constructor(stateDir) {
|
|
41
|
-
super(
|
|
42
|
-
`fact-hash index is not authoritative: the cross-process rebuild lock for ${stateDir} could not be acquired within the bounded retry budget`
|
|
43
|
-
);
|
|
44
|
-
this.name = "FactHashIndexNotAuthoritativeError";
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
var ContentHashIndex = class _ContentHashIndex {
|
|
48
|
-
hashes = /* @__PURE__ */ new Set();
|
|
49
|
-
dirty = false;
|
|
50
|
-
/**
|
|
51
|
-
* Hashes explicitly removed since the last successful save (issue #1909 review
|
|
52
|
-
* round 8 thread 3). Tracked separately so `saveMergingWithDisk` can be
|
|
53
|
-
* removal-AWARE: under the file lock it reads the latest on-disk set, DROPS
|
|
54
|
-
* these, then unions our additions — so a removal batch cannot resurrect a
|
|
55
|
-
* hash (union alone would) and cannot clobber a concurrent extraction's
|
|
56
|
-
* appended hash (blind overwrite would). Consumed + cleared by every save.
|
|
57
|
-
*/
|
|
58
|
-
removed = /* @__PURE__ */ new Set();
|
|
59
|
-
/**
|
|
60
|
-
* Hashes added by THIS instance since the last successful save (issue #1909
|
|
61
|
-
* review round 9 finding 3). Tracked separately from the loaded snapshot so a
|
|
62
|
-
* reconciling save publishes `(on-disk \ removed) ∪ added` — it re-applies only
|
|
63
|
-
* OUR additions, never the stale loaded entries. Without this, seeding the
|
|
64
|
-
* merge from `this.hashes` (loaded ∪ added) would resurrect a hash a peer
|
|
65
|
-
* removed on disk while this instance held a stale snapshot.
|
|
66
|
-
*/
|
|
67
|
-
added = /* @__PURE__ */ new Set();
|
|
68
|
-
filePath;
|
|
69
|
-
secureStoreKeyProvider;
|
|
70
|
-
secureStoreWriteKeyProvider;
|
|
71
|
-
memoryDir;
|
|
72
|
-
lockOptions;
|
|
73
|
-
/**
|
|
74
|
-
* Deferred reconcile-save durable-retry state (issue #1909 / PR #2016). At
|
|
75
|
-
* most ONE retry is ever armed (`reconcileRetryTimer` is the reentrancy guard)
|
|
76
|
-
* so successive timed-out saves cannot stack duplicate/parallel retries.
|
|
77
|
-
* `reconcileRetryBarrier` resolves when the chain settles (published or gave
|
|
78
|
-
* up) — awaited by tests and any caller wanting the eventual result.
|
|
79
|
-
*/
|
|
80
|
-
reconcileRetryTimer = null;
|
|
81
|
-
reconcileRetryAttempts = 0;
|
|
82
|
-
reconcileRetryBarrier = null;
|
|
83
|
-
reconcileRetryResolve = null;
|
|
84
|
-
/**
|
|
85
|
-
* On-disk fingerprint captured at the last point THIS instance's in-memory
|
|
86
|
-
* set matched disk — after `load()`, `save()`, or a reconcile publish. A peer
|
|
87
|
-
* process that advances the durable index changes its file generation even
|
|
88
|
-
* when byte size and mtime collide, so the freshness check remains safe
|
|
89
|
-
* without an O(file-size) read on the hot path.
|
|
90
|
-
*/
|
|
91
|
-
lastSyncedFingerprint = null;
|
|
92
|
-
constructor(stateDir, secureStoreKeyProvider = () => null, secureStoreWriteKeyProvider = secureStoreKeyProvider, memoryDir = path.dirname(stateDir), lockOptions = {}) {
|
|
93
|
-
this.filePath = path.join(stateDir, "fact-hashes.txt");
|
|
94
|
-
this.secureStoreKeyProvider = secureStoreKeyProvider;
|
|
95
|
-
this.secureStoreWriteKeyProvider = secureStoreWriteKeyProvider;
|
|
96
|
-
this.memoryDir = memoryDir;
|
|
97
|
-
this.lockOptions = lockOptions;
|
|
98
|
-
}
|
|
99
|
-
/** Load existing hashes from disk. Safe to call multiple times. */
|
|
100
|
-
async load() {
|
|
101
|
-
try {
|
|
102
|
-
const raw = await readMaybeEncryptedFile(this.filePath, this.secureStoreKeyProvider(), this.memoryDir);
|
|
103
|
-
for (const line of raw.split("\n")) {
|
|
104
|
-
const trimmed = line.trim();
|
|
105
|
-
if (trimmed.length > 0) {
|
|
106
|
-
this.hashes.add(trimmed);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
112
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
113
|
-
log.debug("content-hash index: no existing index \u2014 starting fresh");
|
|
114
|
-
}
|
|
115
|
-
await this.captureSyncedFingerprint();
|
|
116
|
-
}
|
|
117
|
-
/** Cheap `stat` of the durable index file; null when it does not exist yet. */
|
|
118
|
-
async statIndexFile() {
|
|
119
|
-
try {
|
|
120
|
-
const s = await stat(this.filePath);
|
|
121
|
-
return {
|
|
122
|
-
mtimeMs: s.mtimeMs,
|
|
123
|
-
size: s.size,
|
|
124
|
-
dev: s.dev,
|
|
125
|
-
ino: s.ino,
|
|
126
|
-
ctimeMs: s.ctimeMs,
|
|
127
|
-
birthtimeMs: s.birthtimeMs
|
|
128
|
-
};
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (isErrnoCode(err, "ENOENT")) return null;
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Record the current on-disk fingerprint as this instance's synced baseline. */
|
|
135
|
-
async captureSyncedFingerprint() {
|
|
136
|
-
try {
|
|
137
|
-
this.lastSyncedFingerprint = await this.statIndexFile();
|
|
138
|
-
} catch (err) {
|
|
139
|
-
this.lastSyncedFingerprint = null;
|
|
140
|
-
log.debug(`content-hash index: could not fingerprint ${this.filePath}: ${err}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* True when the durable index file is unchanged since this instance last
|
|
145
|
-
* synced it (load / save / reconcile). False when a peer advanced it, when
|
|
146
|
-
* the file appeared or vanished, or when freshness cannot be established
|
|
147
|
-
* (stat error) — the caller then treats the in-memory snapshot as
|
|
148
|
-
* non-authoritative and confirms a dedup miss against the durable corpus.
|
|
149
|
-
* The stat includes file identity and generation metadata so same-size,
|
|
150
|
-
* same-mtime atomic replacements are detected without reading the file body.
|
|
151
|
-
*/
|
|
152
|
-
async isDiskFingerprintCurrent() {
|
|
153
|
-
let current;
|
|
154
|
-
try {
|
|
155
|
-
current = await this.statIndexFile();
|
|
156
|
-
} catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
const last = this.lastSyncedFingerprint;
|
|
160
|
-
if (last === null || current === null) {
|
|
161
|
-
return last === null && current === null;
|
|
162
|
-
}
|
|
163
|
-
return current.mtimeMs === last.mtimeMs && current.size === last.size && current.dev === last.dev && current.ino === last.ino && current.ctimeMs === last.ctimeMs && current.birthtimeMs === last.birthtimeMs;
|
|
164
|
-
}
|
|
165
|
-
/** Check if content already exists in the index. */
|
|
166
|
-
has(content) {
|
|
167
|
-
return this.hashes.has(_ContentHashIndex.computeHash(content));
|
|
168
|
-
}
|
|
169
|
-
/** Add content hash to the index. */
|
|
170
|
-
add(content) {
|
|
171
|
-
const hash = _ContentHashIndex.computeHash(content);
|
|
172
|
-
this.removed.delete(hash);
|
|
173
|
-
this.hashes.add(hash);
|
|
174
|
-
this.added.add(hash);
|
|
175
|
-
this.dirty = true;
|
|
176
|
-
}
|
|
177
|
-
get size() {
|
|
178
|
-
return this.hashes.size;
|
|
179
|
-
}
|
|
180
|
-
/** Clear all loaded hashes so the next save rewrites the index from scratch. */
|
|
181
|
-
clear() {
|
|
182
|
-
if (this.hashes.size > 0) {
|
|
183
|
-
this.hashes.clear();
|
|
184
|
-
}
|
|
185
|
-
this.added.clear();
|
|
186
|
-
this.removed.clear();
|
|
187
|
-
this.cancelReconcileRetry();
|
|
188
|
-
this.dirty = true;
|
|
189
|
-
}
|
|
190
|
-
/** Persist index to disk if changed (plain whole-file overwrite). */
|
|
191
|
-
async save() {
|
|
192
|
-
if (!this.dirty) return;
|
|
193
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
194
|
-
const publishedAdded = new Set(this.added);
|
|
195
|
-
const publishedRemoved = new Set(this.removed);
|
|
196
|
-
const serialized = [...this.hashes].join("\n") + "\n";
|
|
197
|
-
await writeMaybeEncryptedFile(
|
|
198
|
-
this.filePath,
|
|
199
|
-
serialized,
|
|
200
|
-
this.secureStoreWriteKeyProvider(),
|
|
201
|
-
{},
|
|
202
|
-
this.memoryDir
|
|
203
|
-
);
|
|
204
|
-
for (const h of publishedAdded) this.added.delete(h);
|
|
205
|
-
for (const h of publishedRemoved) this.removed.delete(h);
|
|
206
|
-
this.dirty = this.added.size > 0 || this.removed.size > 0;
|
|
207
|
-
await this.captureSyncedFingerprint();
|
|
208
|
-
if (this.dirty) {
|
|
209
|
-
this.scheduleReconcileRetry();
|
|
210
|
-
} else {
|
|
211
|
-
this.cancelReconcileRetry();
|
|
212
|
-
}
|
|
213
|
-
log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Persist the index by RECONCILING with the latest on-disk state under a
|
|
217
|
-
* cross-process lock (issue #1909). Used by the extraction-persist batch save
|
|
218
|
-
* for BOTH append and removal batches, so appends and removals to the same
|
|
219
|
-
* index serialize against each other:
|
|
220
|
-
* final = (on-disk \ this.removed) ∪ this.added
|
|
221
|
-
* Note it re-applies only OUR local additions (`added`), NOT the loaded
|
|
222
|
-
* snapshot (review round 9 finding 3): seeding from `this.hashes` would
|
|
223
|
-
* resurrect a hash a peer removed on disk while we held a stale snapshot. This
|
|
224
|
-
* preserves a concurrent writer's appended hashes (a blind overwrite would
|
|
225
|
-
* drop them), never resurrects a removed hash, and never loses our additions.
|
|
226
|
-
*
|
|
227
|
-
* Hardening:
|
|
228
|
-
* - Dirty short-circuit (round 7 finding 3): when this instance neither added
|
|
229
|
-
* nor removed anything this run, skip the O(file) read+rewrite entirely.
|
|
230
|
-
* - Cross-process lock (round 7 finding 2): serialize the read→reconcile→write
|
|
231
|
-
* window on a per-file advisory lock. If the lock is NOT acquired (timeout),
|
|
232
|
-
* do NOT publish (round 9 finding 2): keep `dirty` set AND schedule a bounded
|
|
233
|
-
* durable background retry (PR #2016) that keeps re-attempting the locked
|
|
234
|
-
* publish until the deferred additions land — never an unlocked write that
|
|
235
|
-
* could clobber a peer, and never a silent "batch save complete" that leaves
|
|
236
|
-
* the addition in this process's memory only.
|
|
237
|
-
*/
|
|
238
|
-
async saveMergingWithDisk() {
|
|
239
|
-
if (!this.dirty) return;
|
|
240
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
241
|
-
const publish = async () => {
|
|
242
|
-
const publishedAdded = new Set(this.added);
|
|
243
|
-
const publishedRemoved = new Set(this.removed);
|
|
244
|
-
const merged = new Set(publishedAdded);
|
|
245
|
-
try {
|
|
246
|
-
const raw = await readMaybeEncryptedFile(
|
|
247
|
-
this.filePath,
|
|
248
|
-
this.secureStoreKeyProvider(),
|
|
249
|
-
this.memoryDir
|
|
250
|
-
);
|
|
251
|
-
for (const line of raw.split("\n")) {
|
|
252
|
-
const trimmed = line.trim();
|
|
253
|
-
if (trimmed.length > 0 && !publishedRemoved.has(trimmed)) merged.add(trimmed);
|
|
254
|
-
}
|
|
255
|
-
} catch (err) {
|
|
256
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
257
|
-
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
258
|
-
}
|
|
259
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
260
|
-
await writeMaybeEncryptedFile(
|
|
261
|
-
this.filePath,
|
|
262
|
-
[...merged].join("\n") + "\n",
|
|
263
|
-
this.secureStoreWriteKeyProvider(),
|
|
264
|
-
{},
|
|
265
|
-
this.memoryDir
|
|
266
|
-
);
|
|
267
|
-
for (const h of publishedAdded) this.added.delete(h);
|
|
268
|
-
for (const h of publishedRemoved) this.removed.delete(h);
|
|
269
|
-
for (const h of this.added) merged.add(h);
|
|
270
|
-
for (const h of this.removed) merged.delete(h);
|
|
271
|
-
this.hashes = merged;
|
|
272
|
-
this.dirty = this.added.size > 0 || this.removed.size > 0;
|
|
273
|
-
await this.captureSyncedFingerprint();
|
|
274
|
-
};
|
|
275
|
-
await withHeldFileLock(
|
|
276
|
-
`${this.filePath}.lock`,
|
|
277
|
-
{
|
|
278
|
-
staleMs: CONTENT_HASH_INDEX_LOCK_STALE_MS,
|
|
279
|
-
...this.lockOptions.maxWaitMs !== void 0 ? { maxWaitMs: this.lockOptions.maxWaitMs } : {},
|
|
280
|
-
...this.lockOptions.pollMs !== void 0 ? { pollMs: this.lockOptions.pollMs } : {}
|
|
281
|
-
},
|
|
282
|
-
async (acquired) => {
|
|
283
|
-
if (!acquired) {
|
|
284
|
-
log.warn(
|
|
285
|
-
`content-hash index: lock not acquired for ${this.filePath}; deferring reconcile save (dirty retained, scheduling durable retry)`
|
|
286
|
-
);
|
|
287
|
-
this.scheduleReconcileRetry();
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
await publish();
|
|
291
|
-
if (this.dirty) {
|
|
292
|
-
this.scheduleReconcileRetry();
|
|
293
|
-
} else {
|
|
294
|
-
this.cancelReconcileRetry();
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
);
|
|
298
|
-
log.debug(`content-hash index: reconcile-saved ${this.hashes.size} hashes`);
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Rebuild-and-publish the index from an authoritative source (the durable
|
|
302
|
-
* corpus) under the SAME per-file cross-process lock the reconciling
|
|
303
|
-
* append/removal saves (`saveMergingWithDisk`) use (issue #1909 / PR #2016).
|
|
304
|
-
* The `populate` callback runs WHILE the lock is held and MUST repopulate this
|
|
305
|
-
* index from the corpus (`clear()` then `addByHash(...)` for every corpus
|
|
306
|
-
* hash); the rebuilt set is then published with a plain overwrite that is now
|
|
307
|
-
* serialized against every locked writer.
|
|
308
|
-
*
|
|
309
|
-
* This closes the finding where the previous unlocked clear→scan→save() could
|
|
310
|
-
* overwrite a peer's newer lock-merged or deferred additions: a locked writer
|
|
311
|
-
* that committed BEFORE the rebuild acquired the lock has its memory `.md` on
|
|
312
|
-
* disk and is re-scanned; one that commits AFTER the rebuild releases
|
|
313
|
-
* reconciles its additions on top of the freshly-published set. No union with
|
|
314
|
-
* the raw on-disk file is performed, so the rebuild keeps its
|
|
315
|
-
* garbage-collection property (orphaned hashes for deleted memories drop).
|
|
316
|
-
*
|
|
317
|
-
* Lock-acquisition failure is explicit and non-destructive: `populate` is NOT
|
|
318
|
-
* run and nothing is written (never an unlocked overwrite). Returns true when
|
|
319
|
-
* the rebuild was published under the lock, false when the lock timed out —
|
|
320
|
-
* the caller keeps the index non-authoritative and retries on next use. Uses
|
|
321
|
-
* the same non-reentrant file lock as the reconcile path, so it can never
|
|
322
|
-
* deadlock with the deferred reconcile-retry: a concurrent locked writer (or a
|
|
323
|
-
* fired retry) simply yields `acquired=false` after the bounded wait.
|
|
324
|
-
*/
|
|
325
|
-
async rebuildUnderLock(populate) {
|
|
326
|
-
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
327
|
-
let published = false;
|
|
328
|
-
await withHeldFileLock(
|
|
329
|
-
`${this.filePath}.lock`,
|
|
330
|
-
{
|
|
331
|
-
staleMs: CONTENT_HASH_INDEX_LOCK_STALE_MS,
|
|
332
|
-
...this.lockOptions.maxWaitMs !== void 0 ? { maxWaitMs: this.lockOptions.maxWaitMs } : {},
|
|
333
|
-
...this.lockOptions.pollMs !== void 0 ? { pollMs: this.lockOptions.pollMs } : {}
|
|
334
|
-
},
|
|
335
|
-
async (acquired) => {
|
|
336
|
-
if (!acquired) {
|
|
337
|
-
log.warn(
|
|
338
|
-
`content-hash index: lock not acquired for ${this.filePath}; deferring authoritative rebuild (index left non-authoritative, retried on next use)`
|
|
339
|
-
);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
await populate();
|
|
343
|
-
await this.save();
|
|
344
|
-
published = true;
|
|
345
|
-
}
|
|
346
|
-
);
|
|
347
|
-
return published;
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* True while a background durable reconcile-save retry is armed (PR #2016).
|
|
351
|
-
* Test/introspection hook.
|
|
352
|
-
*/
|
|
353
|
-
get hasPendingReconcileRetry() {
|
|
354
|
-
return this.reconcileRetryTimer !== null;
|
|
355
|
-
}
|
|
356
|
-
/** True while this index has in-memory changes not yet published to disk. */
|
|
357
|
-
get hasPendingChanges() {
|
|
358
|
-
return this.dirty;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Resolve once any armed background reconcile-save retry chain has settled —
|
|
362
|
-
* either it published the deferred additions, or it exhausted its bounded
|
|
363
|
-
* attempts and fell back to the corpus-rebuild safety net. Resolves
|
|
364
|
-
* immediately when nothing is armed. Lets a caller (or a test) await the
|
|
365
|
-
* eventual durability outcome of a lock-timed-out save.
|
|
366
|
-
*/
|
|
367
|
-
async whenReconcileRetrySettled() {
|
|
368
|
-
await (this.reconcileRetryBarrier ?? Promise.resolve());
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Drive any deferred lock-timeout reconcile save to completion INLINE at a
|
|
372
|
-
* lifecycle boundary (PR #2016 finding 3). The background retry timer is
|
|
373
|
-
* `unref`'d so it never keeps a long-lived daemon alive — but a short-lived
|
|
374
|
-
* writer (a one-shot CLI) can exit before it fires, leaving a durable fact
|
|
375
|
-
* `.md` whose hash never reached `fact-hashes.txt` for peers that already
|
|
376
|
-
* built their in-memory index. `orchestrator.destroy()` calls this so the
|
|
377
|
-
* addition publishes before the process exits; long-lived hosts only reach it
|
|
378
|
-
* at their own shutdown, so their in-flight retries keep running in the
|
|
379
|
-
* background until then. Bounded by the same attempt ceiling as the background
|
|
380
|
-
* retry; a permanently contended lock falls back to the corpus-rebuild-on-
|
|
381
|
-
* restart safety net (the fact `.md` is already durable). No deadlock: each
|
|
382
|
-
* attempt is the same non-reentrant, bounded-wait file lock.
|
|
383
|
-
*/
|
|
384
|
-
async flushReconcileRetry() {
|
|
385
|
-
if (!this.dirty) return;
|
|
386
|
-
const maxAttempts = this.lockOptions.retryMaxAttempts ?? CONTENT_HASH_INDEX_RETRY_MAX_ATTEMPTS;
|
|
387
|
-
const baseMs = this.lockOptions.retryBaseMs ?? CONTENT_HASH_INDEX_RETRY_BASE_MS;
|
|
388
|
-
for (let attempt = 0; this.dirty && attempt < maxAttempts; attempt += 1) {
|
|
389
|
-
if (this.reconcileRetryTimer) {
|
|
390
|
-
clearTimeout(this.reconcileRetryTimer);
|
|
391
|
-
this.reconcileRetryTimer = null;
|
|
392
|
-
}
|
|
393
|
-
await this.saveMergingWithDisk();
|
|
394
|
-
if (!this.dirty) break;
|
|
395
|
-
if (attempt < maxAttempts - 1) {
|
|
396
|
-
const wait = Math.min(baseMs * 2 ** attempt, CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS);
|
|
397
|
-
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (this.reconcileRetryTimer) {
|
|
401
|
-
clearTimeout(this.reconcileRetryTimer);
|
|
402
|
-
this.reconcileRetryTimer = null;
|
|
403
|
-
}
|
|
404
|
-
this.reconcileRetryAttempts = 0;
|
|
405
|
-
this.settleReconcileRetry();
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Schedule ONE bounded, exponentially-backed-off background retry of the
|
|
409
|
-
* deferred reconcile save (PR #2016). Reentrant-safe: an already-armed timer
|
|
410
|
-
* short-circuits, so successive timed-out saves never stack duplicate retries.
|
|
411
|
-
* When attempts are exhausted the chain settles without publishing — the
|
|
412
|
-
* durable fact `.md` is on disk and a process restart rebuilds the index from
|
|
413
|
-
* the corpus, so this degrades to that safety net, never a fatal failure.
|
|
414
|
-
*/
|
|
415
|
-
scheduleReconcileRetry() {
|
|
416
|
-
if (this.reconcileRetryTimer) return;
|
|
417
|
-
const maxAttempts = this.lockOptions.retryMaxAttempts ?? CONTENT_HASH_INDEX_RETRY_MAX_ATTEMPTS;
|
|
418
|
-
if (this.reconcileRetryAttempts >= maxAttempts) {
|
|
419
|
-
log.warn(
|
|
420
|
-
`content-hash index: exhausted ${maxAttempts} deferred reconcile-save retries for ${this.filePath}; ${this.added.size} addition(s) remain dirty in-memory (a process restart rebuilds the index from the durable corpus)`
|
|
421
|
-
);
|
|
422
|
-
this.reconcileRetryAttempts = 0;
|
|
423
|
-
this.settleReconcileRetry();
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (!this.reconcileRetryBarrier) {
|
|
427
|
-
this.reconcileRetryBarrier = new Promise((resolve) => {
|
|
428
|
-
this.reconcileRetryResolve = resolve;
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
const attempt = this.reconcileRetryAttempts + 1;
|
|
432
|
-
const baseMs = this.lockOptions.retryBaseMs ?? CONTENT_HASH_INDEX_RETRY_BASE_MS;
|
|
433
|
-
const delay = Math.min(baseMs * 2 ** (attempt - 1), CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS);
|
|
434
|
-
this.reconcileRetryTimer = setTimeout(() => {
|
|
435
|
-
this.reconcileRetryTimer = null;
|
|
436
|
-
this.reconcileRetryAttempts = attempt;
|
|
437
|
-
void this.saveMergingWithDisk().catch((err) => {
|
|
438
|
-
log.warn(`content-hash index: deferred reconcile-save retry failed for ${this.filePath}: ${err}`);
|
|
439
|
-
}).finally(() => {
|
|
440
|
-
if (this.dirty && !this.reconcileRetryTimer) this.settleReconcileRetry();
|
|
441
|
-
});
|
|
442
|
-
}, delay);
|
|
443
|
-
this.reconcileRetryTimer.unref?.();
|
|
444
|
-
}
|
|
445
|
-
/** Cancel any armed reconcile-save retry and settle the barrier (a publish landed). */
|
|
446
|
-
cancelReconcileRetry() {
|
|
447
|
-
if (this.reconcileRetryTimer) {
|
|
448
|
-
clearTimeout(this.reconcileRetryTimer);
|
|
449
|
-
this.reconcileRetryTimer = null;
|
|
450
|
-
}
|
|
451
|
-
this.reconcileRetryAttempts = 0;
|
|
452
|
-
this.settleReconcileRetry();
|
|
453
|
-
}
|
|
454
|
-
/** Resolve and clear the retry barrier if one is outstanding. */
|
|
455
|
-
settleReconcileRetry() {
|
|
456
|
-
const resolve = this.reconcileRetryResolve;
|
|
457
|
-
this.reconcileRetryBarrier = null;
|
|
458
|
-
this.reconcileRetryResolve = null;
|
|
459
|
-
resolve?.();
|
|
460
|
-
}
|
|
461
|
-
/** Remove a hash from the index (used when archiving/deleting). */
|
|
462
|
-
remove(content) {
|
|
463
|
-
const hash = _ContentHashIndex.computeHash(content);
|
|
464
|
-
this.added.delete(hash);
|
|
465
|
-
this.removed.add(hash);
|
|
466
|
-
this.hashes.delete(hash);
|
|
467
|
-
this.dirty = true;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Remove a pre-computed SHA-256 hash directly from the index without
|
|
471
|
-
* re-hashing. Use this when the caller already holds the stored hash
|
|
472
|
-
* (e.g. `memory.frontmatter.contentHash`) to avoid the double-hash bug
|
|
473
|
-
* where `remove(hash)` would compute `hash(hash)` and never match the
|
|
474
|
-
* entry.
|
|
475
|
-
*/
|
|
476
|
-
removeByHash(hash) {
|
|
477
|
-
this.added.delete(hash);
|
|
478
|
-
this.removed.add(hash);
|
|
479
|
-
this.hashes.delete(hash);
|
|
480
|
-
this.dirty = true;
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* Add a pre-computed SHA-256 hash directly to the index without re-hashing.
|
|
484
|
-
* Use this when the caller already holds the stored hash
|
|
485
|
-
* (e.g. `memory.frontmatter.contentHash`) so that the index records the raw
|
|
486
|
-
* content hash rather than re-hashing the citation-annotated body.
|
|
487
|
-
*
|
|
488
|
-
* @internal Only called from `StorageManager.ensureFactHashIndexAuthoritative`.
|
|
489
|
-
* Not part of the public API — prefer `add(content)` for external callers.
|
|
490
|
-
*/
|
|
491
|
-
addByHash(hash) {
|
|
492
|
-
this.removed.delete(hash);
|
|
493
|
-
this.hashes.add(hash);
|
|
494
|
-
this.added.add(hash);
|
|
495
|
-
this.dirty = true;
|
|
496
|
-
}
|
|
497
|
-
/** Normalize content (delegates to content-hash.ts for a single source of truth). */
|
|
498
|
-
static normalizeContent(content) {
|
|
499
|
-
return normalizeContent(content);
|
|
500
|
-
}
|
|
501
|
-
/** Compute SHA-256 hash (delegates to content-hash.ts for a single source of truth). */
|
|
502
|
-
static computeHash(content) {
|
|
503
|
-
return computeContentHash(content);
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
|
|
507
29
|
// src/storage/tombstone-blocked-capture-mutation.ts
|
|
508
30
|
import { AsyncLocalStorage } from "async_hooks";
|
|
509
31
|
import { createHash } from "crypto";
|
|
510
|
-
import { mkdir
|
|
511
|
-
import
|
|
32
|
+
import { mkdir } from "fs/promises";
|
|
33
|
+
import path from "path";
|
|
512
34
|
var TOMBSTONE_CAPTURE_WRITE_LOCK_STALE_MS = 6e4;
|
|
513
35
|
var CAPTURE_WRITE_LOCK_MAX_ATTEMPTS = 3;
|
|
514
36
|
var CAPTURE_WRITE_LOCK_RETRY_BASE_MS = 25;
|
|
@@ -538,10 +60,10 @@ var TombstoneBlockedCaptureWriteLock = class {
|
|
|
538
60
|
context = new AsyncLocalStorage();
|
|
539
61
|
captureWriteLockPathForHash(identityHash) {
|
|
540
62
|
const filename = identityHash.length === 0 ? "explicit-capture-write.lock" : `explicit-capture-write-${identityHash}.lock`;
|
|
541
|
-
return
|
|
63
|
+
return path.join(this.options.stateDir, "tombstone-blocked-capture", filename);
|
|
542
64
|
}
|
|
543
65
|
async withSingleCaptureWriteLock(lockPath, task) {
|
|
544
|
-
await
|
|
66
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
545
67
|
const retry = /* @__PURE__ */ Symbol("retry");
|
|
546
68
|
const permanentFailure = /* @__PURE__ */ Symbol("permanent-failure");
|
|
547
69
|
const runWithHeldFileLock = this.options.withHeldFileLock ?? withHeldFileLock;
|
|
@@ -693,14 +215,14 @@ async function runTombstoneBlockedChunkMutation(mutation) {
|
|
|
693
215
|
// src/storage/tombstone-blocked-capture-sync.ts
|
|
694
216
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
695
217
|
import { unlink as unlink2 } from "fs/promises";
|
|
696
|
-
import
|
|
218
|
+
import path3 from "path";
|
|
697
219
|
import { isDeepStrictEqual } from "util";
|
|
698
220
|
|
|
699
221
|
// src/storage/tombstone-blocked-capture-index.ts
|
|
700
222
|
import { execFile } from "child_process";
|
|
701
223
|
import { randomUUID } from "crypto";
|
|
702
|
-
import { mkdir as
|
|
703
|
-
import
|
|
224
|
+
import { mkdir as mkdir2, readFile, readdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
225
|
+
import path2 from "path";
|
|
704
226
|
import { promisify } from "util";
|
|
705
227
|
var REBUILD_MAX_ATTEMPTS = 3;
|
|
706
228
|
var execFileAsync = promisify(execFile);
|
|
@@ -722,7 +244,7 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
722
244
|
}
|
|
723
245
|
createIndex() {
|
|
724
246
|
return new ContentHashIndex(
|
|
725
|
-
|
|
247
|
+
path2.join(this.options.stateDir, "tombstone-blocked-capture"),
|
|
726
248
|
this.options.secureStoreKeyProvider,
|
|
727
249
|
this.options.secureStoreWriteKeyProvider,
|
|
728
250
|
this.options.memoryDir,
|
|
@@ -730,17 +252,17 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
730
252
|
);
|
|
731
253
|
}
|
|
732
254
|
indexPath() {
|
|
733
|
-
return
|
|
255
|
+
return path2.join(this.options.stateDir, "tombstone-blocked-capture", "fact-hashes.txt");
|
|
734
256
|
}
|
|
735
257
|
rebuildMarkerPath() {
|
|
736
|
-
return
|
|
258
|
+
return path2.join(this.options.stateDir, "tombstone-blocked-capture", "rebuild-required");
|
|
737
259
|
}
|
|
738
260
|
async readRebuildMarkers() {
|
|
739
261
|
try {
|
|
740
262
|
const entries = (await readdir(this.rebuildMarkerPath())).filter((entry) => !entry.startsWith("."));
|
|
741
263
|
return (await Promise.all(
|
|
742
264
|
entries.map(async (entry) => {
|
|
743
|
-
const markerPath =
|
|
265
|
+
const markerPath = path2.join(this.rebuildMarkerPath(), entry);
|
|
744
266
|
try {
|
|
745
267
|
const raw = (await readFile(markerPath, "utf8")).trim();
|
|
746
268
|
if (raw === "committed") return { path: markerPath, committed: true };
|
|
@@ -760,10 +282,10 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
760
282
|
}
|
|
761
283
|
const state = value.state;
|
|
762
284
|
if (state !== "pending" && state !== "committed") malformed = true;
|
|
763
|
-
const markerStat = await
|
|
285
|
+
const markerStat = await stat(markerPath).catch(() => null);
|
|
764
286
|
const createdAt = typeof value.createdAt === "number" && Number.isFinite(value.createdAt) ? value.createdAt : markerStat?.mtimeMs;
|
|
765
287
|
if (typeof value.pid !== "number" || !Number.isInteger(value.pid)) malformed = true;
|
|
766
|
-
const ownerId = typeof value.ownerId === "string" && value.ownerId.length > 0 ? value.ownerId :
|
|
288
|
+
const ownerId = typeof value.ownerId === "string" && value.ownerId.length > 0 ? value.ownerId : path2.basename(markerPath);
|
|
767
289
|
if (typeof value.ownerId !== "string" || value.ownerId.length === 0) malformed = true;
|
|
768
290
|
const processStartedAtMs = typeof value.processStartedAtMs === "number" && Number.isFinite(value.processStartedAtMs) && value.processStartedAtMs > 0 ? value.processStartedAtMs : void 0;
|
|
769
291
|
return {
|
|
@@ -855,9 +377,9 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
855
377
|
}
|
|
856
378
|
async markRebuildRequired(committed = false) {
|
|
857
379
|
const markerDir = this.rebuildMarkerPath();
|
|
858
|
-
await
|
|
380
|
+
await mkdir2(markerDir, { recursive: true });
|
|
859
381
|
const ownerId = randomUUID();
|
|
860
|
-
const markerPath =
|
|
382
|
+
const markerPath = path2.join(markerDir, ownerId);
|
|
861
383
|
await this.writeMarkerAtomically(
|
|
862
384
|
markerPath,
|
|
863
385
|
`${JSON.stringify({
|
|
@@ -877,7 +399,7 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
877
399
|
`${JSON.stringify({
|
|
878
400
|
state: "committed",
|
|
879
401
|
pid: process.pid,
|
|
880
|
-
ownerId:
|
|
402
|
+
ownerId: path2.basename(markerPath),
|
|
881
403
|
createdAt: Date.now(),
|
|
882
404
|
processStartedAtMs: MARKER_PROCESS_STARTED_AT_MS
|
|
883
405
|
})}
|
|
@@ -919,7 +441,7 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
919
441
|
}
|
|
920
442
|
async hasPersistedIndexOrMarkers() {
|
|
921
443
|
try {
|
|
922
|
-
await
|
|
444
|
+
await stat(this.indexPath());
|
|
923
445
|
return true;
|
|
924
446
|
} catch (err) {
|
|
925
447
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
@@ -936,7 +458,7 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
936
458
|
const clearableRebuildMarkers = await this.getClearableRebuildMarkerPaths(rebuildMarkers);
|
|
937
459
|
let persisted = true;
|
|
938
460
|
try {
|
|
939
|
-
await
|
|
461
|
+
await stat(this.indexPath());
|
|
940
462
|
if (rebuildMarkers.length > 0) persisted = false;
|
|
941
463
|
} catch (err) {
|
|
942
464
|
if (isErrnoCode(err, "ENOENT")) persisted = false;
|
|
@@ -1121,8 +643,8 @@ var TombstoneBlockedCaptureIndex = class {
|
|
|
1121
643
|
await this.failOpen("writeMemoryFrontmatter", () => this.sync(before, { ...before, frontmatter }, rebuildMarker));
|
|
1122
644
|
}
|
|
1123
645
|
shouldRebuildAfterInvalidationForPath(filePath) {
|
|
1124
|
-
const relative =
|
|
1125
|
-
const category = relative.split(
|
|
646
|
+
const relative = path2.relative(path2.resolve(this.options.memoryDir), path2.resolve(filePath));
|
|
647
|
+
const category = relative.split(path2.sep)[0];
|
|
1126
648
|
return category === "cold" || RECALL_FALLBACK_DIRS.includes(category);
|
|
1127
649
|
}
|
|
1128
650
|
};
|
|
@@ -1383,11 +905,11 @@ var TombstoneBlockedCaptureIndexHost = class {
|
|
|
1383
905
|
this.invalidateAllMemoriesCache();
|
|
1384
906
|
this.invalidateKnowledgeIndexCache();
|
|
1385
907
|
this.markFactHashIndexNotAuthoritative();
|
|
1386
|
-
if (filePath.includes(`${
|
|
908
|
+
if (filePath.includes(`${path3.sep}cold${path3.sep}`)) {
|
|
1387
909
|
this.invalidateColdMemoriesCache();
|
|
1388
910
|
}
|
|
1389
911
|
await this.rebuildTombstoneBlockedCaptureAfterInvalidationForPath(filePath, ownedMarker);
|
|
1390
|
-
if (filePath.includes(`${
|
|
912
|
+
if (filePath.includes(`${path3.sep}artifacts${path3.sep}`)) {
|
|
1391
913
|
this.bumpArtifactWriteVersion();
|
|
1392
914
|
}
|
|
1393
915
|
this.bumpMemoryStatusVersion();
|
|
@@ -1483,7 +1005,7 @@ var TombstoneBlockedCaptureIndexHost = class {
|
|
|
1483
1005
|
const memoryDir = this.tombstoneBlockedCaptureIndexOptions().memoryDir;
|
|
1484
1006
|
markProjectedMemoryPathInvalid(memoryDir, current.frontmatter.id);
|
|
1485
1007
|
this.invalidateAllMemoriesCache();
|
|
1486
|
-
if (current.path.includes(`${
|
|
1008
|
+
if (current.path.includes(`${path3.sep}cold${path3.sep}`)) {
|
|
1487
1009
|
this.invalidateColdMemoriesCache();
|
|
1488
1010
|
}
|
|
1489
1011
|
await this.rebuildTombstoneBlockedCaptureAfterInvalidation(rebuildMarker);
|
|
@@ -1601,7 +1123,7 @@ var TombstoneBlockedCaptureIndexHost = class {
|
|
|
1601
1123
|
return { after, chunks: replay };
|
|
1602
1124
|
}
|
|
1603
1125
|
const options = this.tombstoneBlockedCaptureIndexOptions();
|
|
1604
|
-
const stagePath =
|
|
1126
|
+
const stagePath = path3.join(
|
|
1605
1127
|
options.stateDir,
|
|
1606
1128
|
"tombstone-blocked-capture",
|
|
1607
1129
|
"offline-sync-staging",
|
|
@@ -1738,7 +1260,7 @@ var TombstoneBlockedCaptureIndexHost = class {
|
|
|
1738
1260
|
*/
|
|
1739
1261
|
async requestSyncReconcileIfMemoryPath(target) {
|
|
1740
1262
|
const stateDir = this.tombstoneBlockedCaptureIndexOptions().stateDir;
|
|
1741
|
-
if (!pathMayCarryEntityRefs(
|
|
1263
|
+
if (!pathMayCarryEntityRefs(path3.dirname(stateDir), target)) return;
|
|
1742
1264
|
await requestEntityCanonicalIdReconcile(stateDir);
|
|
1743
1265
|
}
|
|
1744
1266
|
async deleteOfflineSyncFile(filePath) {
|
|
@@ -1774,13 +1296,10 @@ var TombstoneBlockedCaptureIndexHost = class {
|
|
|
1774
1296
|
};
|
|
1775
1297
|
|
|
1776
1298
|
export {
|
|
1777
|
-
CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
|
|
1778
|
-
FactHashIndexNotAuthoritativeError,
|
|
1779
|
-
ContentHashIndex,
|
|
1780
1299
|
isTombstoneBlockedCaptureWriteLockBusy,
|
|
1781
1300
|
normalizeExplicitCaptureContent,
|
|
1782
1301
|
buildExplicitCaptureDedupKey,
|
|
1783
1302
|
parseTombstoneBlockedOfflineSyncMemory,
|
|
1784
1303
|
TombstoneBlockedCaptureIndexHost
|
|
1785
1304
|
};
|
|
1786
|
-
//# sourceMappingURL=chunk-
|
|
1305
|
+
//# sourceMappingURL=chunk-RVLJEII5.js.map
|