@remnic/core 9.52.2 → 9.53.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 +22 -22
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +23 -23
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +22 -22
- package/dist/access-cli.js +54 -54
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +22 -22
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http-offline-stream.d.ts +5 -5
- package/dist/access-http-query.js +22 -22
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +29 -29
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +22 -22
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +22 -22
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +27 -27
- package/dist/access-namespace-preflight.js +22 -22
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +22 -22
- package/dist/access-offline-manifest.d.ts +4 -4
- package/dist/access-offline-manifest.js +8 -4
- package/dist/access-operations-batch.js +24 -24
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +26 -26
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +22 -22
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +22 -22
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +22 -22
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-Cbk3bNbD.d.ts → access-service-CgCjvbQu.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +22 -22
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/active-recall.js +4 -4
- package/dist/{auto-sync-QFQGOEO2.js → auto-sync-JXEW7444.js} +6 -6
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +12 -12
- package/dist/buffer.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/{chunk-MLXZD4WM.js → chunk-2GFK22OZ.js} +2 -2
- package/dist/{chunk-D5YOZ66N.js → chunk-3DWTDC54.js} +20 -20
- package/dist/{chunk-RANKJI5T.js → chunk-3JLPGJ43.js} +2 -2
- package/dist/chunk-5FVVY2EB.js +27 -0
- package/dist/chunk-5FVVY2EB.js.map +1 -0
- package/dist/{chunk-GXSOUIA4.js → chunk-5WQMORU5.js} +4 -4
- package/dist/{chunk-J272WKX2.js → chunk-6HPHEFXT.js} +2 -2
- package/dist/{chunk-B6LZ4IBH.js → chunk-6LAPLZOS.js} +2 -2
- package/dist/{chunk-WD5B5AIE.js → chunk-6R4G7N4S.js} +118 -173
- package/dist/chunk-6R4G7N4S.js.map +1 -0
- package/dist/{chunk-VXCNQYTC.js → chunk-APPFNGX6.js} +2 -2
- package/dist/{chunk-RMHMUUBW.js → chunk-AY43LBE4.js} +104 -48
- package/dist/{chunk-RMHMUUBW.js.map → chunk-AY43LBE4.js.map} +1 -1
- package/dist/{chunk-GXFSQ6SD.js → chunk-C4ZNGVK3.js} +2 -2
- package/dist/{chunk-JSEJH2YQ.js → chunk-CGVHPCP2.js} +2 -2
- package/dist/{chunk-BQIFZ2MP.js → chunk-CX5SW2EW.js} +41 -24
- package/dist/chunk-CX5SW2EW.js.map +1 -0
- package/dist/{chunk-TEMSWHQD.js → chunk-DZD576ZU.js} +5 -5
- package/dist/{chunk-AMTEYNYK.js → chunk-EVWWT3KE.js} +4 -4
- package/dist/{chunk-XAPMEU3K.js → chunk-FHFERRYE.js} +2 -2
- package/dist/{chunk-P2KBDJJ6.js → chunk-G2LUMAUF.js} +4 -3
- package/dist/{chunk-P2KBDJJ6.js.map → chunk-G2LUMAUF.js.map} +1 -1
- package/dist/{chunk-AXIDQG5A.js → chunk-G4T7L557.js} +5 -5
- package/dist/{chunk-7M6NYQWB.js → chunk-G7Z6EENO.js} +4 -4
- package/dist/{chunk-YXKVLYXU.js → chunk-HWRD23DW.js} +2 -2
- package/dist/{chunk-SOEN6KCF.js → chunk-IBKR2TFR.js} +2 -2
- package/dist/{chunk-R3DJVOSM.js → chunk-JDSVLVCG.js} +8 -1
- package/dist/chunk-JDSVLVCG.js.map +1 -0
- package/dist/{chunk-F4EU6VUC.js → chunk-JDZPNCMA.js} +2 -2
- package/dist/{chunk-DLG5V6ND.js → chunk-K4QS7LXU.js} +4 -7
- package/dist/chunk-K4QS7LXU.js.map +1 -0
- package/dist/{chunk-BAKXNQ3C.js → chunk-KRTY3ENI.js} +2 -2
- package/dist/{chunk-NK5RGJ65.js → chunk-L4DLLKPN.js} +2 -2
- package/dist/{chunk-VFSAWRMZ.js → chunk-LJS7KUPV.js} +2 -2
- package/dist/{chunk-JJRVPWFY.js → chunk-MLPTFODV.js} +166 -37
- package/dist/chunk-MLPTFODV.js.map +1 -0
- package/dist/{chunk-NOEMGE56.js → chunk-NH5IR2WM.js} +2 -2
- package/dist/{chunk-5V6IXHPI.js → chunk-NSVDUN76.js} +2 -2
- package/dist/{chunk-IARXXZU4.js → chunk-P7XI2AQ3.js} +2 -2
- package/dist/{chunk-W7Y5JDVB.js → chunk-PDDTI7IC.js} +4 -4
- package/dist/{chunk-QINJITDS.js → chunk-PX2LSHYM.js} +2 -2
- package/dist/{chunk-O5FFBO2M.js → chunk-QAVH6X2A.js} +2 -2
- package/dist/{chunk-HDIUYMSB.js → chunk-QWGE2PUH.js} +6 -6
- package/dist/{chunk-X57M2Q4I.js → chunk-RDJRFDJY.js} +3 -3
- package/dist/{chunk-RYLEDKQ2.js → chunk-RUMNY5DT.js} +3 -3
- package/dist/{chunk-BKIFTXHR.js → chunk-RZ4XGKOJ.js} +2 -2
- package/dist/{chunk-66SE3BNH.js → chunk-SUPEYLH2.js} +2 -2
- package/dist/{chunk-BF6SXNQJ.js → chunk-T2JHZWH6.js} +20 -20
- package/dist/{chunk-VHFXP27N.js → chunk-THPPCISA.js} +60 -23
- package/dist/chunk-THPPCISA.js.map +1 -0
- package/dist/{chunk-QPOHMO6M.js → chunk-UES4BHA2.js} +2 -2
- package/dist/{chunk-X7XEDOSL.js → chunk-VLL43QEQ.js} +8 -8
- package/dist/{chunk-QBSFOKY6.js → chunk-VOELBUS4.js} +4 -4
- package/dist/{chunk-2LY6GIIE.js → chunk-WDBX6VER.js} +2 -2
- package/dist/{chunk-BMEYMKWX.js → chunk-WM65LU67.js} +2 -2
- package/dist/{chunk-WLRD2TXF.js → chunk-WOPA3SGI.js} +4 -4
- package/dist/{chunk-5UZ6UCOZ.js → chunk-XUAMCLY4.js} +3 -3
- package/dist/{chunk-7HXZJAID.js → chunk-YGLY6QFV.js} +3 -3
- package/dist/{chunk-2OIQDTDC.js → chunk-ZPJ4QSGP.js} +2 -2
- package/dist/{chunk-3OY7KXHM.js → chunk-ZYLC7NFO.js} +3 -3
- package/dist/{cli-DMAFI0qt.d.ts → cli-D-HiEYhl.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +41 -41
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +13 -13
- package/dist/config.js +4 -4
- package/dist/connectors/codex-materialize-runner.js +12 -12
- package/dist/connectors/index.js +12 -12
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/content-hash.d.ts +12 -2
- package/dist/content-hash.js +9 -3
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/contradiction/index.js +3 -3
- package/dist/convergence-refresh.d.ts +1 -1
- package/dist/convergence-refresh.js +14 -14
- package/dist/corpus-watermark.js +14 -14
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +12 -12
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +8 -8
- package/dist/external-wiki-access.d.ts +5 -5
- package/dist/external-wiki-access.js +23 -23
- package/dist/external-wiki-mcp-tools.d.ts +5 -5
- package/dist/extraction-normalization.js +2 -2
- package/dist/extraction-source-grounding.js +3 -3
- package/dist/extraction.js +16 -16
- package/dist/{forget-HDS4N6CG.js → forget-EHGGJZLX.js} +3 -2
- package/dist/{forget-HDS4N6CG.js.map → forget-EHGGJZLX.js.map} +1 -1
- package/dist/harmonic-construction.js +2 -2
- package/dist/harmonic-retrieval.js +4 -4
- package/dist/index.d.ts +684 -684
- package/dist/index.js +66 -66
- package/dist/maintenance/memory-governance.js +12 -12
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -12
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +13 -13
- package/dist/{maintenance-kBII2qUI.d.ts → maintenance-CN_Ct-Hr.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 +13 -13
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +12 -12
- package/dist/operator-doctor-corpus.js +15 -15
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +20 -20
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +17 -17
- package/dist/{orchestrator-DHy5LtTy.d.ts → orchestrator-B3Xe8qu0.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +54 -54
- package/dist/provenance.js +2 -2
- package/dist/reconcile/manifest.d.ts +3 -1
- package/dist/reconcile/manifest.js +7 -3
- package/dist/{resolution-MDPNOEG6.js → resolution-MMOD5WC4.js} +4 -4
- package/dist/resume-bundles.js +5 -5
- package/dist/salvage-envelope.js +3 -3
- package/dist/schemas.d.ts +116 -116
- package/dist/semantic-consolidation.js +13 -13
- package/dist/semantic-rule-promotion.js +12 -12
- package/dist/semantic-rule-verifier.js +12 -12
- package/dist/{service-PT2_z7xm.d.ts → service-CQGEqL1g.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +14 -14
- package/dist/{storage-BmVudhk4.d.ts → storage-DmkCjo0l.d.ts} +36 -32
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +14 -14
- package/dist/structured-attributes.d.ts +3 -1
- package/dist/structured-attributes.js +3 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +12 -12
- package/dist/write-envelope.js +2 -2
- package/package.json +2 -2
- package/src/access-offline-manifest.ts +1 -0
- package/src/access-service-offline-file-content.test.ts +1 -0
- package/src/content-hash.ts +34 -1
- package/src/dedup/index.test.ts +56 -38
- package/src/dedup/index.ts +85 -38
- package/src/lifecycle/tombstones.test.ts +763 -16
- package/src/lifecycle/tombstones.ts +217 -50
- package/src/reconcile/manifest.test.ts +158 -1
- package/src/reconcile/manifest.ts +76 -22
- package/src/storage/content-hash-index.ts +56 -40
- package/src/storage/entity-ref-repair.ts +3 -3
- package/src/storage/memory-read-store.ts +9 -0
- package/src/storage/tombstone-blocked-capture-index.ts +4 -9
- package/src/storage-contract/content-hash.test.ts +13 -0
- package/src/storage-fact-hash-index-defer.test.ts +1 -1
- package/src/storage.ts +107 -93
- package/src/structured-attributes.ts +11 -0
- package/dist/chunk-BQIFZ2MP.js.map +0 -1
- package/dist/chunk-DLG5V6ND.js.map +0 -1
- package/dist/chunk-JJRVPWFY.js.map +0 -1
- package/dist/chunk-R3DJVOSM.js.map +0 -1
- package/dist/chunk-VHFXP27N.js.map +0 -1
- package/dist/chunk-W5H6ZRPT.js +0 -14
- package/dist/chunk-W5H6ZRPT.js.map +0 -1
- package/dist/chunk-WD5B5AIE.js.map +0 -1
- /package/dist/{auto-sync-QFQGOEO2.js.map → auto-sync-JXEW7444.js.map} +0 -0
- /package/dist/{chunk-MLXZD4WM.js.map → chunk-2GFK22OZ.js.map} +0 -0
- /package/dist/{chunk-D5YOZ66N.js.map → chunk-3DWTDC54.js.map} +0 -0
- /package/dist/{chunk-RANKJI5T.js.map → chunk-3JLPGJ43.js.map} +0 -0
- /package/dist/{chunk-GXSOUIA4.js.map → chunk-5WQMORU5.js.map} +0 -0
- /package/dist/{chunk-J272WKX2.js.map → chunk-6HPHEFXT.js.map} +0 -0
- /package/dist/{chunk-B6LZ4IBH.js.map → chunk-6LAPLZOS.js.map} +0 -0
- /package/dist/{chunk-VXCNQYTC.js.map → chunk-APPFNGX6.js.map} +0 -0
- /package/dist/{chunk-GXFSQ6SD.js.map → chunk-C4ZNGVK3.js.map} +0 -0
- /package/dist/{chunk-JSEJH2YQ.js.map → chunk-CGVHPCP2.js.map} +0 -0
- /package/dist/{chunk-TEMSWHQD.js.map → chunk-DZD576ZU.js.map} +0 -0
- /package/dist/{chunk-AMTEYNYK.js.map → chunk-EVWWT3KE.js.map} +0 -0
- /package/dist/{chunk-XAPMEU3K.js.map → chunk-FHFERRYE.js.map} +0 -0
- /package/dist/{chunk-AXIDQG5A.js.map → chunk-G4T7L557.js.map} +0 -0
- /package/dist/{chunk-7M6NYQWB.js.map → chunk-G7Z6EENO.js.map} +0 -0
- /package/dist/{chunk-YXKVLYXU.js.map → chunk-HWRD23DW.js.map} +0 -0
- /package/dist/{chunk-SOEN6KCF.js.map → chunk-IBKR2TFR.js.map} +0 -0
- /package/dist/{chunk-F4EU6VUC.js.map → chunk-JDZPNCMA.js.map} +0 -0
- /package/dist/{chunk-BAKXNQ3C.js.map → chunk-KRTY3ENI.js.map} +0 -0
- /package/dist/{chunk-NK5RGJ65.js.map → chunk-L4DLLKPN.js.map} +0 -0
- /package/dist/{chunk-VFSAWRMZ.js.map → chunk-LJS7KUPV.js.map} +0 -0
- /package/dist/{chunk-NOEMGE56.js.map → chunk-NH5IR2WM.js.map} +0 -0
- /package/dist/{chunk-5V6IXHPI.js.map → chunk-NSVDUN76.js.map} +0 -0
- /package/dist/{chunk-IARXXZU4.js.map → chunk-P7XI2AQ3.js.map} +0 -0
- /package/dist/{chunk-W7Y5JDVB.js.map → chunk-PDDTI7IC.js.map} +0 -0
- /package/dist/{chunk-QINJITDS.js.map → chunk-PX2LSHYM.js.map} +0 -0
- /package/dist/{chunk-O5FFBO2M.js.map → chunk-QAVH6X2A.js.map} +0 -0
- /package/dist/{chunk-HDIUYMSB.js.map → chunk-QWGE2PUH.js.map} +0 -0
- /package/dist/{chunk-X57M2Q4I.js.map → chunk-RDJRFDJY.js.map} +0 -0
- /package/dist/{chunk-RYLEDKQ2.js.map → chunk-RUMNY5DT.js.map} +0 -0
- /package/dist/{chunk-BKIFTXHR.js.map → chunk-RZ4XGKOJ.js.map} +0 -0
- /package/dist/{chunk-66SE3BNH.js.map → chunk-SUPEYLH2.js.map} +0 -0
- /package/dist/{chunk-BF6SXNQJ.js.map → chunk-T2JHZWH6.js.map} +0 -0
- /package/dist/{chunk-QPOHMO6M.js.map → chunk-UES4BHA2.js.map} +0 -0
- /package/dist/{chunk-X7XEDOSL.js.map → chunk-VLL43QEQ.js.map} +0 -0
- /package/dist/{chunk-QBSFOKY6.js.map → chunk-VOELBUS4.js.map} +0 -0
- /package/dist/{chunk-2LY6GIIE.js.map → chunk-WDBX6VER.js.map} +0 -0
- /package/dist/{chunk-BMEYMKWX.js.map → chunk-WM65LU67.js.map} +0 -0
- /package/dist/{chunk-WLRD2TXF.js.map → chunk-WOPA3SGI.js.map} +0 -0
- /package/dist/{chunk-5UZ6UCOZ.js.map → chunk-XUAMCLY4.js.map} +0 -0
- /package/dist/{chunk-7HXZJAID.js.map → chunk-YGLY6QFV.js.map} +0 -0
- /package/dist/{chunk-2OIQDTDC.js.map → chunk-ZPJ4QSGP.js.map} +0 -0
- /package/dist/{chunk-3OY7KXHM.js.map → chunk-ZYLC7NFO.js.map} +0 -0
- /package/dist/{resolution-MDPNOEG6.js.map → resolution-MMOD5WC4.js.map} +0 -0
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
43
|
import { serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
44
|
+
import { computeLegacyContentHash } from "../content-hash.js";
|
|
44
45
|
|
|
45
46
|
/** Why a tombstone was emitted. */
|
|
46
47
|
export type TombstoneReason =
|
|
@@ -72,10 +73,14 @@ export interface TombstoneEntry {
|
|
|
72
73
|
reason: TombstoneReason;
|
|
73
74
|
/** The memory that was retired. */
|
|
74
75
|
sourceMemoryId: string;
|
|
75
|
-
/** sha256 of the retired
|
|
76
|
+
/** sha256 of the retired identity; explicit contentHashSource may define it. */
|
|
76
77
|
contentHash: string;
|
|
77
|
-
/**
|
|
78
|
+
/** Current body hash retained alongside an ambiguous or explicit primary hash. */
|
|
79
|
+
currentContentHashAlias?: string;
|
|
80
|
+
/** Current normalized body used by the normalized lookup tier. */
|
|
78
81
|
normalizedText: string;
|
|
82
|
+
/** Version of the current body identity fields. */
|
|
83
|
+
normalizerVersion?: number;
|
|
79
84
|
entityRef?: string;
|
|
80
85
|
/** Structured-attribute supersession key when one existed. */
|
|
81
86
|
supersessionKey?: string;
|
|
@@ -111,7 +116,7 @@ export interface TombstoneLookupQuery {
|
|
|
111
116
|
/** Single supersession key (direct/unit callers). */
|
|
112
117
|
supersessionKey?: string;
|
|
113
118
|
/** All derived supersession keys (write chokepoint). The keyed tier is
|
|
114
|
-
*
|
|
119
|
+
* checked for each; the first active match wins. */
|
|
115
120
|
supersessionKeys?: string[];
|
|
116
121
|
namespace: string;
|
|
117
122
|
}
|
|
@@ -131,6 +136,15 @@ export interface TombstoneStoreOptions {
|
|
|
131
136
|
* skipped entirely.
|
|
132
137
|
*/
|
|
133
138
|
semanticSimilarity?: (a: string, b: string) => number;
|
|
139
|
+
/**
|
|
140
|
+
* Resolve source identity candidates for bounded legacy sourceMemoryIds.
|
|
141
|
+
* StorageManager reads the corpus before the tombstone write lock.
|
|
142
|
+
*/
|
|
143
|
+
readonly sourceContentsForMemoryIds?: (
|
|
144
|
+
sourceMemoryIds: readonly string[],
|
|
145
|
+
) => Promise<ReadonlyMap<string, string | readonly string[]>>;
|
|
146
|
+
/** Maximum number of legacy entries considered during one load. */
|
|
147
|
+
readonly legacyMigrationLimit?: number;
|
|
134
148
|
/**
|
|
135
149
|
* Cross-process write-lock timings for the tombstone JSONL mutation lock
|
|
136
150
|
* (issue #1639). The secure-store append is a read-merge-write (read
|
|
@@ -168,6 +182,9 @@ export interface TombstoneStats {
|
|
|
168
182
|
loaded: boolean;
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
const TOMBSTONE_NORMALIZER_VERSION = 2;
|
|
186
|
+
const DEFAULT_LEGACY_MIGRATION_LIMIT = 10_000;
|
|
187
|
+
|
|
171
188
|
const TOMBSTONE_PREFIX = "tomb";
|
|
172
189
|
|
|
173
190
|
function newTombstoneId(): string {
|
|
@@ -235,6 +252,12 @@ export function parseTombstoneLine(line: string): TombstoneEntry | null {
|
|
|
235
252
|
createdAt: e.createdAt,
|
|
236
253
|
createdBy: e.createdBy,
|
|
237
254
|
};
|
|
255
|
+
if (typeof e.currentContentHashAlias === "string") {
|
|
256
|
+
out.currentContentHashAlias = e.currentContentHashAlias;
|
|
257
|
+
}
|
|
258
|
+
if (typeof e.normalizerVersion === "number" && Number.isInteger(e.normalizerVersion)) {
|
|
259
|
+
out.normalizerVersion = e.normalizerVersion;
|
|
260
|
+
}
|
|
238
261
|
if (typeof e.operationKey === "string") out.operationKey = e.operationKey;
|
|
239
262
|
if (typeof e.entityRef === "string") out.entityRef = e.entityRef;
|
|
240
263
|
if (typeof e.supersessionKey === "string") out.supersessionKey = e.supersessionKey;
|
|
@@ -279,6 +302,10 @@ export class TombstoneStore {
|
|
|
279
302
|
private readonly options: TombstoneStoreOptions,
|
|
280
303
|
private readonly io: TombstoneFileIo,
|
|
281
304
|
) {
|
|
305
|
+
const limit = this.options.legacyMigrationLimit;
|
|
306
|
+
if (limit !== undefined && (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 0)) {
|
|
307
|
+
throw new Error("legacyMigrationLimit must be a finite non-negative integer");
|
|
308
|
+
}
|
|
282
309
|
this.lockStaleMs = this.options.lockStaleMs ?? 30_000;
|
|
283
310
|
this.lockMaxWaitMs = this.options.lockMaxWaitMs ?? 5_000;
|
|
284
311
|
this.lockHeartbeatMs =
|
|
@@ -318,39 +345,132 @@ export class TombstoneStore {
|
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
347
|
|
|
321
|
-
private async loadInternal(): Promise<void> {
|
|
322
|
-
// Record the file
|
|
323
|
-
//
|
|
324
|
-
// any non-zero mtime would trigger a spurious reload). Done before the
|
|
325
|
-
// read so an ENOENT still records 0.
|
|
348
|
+
private async loadInternal(migrateLegacy = true): Promise<void> {
|
|
349
|
+
// Record the file mtime before reading so an ENOENT still records 0 and
|
|
350
|
+
// a successful load does not immediately trigger a staleness reload.
|
|
326
351
|
this.recordFileMtime();
|
|
327
352
|
let raw: string;
|
|
328
353
|
try {
|
|
329
354
|
raw = await this.io.read(this.filePath);
|
|
330
355
|
} catch (err) {
|
|
331
|
-
// ENOENT is fine — fresh store. Other errors leave the store empty
|
|
332
|
-
// rather than crashing the write path (rule 34 spirit: degrade to
|
|
333
|
-
// "no tombstones known" rather than blocking all writes).
|
|
334
356
|
const code = (err as NodeJS.ErrnoException)?.code;
|
|
335
|
-
if (code !== "ENOENT")
|
|
336
|
-
// Swallow — the write path must not crash on a corrupt tombstone file.
|
|
337
|
-
// Rebuild will repair on the next doctor/maintenance run.
|
|
338
|
-
}
|
|
357
|
+
if (code !== "ENOENT") throw err;
|
|
339
358
|
this.loaded = true;
|
|
340
359
|
return;
|
|
341
360
|
}
|
|
361
|
+
const initial = this.parseEntries(raw);
|
|
362
|
+
const migrated = migrateLegacy
|
|
363
|
+
? await this.migrateLegacyEntries(initial)
|
|
364
|
+
: initial;
|
|
342
365
|
this.resetIndex();
|
|
343
|
-
|
|
366
|
+
for (const entry of migrated.entries) this.indexEntry(entry);
|
|
367
|
+
this.corruptedLines = migrated.corruptedLines;
|
|
368
|
+
this.loaded = true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private parseEntries(raw: string): {
|
|
372
|
+
entries: TombstoneEntry[];
|
|
373
|
+
corruptedLines: number;
|
|
374
|
+
} {
|
|
375
|
+
const entries: TombstoneEntry[] = [];
|
|
376
|
+
let corruptedLines = 0;
|
|
344
377
|
for (const line of raw.split("\n")) {
|
|
345
378
|
const entry = parseTombstoneLine(line);
|
|
346
379
|
if (!entry) {
|
|
347
|
-
if (line.trim().length > 0)
|
|
380
|
+
if (line.trim().length > 0) corruptedLines += 1;
|
|
348
381
|
continue;
|
|
349
382
|
}
|
|
350
|
-
|
|
383
|
+
entries.push(entry);
|
|
351
384
|
}
|
|
352
|
-
|
|
353
|
-
|
|
385
|
+
return { entries, corruptedLines };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Re-index pre-Unicode records from retired source content.
|
|
390
|
+
* Source reads stay bounded, but every batch finishes before the store
|
|
391
|
+
* publishes its in-memory index. Missing sources remain eligible on restart.
|
|
392
|
+
*/
|
|
393
|
+
private async migrateLegacyEntries(initial: {
|
|
394
|
+
entries: TombstoneEntry[];
|
|
395
|
+
corruptedLines: number;
|
|
396
|
+
}): Promise<{ entries: TombstoneEntry[]; corruptedLines: number }> {
|
|
397
|
+
const limit = this.options.legacyMigrationLimit ?? DEFAULT_LEGACY_MIGRATION_LIMIT;
|
|
398
|
+
if (limit === 0 || !this.options.sourceContentsForMemoryIds) return initial;
|
|
399
|
+
const sourceMemoryIds: string[] = [];
|
|
400
|
+
const requested = new Set<string>();
|
|
401
|
+
for (const entry of initial.entries) {
|
|
402
|
+
if (
|
|
403
|
+
entry.kind !== "tombstone" ||
|
|
404
|
+
entry.normalizerVersion === TOMBSTONE_NORMALIZER_VERSION
|
|
405
|
+
) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (requested.has(entry.sourceMemoryId)) continue;
|
|
409
|
+
requested.add(entry.sourceMemoryId);
|
|
410
|
+
sourceMemoryIds.push(entry.sourceMemoryId);
|
|
411
|
+
}
|
|
412
|
+
if (sourceMemoryIds.length === 0) return initial;
|
|
413
|
+
const sourceContents = new Map<string, string | readonly string[]>();
|
|
414
|
+
for (let offset = 0; offset < sourceMemoryIds.length; offset += limit) {
|
|
415
|
+
const batch = sourceMemoryIds.slice(offset, offset + limit);
|
|
416
|
+
const fetched = await this.options.sourceContentsForMemoryIds(batch);
|
|
417
|
+
for (const [sourceMemoryId, content] of fetched) sourceContents.set(sourceMemoryId, content);
|
|
418
|
+
}
|
|
419
|
+
return await serializeMutations(`tombstone:${this.filePath}`, () =>
|
|
420
|
+
this.withWriteLock(async () => {
|
|
421
|
+
const latestRaw = await this.io.read(this.filePath);
|
|
422
|
+
const latest = this.parseEntries(latestRaw);
|
|
423
|
+
let changed = false;
|
|
424
|
+
const migrateEntry = (entry: TombstoneEntry): TombstoneEntry => {
|
|
425
|
+
if (
|
|
426
|
+
entry.kind !== "tombstone" ||
|
|
427
|
+
entry.normalizerVersion === TOMBSTONE_NORMALIZER_VERSION ||
|
|
428
|
+
!requested.has(entry.sourceMemoryId)
|
|
429
|
+
) {
|
|
430
|
+
return entry;
|
|
431
|
+
}
|
|
432
|
+
const resolved = sourceContents.get(entry.sourceMemoryId);
|
|
433
|
+
if (resolved === undefined) return entry;
|
|
434
|
+
const candidates = typeof resolved === "string" ? [resolved] : resolved;
|
|
435
|
+
const matches = candidates.filter((source) =>
|
|
436
|
+
entry.contentHash === this.options.hashContent(source) ||
|
|
437
|
+
entry.contentHash === computeLegacyContentHash(source)
|
|
438
|
+
);
|
|
439
|
+
const explicitPrimary =
|
|
440
|
+
matches.length === 0 &&
|
|
441
|
+
entry.contentHash !== this.options.hashContent(entry.normalizedText);
|
|
442
|
+
const source = matches[0] ?? (explicitPrimary ? candidates[0] : undefined);
|
|
443
|
+
if (source === undefined) return entry;
|
|
444
|
+
const currentHash = this.options.hashContent(source);
|
|
445
|
+
const alias = explicitPrimary
|
|
446
|
+
? currentHash
|
|
447
|
+
: matches
|
|
448
|
+
.map((candidate) => this.options.hashContent(candidate))
|
|
449
|
+
.find((hash) => hash !== currentHash);
|
|
450
|
+
changed = true;
|
|
451
|
+
return {
|
|
452
|
+
...entry,
|
|
453
|
+
contentHash: explicitPrimary ? entry.contentHash : currentHash,
|
|
454
|
+
...(alias ? { currentContentHashAlias: alias } : {}),
|
|
455
|
+
normalizedText: this.options.normalizeText(source),
|
|
456
|
+
normalizerVersion: TOMBSTONE_NORMALIZER_VERSION,
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const serialized = latestRaw
|
|
460
|
+
.split("\n")
|
|
461
|
+
.map((line) => {
|
|
462
|
+
const entry = parseTombstoneLine(line);
|
|
463
|
+
if (!entry) return line;
|
|
464
|
+
const migrated = migrateEntry(entry);
|
|
465
|
+
return migrated === entry ? line : JSON.stringify(migrated);
|
|
466
|
+
})
|
|
467
|
+
.join("\n");
|
|
468
|
+
if (!changed) return latest;
|
|
469
|
+
await this.io.write(this.filePath, serialized);
|
|
470
|
+
this.markWritten();
|
|
471
|
+
return this.parseEntries(serialized);
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
354
474
|
}
|
|
355
475
|
|
|
356
476
|
private resetIndex(): void {
|
|
@@ -384,8 +504,13 @@ export class TombstoneStore {
|
|
|
384
504
|
// entry — otherwise A's lookup finds B's id, rejects it on namespace
|
|
385
505
|
// mismatch, and misses its own still-active tombstone (resurrection).
|
|
386
506
|
const ns = entry.namespace;
|
|
387
|
-
if (entry.
|
|
388
|
-
|
|
507
|
+
if (entry.normalizerVersion === TOMBSTONE_NORMALIZER_VERSION) {
|
|
508
|
+
if (entry.contentHash) this.byHash.set(`${ns}\0${entry.contentHash}`, entry.id);
|
|
509
|
+
if (entry.currentContentHashAlias) {
|
|
510
|
+
this.byHash.set(`${ns}\0${entry.currentContentHashAlias}`, entry.id);
|
|
511
|
+
}
|
|
512
|
+
if (entry.normalizedText) this.byNormalized.set(`${ns}\0${entry.normalizedText}`, entry.id);
|
|
513
|
+
}
|
|
389
514
|
if (entry.entityRef && entry.supersessionKey) {
|
|
390
515
|
this.byKey.set(keyedTierKey(ns, entry.entityRef, entry.supersessionKey), entry.id);
|
|
391
516
|
}
|
|
@@ -400,15 +525,11 @@ export class TombstoneStore {
|
|
|
400
525
|
|
|
401
526
|
/**
|
|
402
527
|
* Cross-process staleness probe (#1579). If the file's mtime advanced since
|
|
403
|
-
* the last load/own-write, a peer process appended and our in-memory index
|
|
404
|
-
* stale — invalidate
|
|
405
|
-
*
|
|
406
|
-
* retained rather than crashing the write path). No-op when no `stat` was
|
|
407
|
-
* injected or when the mtime is unchanged. After our own append/revoke/rebuild
|
|
408
|
-
* `markWritten` records the new mtime, so this probe does not fire for our
|
|
409
|
-
* own writes (only for peer-process appends).
|
|
528
|
+
* the last load/own-write, a peer process appended and our in-memory index
|
|
529
|
+
* is stale — invalidate and reload before the next access. Mutation callers
|
|
530
|
+
* pass `migrate: false` to avoid nested serializer/lock acquisition.
|
|
410
531
|
*/
|
|
411
|
-
async ensureFreshAgainstDisk(): Promise<void> {
|
|
532
|
+
async ensureFreshAgainstDisk(options: { migrate?: boolean } = {}): Promise<void> {
|
|
412
533
|
if (!this.io.stat) return;
|
|
413
534
|
let mtimeMs: number;
|
|
414
535
|
try {
|
|
@@ -419,7 +540,11 @@ export class TombstoneStore {
|
|
|
419
540
|
if (mtimeMs === this.fileMtimeMs && this.loaded) return;
|
|
420
541
|
this.fileMtimeMs = mtimeMs;
|
|
421
542
|
this.invalidate();
|
|
422
|
-
|
|
543
|
+
if (options.migrate === false) {
|
|
544
|
+
await this.loadInternal(false);
|
|
545
|
+
} else {
|
|
546
|
+
await this.load();
|
|
547
|
+
}
|
|
423
548
|
}
|
|
424
549
|
|
|
425
550
|
/**
|
|
@@ -470,13 +595,19 @@ export class TombstoneStore {
|
|
|
470
595
|
await this.load();
|
|
471
596
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
472
597
|
const id = newTombstoneId();
|
|
598
|
+
const currentHash = this.options.hashContent(input.rawContent);
|
|
599
|
+
const currentNormalizedText = this.options.normalizeText(input.rawContent);
|
|
473
600
|
const entry: TombstoneEntry = {
|
|
474
601
|
id,
|
|
602
|
+
...(input.contentHash && input.contentHash !== currentHash
|
|
603
|
+
? { currentContentHashAlias: currentHash }
|
|
604
|
+
: {}),
|
|
475
605
|
kind: "tombstone",
|
|
476
606
|
reason: input.reason,
|
|
477
607
|
sourceMemoryId: input.sourceMemoryId,
|
|
478
|
-
contentHash: input.contentHash ??
|
|
479
|
-
normalizedText:
|
|
608
|
+
contentHash: input.contentHash ?? currentHash,
|
|
609
|
+
normalizedText: currentNormalizedText,
|
|
610
|
+
normalizerVersion: TOMBSTONE_NORMALIZER_VERSION,
|
|
480
611
|
...(input.entityRef ? { entityRef: input.entityRef } : {}),
|
|
481
612
|
...(input.supersessionKey ? { supersessionKey: input.supersessionKey } : {}),
|
|
482
613
|
...(input.operationKey ? { operationKey: input.operationKey } : {}),
|
|
@@ -487,6 +618,7 @@ export class TombstoneStore {
|
|
|
487
618
|
await this.serializeAppend(entry);
|
|
488
619
|
this.indexEntry(entry);
|
|
489
620
|
this.markWritten();
|
|
621
|
+
await this.migrateLoadedLegacyEntries();
|
|
490
622
|
return id;
|
|
491
623
|
}
|
|
492
624
|
|
|
@@ -513,6 +645,7 @@ export class TombstoneStore {
|
|
|
513
645
|
await this.serializeAppend(entry);
|
|
514
646
|
this.indexEntry(entry);
|
|
515
647
|
this.markWritten();
|
|
648
|
+
await this.migrateLoadedLegacyEntries();
|
|
516
649
|
return id;
|
|
517
650
|
}
|
|
518
651
|
|
|
@@ -526,12 +659,33 @@ export class TombstoneStore {
|
|
|
526
659
|
// disk first, so indexEntry builds on the peer's just-appended entries.
|
|
527
660
|
return serializeMutations(`tombstone:${this.filePath}`, () =>
|
|
528
661
|
this.withWriteLock(async () => {
|
|
529
|
-
await this.ensureFreshAgainstDisk();
|
|
662
|
+
await this.ensureFreshAgainstDisk({ migrate: false });
|
|
530
663
|
await this.io.append(this.filePath, line);
|
|
531
664
|
}),
|
|
532
665
|
);
|
|
533
666
|
}
|
|
534
667
|
|
|
668
|
+
private async migrateLoadedLegacyEntries(): Promise<void> {
|
|
669
|
+
const initial = {
|
|
670
|
+
entries: [...this.entries],
|
|
671
|
+
corruptedLines: this.corruptedLines,
|
|
672
|
+
};
|
|
673
|
+
if (!initial.entries.some(
|
|
674
|
+
(entry) => entry.kind === "tombstone" && entry.normalizerVersion !== TOMBSTONE_NORMALIZER_VERSION,
|
|
675
|
+
)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const migrated = await this.migrateLegacyEntries(initial);
|
|
680
|
+
this.resetIndex();
|
|
681
|
+
for (const entry of migrated.entries) this.indexEntry(entry);
|
|
682
|
+
this.corruptedLines = migrated.corruptedLines;
|
|
683
|
+
this.loaded = true;
|
|
684
|
+
} catch {
|
|
685
|
+
// The durable mutation already succeeded; defer migration to next load.
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
535
689
|
/**
|
|
536
690
|
* Acquire the cross-process tombstone write lock and run `task` under it
|
|
537
691
|
* (issue #1639). Strict-fail on acquisition timeout: the secure-store append
|
|
@@ -635,6 +789,7 @@ export class TombstoneStore {
|
|
|
635
789
|
let best: { id: string; reason: TombstoneReason; score: number } | null = null;
|
|
636
790
|
for (const entry of this.entries) {
|
|
637
791
|
if (entry.kind !== "tombstone") continue;
|
|
792
|
+
if (entry.normalizerVersion !== TOMBSTONE_NORMALIZER_VERSION) continue;
|
|
638
793
|
if (entry.namespace !== query.namespace) continue;
|
|
639
794
|
if (this.revokedIds.has(entry.id)) continue;
|
|
640
795
|
if (!entry.normalizedText) continue;
|
|
@@ -714,7 +869,7 @@ export class TombstoneStore {
|
|
|
714
869
|
// interleave in-process either.
|
|
715
870
|
const rebuiltCount = await serializeMutations(`tombstone:${this.filePath}`, () =>
|
|
716
871
|
this.withWriteLock(async () => {
|
|
717
|
-
await this.ensureFreshAgainstDisk();
|
|
872
|
+
await this.ensureFreshAgainstDisk({ migrate: false });
|
|
718
873
|
// Preserve existing revocations (all namespaces — ids are globally
|
|
719
874
|
// unique) so a rebuild does not silently un-revoke.
|
|
720
875
|
const existingRevocations = this.entries.filter((e) => e.kind === "revocation");
|
|
@@ -737,21 +892,32 @@ export class TombstoneStore {
|
|
|
737
892
|
existingBySource.set(`${e.sourceMemoryId}\u{0000}${e.supersessionKey ?? ""}`, e.id);
|
|
738
893
|
}
|
|
739
894
|
}
|
|
740
|
-
const rebuilt: TombstoneEntry[] = retiredMemories.map((m) =>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
895
|
+
const rebuilt: TombstoneEntry[] = retiredMemories.map((m) => {
|
|
896
|
+
const currentHash = this.options.hashContent(m.rawContent);
|
|
897
|
+
const currentNormalizedText = this.options.normalizeText(m.rawContent);
|
|
898
|
+
const persistedHash = m.contentHash;
|
|
899
|
+
const preserveOverride =
|
|
900
|
+
persistedHash !== undefined &&
|
|
901
|
+
persistedHash !== currentHash &&
|
|
902
|
+
persistedHash !== computeLegacyContentHash(m.rawContent);
|
|
903
|
+
return {
|
|
904
|
+
id:
|
|
905
|
+
existingBySource.get(`${m.memoryId}\u{0000}${m.supersessionKey ?? ""}`) ??
|
|
906
|
+
newTombstoneId(),
|
|
907
|
+
...(preserveOverride ? { currentContentHashAlias: currentHash } : {}),
|
|
908
|
+
kind: "tombstone" as const,
|
|
909
|
+
reason: m.reason,
|
|
910
|
+
sourceMemoryId: m.memoryId,
|
|
911
|
+
contentHash: preserveOverride ? persistedHash : currentHash,
|
|
912
|
+
normalizedText: currentNormalizedText,
|
|
913
|
+
normalizerVersion: TOMBSTONE_NORMALIZER_VERSION,
|
|
914
|
+
...(m.entityRef ? { entityRef: m.entityRef } : {}),
|
|
915
|
+
...(m.supersessionKey ? { supersessionKey: m.supersessionKey } : {}),
|
|
916
|
+
namespace: this.namespace,
|
|
917
|
+
createdAt: m.createdAt,
|
|
918
|
+
createdBy: m.createdBy,
|
|
919
|
+
};
|
|
920
|
+
});
|
|
755
921
|
// Sort deterministically (rule 38): createdAt, then id for stability.
|
|
756
922
|
rebuilt.sort((a, b) =>
|
|
757
923
|
a.createdAt === b.createdAt
|
|
@@ -777,6 +943,7 @@ export class TombstoneStore {
|
|
|
777
943
|
return rebuilt.length;
|
|
778
944
|
}),
|
|
779
945
|
);
|
|
946
|
+
await this.migrateLoadedLegacyEntries();
|
|
780
947
|
return rebuiltCount;
|
|
781
948
|
}
|
|
782
949
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as assert from "node:assert/strict";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import {
|
|
4
|
+
import { computeLegacyContentHash } from "../content-hash.js";
|
|
5
|
+
import { attachCitation, formatCitation } from "../source-attribution.js";
|
|
5
6
|
import { parseFrontmatter } from "../storage.js";
|
|
7
|
+
import { ContentHashIndex } from "../storage/content-hash-index.js";
|
|
6
8
|
import { type ReconcileManifest, buildReconcileManifest, collapseActiveFactDuplicates } from "./manifest.js";
|
|
7
9
|
import { planReconciliation } from "./plan.js";
|
|
8
10
|
|
|
@@ -47,6 +49,7 @@ test("reconcile manifest keeps file identity separate from canonical semantic id
|
|
|
47
49
|
id: "fact-a",
|
|
48
50
|
category: "fact",
|
|
49
51
|
contentHash: semanticHash,
|
|
52
|
+
normalizerVersion: 4,
|
|
50
53
|
status: "active",
|
|
51
54
|
});
|
|
52
55
|
});
|
|
@@ -71,6 +74,125 @@ test("reconcile manifest hashes parsed legacy content and uses effective lifecyc
|
|
|
71
74
|
assert.equal(manifest.files[1]?.memory?.status, "active");
|
|
72
75
|
});
|
|
73
76
|
|
|
77
|
+
test("reconcile manifest reparses cached pre-version identities after Unicode migration", async () => {
|
|
78
|
+
const content = "The user prefers café.";
|
|
79
|
+
const legacyHash = computeLegacyContentHash(content);
|
|
80
|
+
const serialized = memoryFile({ id: "legacy-unicode", content, contentHash: legacyHash });
|
|
81
|
+
const file = { path: "facts/legacy-unicode.md", sha256: fileHash(serialized) };
|
|
82
|
+
let reads = 0;
|
|
83
|
+
|
|
84
|
+
const manifest = await buildReconcileManifest({
|
|
85
|
+
files: [file],
|
|
86
|
+
parseMemory: parseFrontmatter,
|
|
87
|
+
readFile: async () => {
|
|
88
|
+
reads += 1;
|
|
89
|
+
return serialized;
|
|
90
|
+
},
|
|
91
|
+
cachedFiles: [{
|
|
92
|
+
...file,
|
|
93
|
+
memory: {
|
|
94
|
+
id: "legacy-unicode",
|
|
95
|
+
category: "fact",
|
|
96
|
+
contentHash: legacyHash,
|
|
97
|
+
status: "active",
|
|
98
|
+
},
|
|
99
|
+
}],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(reads, 1);
|
|
103
|
+
assert.equal(
|
|
104
|
+
manifest.files[0]?.memory?.contentHash,
|
|
105
|
+
ContentHashIndex.computeHash(content),
|
|
106
|
+
);
|
|
107
|
+
assert.equal("contentHashAliases" in (manifest.files[0]?.memory ?? {}), false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("reconcile manifest replaces a pure-CJK legacy identity with its current hash", async () => {
|
|
111
|
+
const content = "利用者は紅茶を好む。";
|
|
112
|
+
const serialized = memoryFile({
|
|
113
|
+
id: "legacy-cjk",
|
|
114
|
+
content,
|
|
115
|
+
contentHash: computeLegacyContentHash(content),
|
|
116
|
+
});
|
|
117
|
+
const manifest = await buildReconcileManifest({
|
|
118
|
+
files: [{ path: "facts/legacy-cjk.md", sha256: fileHash(serialized) }],
|
|
119
|
+
parseMemory: parseFrontmatter,
|
|
120
|
+
readFile: async () => serialized,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.equal(
|
|
124
|
+
manifest.files[0]?.memory?.contentHash,
|
|
125
|
+
ContentHashIndex.computeHash(content),
|
|
126
|
+
);
|
|
127
|
+
assert.equal("contentHashAliases" in (manifest.files[0]?.memory ?? {}), false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("reconcile manifest recovers a raw hash source beneath citation and attributes", async () => {
|
|
131
|
+
const content = "The user prefers café.";
|
|
132
|
+
const cited = attachCitation(content, {
|
|
133
|
+
agent: "planner",
|
|
134
|
+
session: "agent:planner:main",
|
|
135
|
+
ts: "2026-08-11T00:00:00.000Z",
|
|
136
|
+
});
|
|
137
|
+
const storedBody = `${cited}\n[Attributes: topic: coffee]`;
|
|
138
|
+
const serialized = memoryFile({
|
|
139
|
+
id: "legacy-enriched",
|
|
140
|
+
content: storedBody,
|
|
141
|
+
contentHash: computeLegacyContentHash(content),
|
|
142
|
+
});
|
|
143
|
+
const manifest = await buildReconcileManifest({
|
|
144
|
+
files: [{ path: "facts/legacy-enriched.md", sha256: fileHash(serialized) }],
|
|
145
|
+
parseMemory: parseFrontmatter,
|
|
146
|
+
readFile: async () => serialized,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.equal(manifest.files[0]?.memory?.contentHash, ContentHashIndex.computeHash(content));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("reconcile manifest recovers raw identity beneath a configured citation", async () => {
|
|
153
|
+
const content = "The user prefers café.";
|
|
154
|
+
const citationTemplate = "[src:{agent}/{sessionId}@{date}]";
|
|
155
|
+
const storedBody = `${content} ${formatCitation({
|
|
156
|
+
agent: "planner",
|
|
157
|
+
session: "agent:planner:main",
|
|
158
|
+
ts: "2026-08-11T00:00:00.000Z",
|
|
159
|
+
}, citationTemplate)}`;
|
|
160
|
+
const serialized = memoryFile({
|
|
161
|
+
id: "legacy-custom-citation",
|
|
162
|
+
content: storedBody,
|
|
163
|
+
contentHash: computeLegacyContentHash(content),
|
|
164
|
+
});
|
|
165
|
+
const manifest = await buildReconcileManifest({
|
|
166
|
+
files: [{ path: "facts/legacy-custom-citation.md", sha256: fileHash(serialized) }],
|
|
167
|
+
parseMemory: parseFrontmatter,
|
|
168
|
+
readFile: async () => serialized,
|
|
169
|
+
citationTemplate,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
assert.equal(manifest.files[0]?.memory?.contentHash, ContentHashIndex.computeHash(content));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("reconcile manifest does not add a legacy alias to a current Unicode identity", async () => {
|
|
176
|
+
const unicode = "The user prefers café.";
|
|
177
|
+
const ascii = "The user prefers caf.";
|
|
178
|
+
const serialized = memoryFile({
|
|
179
|
+
id: "current-unicode",
|
|
180
|
+
content: unicode,
|
|
181
|
+
contentHash: ContentHashIndex.computeHash(unicode),
|
|
182
|
+
});
|
|
183
|
+
const manifest = await buildReconcileManifest({
|
|
184
|
+
files: [{ path: "facts/current-unicode.md", sha256: fileHash(serialized) }],
|
|
185
|
+
parseMemory: parseFrontmatter,
|
|
186
|
+
readFile: async () => serialized,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
assert.notEqual(
|
|
190
|
+
manifest.files[0]?.memory?.contentHash,
|
|
191
|
+
ContentHashIndex.computeHash(ascii),
|
|
192
|
+
);
|
|
193
|
+
assert.equal("contentHashAliases" in (manifest.files[0]?.memory ?? {}), false);
|
|
194
|
+
});
|
|
195
|
+
|
|
74
196
|
test("reconcile manifest keeps file identity when memory bytes cannot be trusted", async () => {
|
|
75
197
|
const readable = memoryFile({ id: "readable", content: "Readable fact" });
|
|
76
198
|
const changed = memoryFile({ id: "changed", content: "Changed after census" });
|
|
@@ -148,6 +270,41 @@ test("semantic collapse prevents different active fact paths from transferring b
|
|
|
148
270
|
assert.equal(collapsed.byNamespace[0]?.push, 0);
|
|
149
271
|
});
|
|
150
272
|
|
|
273
|
+
test("semantic collapse matches legacy and current Unicode identities across replicas", async () => {
|
|
274
|
+
const content = "The user prefers café.";
|
|
275
|
+
const localSerialized = memoryFile({
|
|
276
|
+
id: "legacy-id",
|
|
277
|
+
content,
|
|
278
|
+
contentHash: computeLegacyContentHash(content),
|
|
279
|
+
});
|
|
280
|
+
const peerSerialized = memoryFile({
|
|
281
|
+
id: "current-id",
|
|
282
|
+
content,
|
|
283
|
+
contentHash: ContentHashIndex.computeHash(content),
|
|
284
|
+
});
|
|
285
|
+
const localFile = { path: "facts/legacy-id.md", sha256: fileHash(localSerialized) };
|
|
286
|
+
const peerFile = { path: "facts/current-id.md", sha256: fileHash(peerSerialized) };
|
|
287
|
+
const build = async (file: typeof localFile, raw: string) =>
|
|
288
|
+
await buildReconcileManifest({
|
|
289
|
+
files: [file],
|
|
290
|
+
parseMemory: parseFrontmatter,
|
|
291
|
+
readFile: async () => raw,
|
|
292
|
+
});
|
|
293
|
+
const local = await build(localFile, localSerialized);
|
|
294
|
+
const peer = await build(peerFile, peerSerialized);
|
|
295
|
+
const plan = planReconciliation([{ namespace: "default", local: [localFile], peer: [peerFile] }]);
|
|
296
|
+
|
|
297
|
+
const collapsed = collapseActiveFactDuplicates(
|
|
298
|
+
plan,
|
|
299
|
+
new Map([["default", local]]),
|
|
300
|
+
new Map([["default", peer]]),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
assert.equal(collapsed.converged, true);
|
|
304
|
+
assert.equal(collapsed.entries.length, 1);
|
|
305
|
+
assert.equal(collapsed.entries[0]?.reason, "semantic_duplicate");
|
|
306
|
+
});
|
|
307
|
+
|
|
151
308
|
test("semantic collapse classifies a later one-sided metadata edit from its per-side base", () => {
|
|
152
309
|
const semanticHash = ContentHashIndex.computeHash("same active fact");
|
|
153
310
|
const priorLocal = { path: "facts/local-id.md", sha256: "a".repeat(64) };
|