@remnic/core 9.3.760 → 9.3.762
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 +3 -3
- package/dist/access-admin-ops-surface.js +11 -10
- package/dist/access-boundary.d.ts +3 -3
- package/dist/access-boundary.js +12 -11
- package/dist/access-cli.js +46 -45
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +3 -3
- package/dist/access-http.js +18 -17
- package/dist/access-identity-continuity-surface.d.ts +2 -2
- package/dist/access-identity-continuity-surface.js +11 -10
- package/dist/access-lcm-surface.d.ts +3 -3
- package/dist/access-lcm-surface.js +11 -10
- package/dist/access-mcp.d.ts +3 -3
- package/dist/access-mcp.js +15 -14
- package/dist/access-observe-write-surface.d.ts +3 -3
- package/dist/access-observe-write-surface.js +11 -10
- package/dist/access-operations-batch.js +13 -12
- package/dist/access-operations.d.ts +7 -7
- package/dist/access-operations.js +14 -13
- package/dist/access-recall-surface.d.ts +3 -3
- package/dist/access-recall-surface.js +11 -10
- package/dist/access-schema.d.ts +68 -68
- package/dist/{access-service-BW-gb3DL.d.ts → access-service-CaWJP_f6.d.ts} +2 -2
- package/dist/access-service.d.ts +3 -3
- package/dist/access-service.js +11 -10
- package/dist/access-surface-catalog.d.ts +3 -3
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/bootstrap.d.ts +2 -2
- package/dist/briefing.js +4 -3
- package/dist/{catalog-DaKAFbSJ.d.ts → catalog-bflYNWsM.d.ts} +8 -4
- package/dist/causal-consolidation.js +5 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-OLEBPUZ7.js → chunk-2KYXXMTE.js} +6 -6
- package/dist/{chunk-6CUUFFU7.js → chunk-2PGB63VU.js} +131 -39
- package/dist/chunk-2PGB63VU.js.map +1 -0
- package/dist/{chunk-FYJWEGYP.js → chunk-4HYJETVO.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-W3M34XT4.js → chunk-BAVVI5GP.js} +2 -2
- package/dist/{chunk-XQT5GSWU.js → chunk-BRYPUVJU.js} +3 -3
- package/dist/{chunk-2XE4F3VZ.js → chunk-DB647OZ4.js} +24 -24
- package/dist/{chunk-3JBW3GHN.js → chunk-DPZ5GJMD.js} +2 -2
- package/dist/chunk-DQC3ZZYM.js +35 -0
- package/dist/chunk-DQC3ZZYM.js.map +1 -0
- package/dist/{chunk-SMLMH7S5.js → chunk-EDC7RZCW.js} +24 -24
- package/dist/{chunk-ZFXCQPNO.js → chunk-EGGE52UU.js} +2 -2
- package/dist/chunk-EGGE52UU.js.map +1 -0
- package/dist/{chunk-IOZZG75J.js → chunk-ES3GAMSQ.js} +2 -2
- package/dist/{chunk-QUGPXPFJ.js → chunk-ESDBHQY6.js} +5 -5
- package/dist/{chunk-ILVF6TIY.js → chunk-HDFS6RWQ.js} +2 -2
- package/dist/{chunk-SN2EWIAI.js → chunk-HDYFNXVO.js} +2 -2
- package/dist/{chunk-ZEQVS6AZ.js → chunk-HMUEAKSD.js} +2 -2
- package/dist/{chunk-P26O6R3Q.js → chunk-IIURIWJG.js} +102 -12
- package/dist/chunk-IIURIWJG.js.map +1 -0
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-QQJMVF6P.js → chunk-JU6BKZHX.js} +3 -3
- package/dist/{chunk-SLAZZXBU.js → chunk-KZWUJC7Y.js} +2 -2
- package/dist/{chunk-IZLGHKWA.js → chunk-LAJIXABF.js} +10 -10
- package/dist/{chunk-3QLPBCFG.js → chunk-NKP6II6G.js} +4 -4
- package/dist/{chunk-WIXQ3F5P.js → chunk-OQ6F3WBX.js} +2 -2
- package/dist/{chunk-YP23FQPT.js → chunk-P7W4CDX6.js} +92 -41
- package/dist/chunk-P7W4CDX6.js.map +1 -0
- package/dist/{chunk-EGQ3AVZV.js → chunk-PNFN7SIN.js} +3 -3
- package/dist/{chunk-M2AAYJVI.js → chunk-QPFO54VC.js} +4 -4
- package/dist/{chunk-7HWOEYHW.js → chunk-QWIE43BS.js} +2 -2
- package/dist/chunk-TEKTCF2V.js +118 -0
- package/dist/chunk-TEKTCF2V.js.map +1 -0
- package/dist/{chunk-QOM3F6YW.js → chunk-TIWRP5NW.js} +5 -5
- package/dist/{chunk-CXW6Y7SI.js → chunk-V5RVMULT.js} +4 -4
- package/dist/{chunk-DTSOB2YF.js → chunk-VHYFY3JI.js} +2 -2
- package/dist/{chunk-AZSSKOG6.js → chunk-WAIMHMO3.js} +3 -3
- package/dist/{chunk-PKRRBRY2.js → chunk-ZTBH2OHD.js} +2 -2
- package/dist/{cli-D551dnva.d.ts → cli-CvWpIbMD.d.ts} +2 -2
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +35 -34
- package/dist/compounding/engine.js +4 -3
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/index.js +4 -3
- package/dist/entity-retrieval.js +4 -3
- package/dist/explicit-capture.d.ts +2 -2
- package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +48 -47
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +4 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +11 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
- package/dist/maintenance/rebuild-memory-projection.d.ts +22 -1
- package/dist/maintenance/rebuild-memory-projection.js +5 -4
- package/dist/mcp-memory-inspector-app.d.ts +3 -3
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +9 -8
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +5 -4
- package/dist/operator-toolkit.js +13 -12
- package/dist/orchestration/maintenance.d.ts +1 -1
- package/dist/orchestration/maintenance.js +8 -7
- package/dist/{orchestrator-BnM187cb.d.ts → orchestrator-C04Q4Zao.d.ts} +1 -1
- package/dist/orchestrator.d.ts +2 -2
- package/dist/orchestrator.js +46 -45
- package/dist/schemas.d.ts +84 -84
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +4 -4
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +4 -3
- package/dist/semantic-rule-verifier.js +4 -3
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage-guards.d.ts +9 -0
- package/dist/storage-guards.js +11 -0
- package/dist/storage-guards.js.map +1 -0
- package/dist/storage.js +3 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +4 -3
- package/package.json +2 -2
- package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +59 -8
- package/src/maintenance/rebuild-memory-projection.ts +149 -9
- package/src/namespaces/catalog.test.ts +133 -1
- package/src/namespaces/catalog.ts +163 -48
- package/src/namespaces/identity.ts +2 -2
- package/src/storage/secure-line-reader.ts +79 -0
- package/src/storage-guards.ts +30 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +130 -0
- package/src/storage.ts +24 -41
- package/dist/chunk-6CUUFFU7.js.map +0 -1
- package/dist/chunk-LLVKDFNU.js +0 -77
- package/dist/chunk-LLVKDFNU.js.map +0 -1
- package/dist/chunk-P26O6R3Q.js.map +0 -1
- package/dist/chunk-YP23FQPT.js.map +0 -1
- package/dist/chunk-ZFXCQPNO.js.map +0 -1
- /package/dist/{chunk-OLEBPUZ7.js.map → chunk-2KYXXMTE.js.map} +0 -0
- /package/dist/{chunk-FYJWEGYP.js.map → chunk-4HYJETVO.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-W3M34XT4.js.map → chunk-BAVVI5GP.js.map} +0 -0
- /package/dist/{chunk-XQT5GSWU.js.map → chunk-BRYPUVJU.js.map} +0 -0
- /package/dist/{chunk-2XE4F3VZ.js.map → chunk-DB647OZ4.js.map} +0 -0
- /package/dist/{chunk-3JBW3GHN.js.map → chunk-DPZ5GJMD.js.map} +0 -0
- /package/dist/{chunk-SMLMH7S5.js.map → chunk-EDC7RZCW.js.map} +0 -0
- /package/dist/{chunk-IOZZG75J.js.map → chunk-ES3GAMSQ.js.map} +0 -0
- /package/dist/{chunk-QUGPXPFJ.js.map → chunk-ESDBHQY6.js.map} +0 -0
- /package/dist/{chunk-ILVF6TIY.js.map → chunk-HDFS6RWQ.js.map} +0 -0
- /package/dist/{chunk-SN2EWIAI.js.map → chunk-HDYFNXVO.js.map} +0 -0
- /package/dist/{chunk-ZEQVS6AZ.js.map → chunk-HMUEAKSD.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-QQJMVF6P.js.map → chunk-JU6BKZHX.js.map} +0 -0
- /package/dist/{chunk-SLAZZXBU.js.map → chunk-KZWUJC7Y.js.map} +0 -0
- /package/dist/{chunk-IZLGHKWA.js.map → chunk-LAJIXABF.js.map} +0 -0
- /package/dist/{chunk-3QLPBCFG.js.map → chunk-NKP6II6G.js.map} +0 -0
- /package/dist/{chunk-WIXQ3F5P.js.map → chunk-OQ6F3WBX.js.map} +0 -0
- /package/dist/{chunk-EGQ3AVZV.js.map → chunk-PNFN7SIN.js.map} +0 -0
- /package/dist/{chunk-M2AAYJVI.js.map → chunk-QPFO54VC.js.map} +0 -0
- /package/dist/{chunk-7HWOEYHW.js.map → chunk-QWIE43BS.js.map} +0 -0
- /package/dist/{chunk-QOM3F6YW.js.map → chunk-TIWRP5NW.js.map} +0 -0
- /package/dist/{chunk-CXW6Y7SI.js.map → chunk-V5RVMULT.js.map} +0 -0
- /package/dist/{chunk-DTSOB2YF.js.map → chunk-VHYFY3JI.js.map} +0 -0
- /package/dist/{chunk-AZSSKOG6.js.map → chunk-WAIMHMO3.js.map} +0 -0
- /package/dist/{chunk-PKRRBRY2.js.map → chunk-ZTBH2OHD.js.map} +0 -0
- /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
appendFile,
|
|
6
6
|
lstat,
|
|
7
7
|
mkdir,
|
|
8
|
+
open,
|
|
8
9
|
readdir,
|
|
9
|
-
readFile,
|
|
10
10
|
realpath,
|
|
11
11
|
rename,
|
|
12
12
|
stat,
|
|
@@ -426,6 +426,18 @@ function inferKind(namespace: string, config: PluginConfig): NamespaceKind {
|
|
|
426
426
|
return "explicit";
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
interface CatalogFileIdentity {
|
|
430
|
+
size: number;
|
|
431
|
+
mtimeMs: number;
|
|
432
|
+
ctimeMs: number;
|
|
433
|
+
ino: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
interface CompactedCatalogCache {
|
|
437
|
+
identity: CatalogFileIdentity;
|
|
438
|
+
records: Map<string, NamespaceRecord>;
|
|
439
|
+
}
|
|
440
|
+
|
|
429
441
|
export class NamespaceCatalog {
|
|
430
442
|
private readonly memoryDir: string;
|
|
431
443
|
private readonly stateDir: string;
|
|
@@ -441,12 +453,19 @@ export class NamespaceCatalog {
|
|
|
441
453
|
// neither mistakes the other's lock for self-held (round 6, codex P2 — NBsGP
|
|
442
454
|
// invariant preserved and tightened).
|
|
443
455
|
private readonly criticalSection = new MutationSerializer();
|
|
456
|
+
private compactedCache: CompactedCatalogCache | undefined;
|
|
444
457
|
// Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
|
|
445
458
|
// section, after the lock is acquired but BEFORE the read→merge→append. A
|
|
446
459
|
// deterministic concurrency test installs a hook here to widen the (otherwise
|
|
447
460
|
// microscopic) window and prove that a cross-process rebuild CANNOT run its
|
|
448
461
|
// load→rename while a touch holds the lock. Never set in production code.
|
|
449
462
|
protected onTouchCriticalSectionForTest?: () => Promise<void>;
|
|
463
|
+
// Test-only observation seam: fires only when the JSONL file is fully read.
|
|
464
|
+
// Cached loads do not invoke it.
|
|
465
|
+
protected onCatalogReadForTest?: () => void;
|
|
466
|
+
// Test-only seam: fires after this process appends but before it stats the
|
|
467
|
+
// file to refresh the cache identity.
|
|
468
|
+
protected onAfterCatalogAppendForTest?: () => Promise<void>;
|
|
450
469
|
// Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock
|
|
451
470
|
// critical section, after the final cross-process re-merge `loadCompacted()` and
|
|
452
471
|
// BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-
|
|
@@ -597,19 +616,20 @@ export class NamespaceCatalog {
|
|
|
597
616
|
}
|
|
598
617
|
return [...byStorageDir.values()];
|
|
599
618
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
619
|
+
private loadSanitizedRecords(): Promise<NamespaceRecord[]> {
|
|
620
|
+
return this.queueCritical(async () => {
|
|
621
|
+
const records = await this.loadCompacted();
|
|
622
|
+
const sanitized = await Promise.all(
|
|
623
|
+
[...records.values()].map((r) => this.sanitizeRecordForRead(r)),
|
|
624
|
+
);
|
|
625
|
+
// Drop unsafe-namespace rows (sanitizer returned null) at the read boundary.
|
|
626
|
+
// Then collapse duplicate root aliases so maintenance/QMD see exactly one
|
|
627
|
+
// namespace owner for a physical storage root, matching rebuild ownership,
|
|
628
|
+
// while preserving touch recency from every alias row.
|
|
629
|
+
return this.dropDuplicateStorageRootAliases(
|
|
630
|
+
sanitized.filter((r): r is NamespaceRecord => r !== null),
|
|
631
|
+
);
|
|
632
|
+
});
|
|
613
633
|
}
|
|
614
634
|
|
|
615
635
|
async listNamespaces(filter?: NamespaceCatalogFilter): Promise<NamespaceRecord[]> {
|
|
@@ -1701,7 +1721,7 @@ export class NamespaceCatalog {
|
|
|
1701
1721
|
// scan, which the in-process `queueCritical` alone cannot see. Only runs
|
|
1702
1722
|
// when we hold the lock (round 6, codex P2 — NBPmY): an unlocked rebuild
|
|
1703
1723
|
// must not re-merge then rename, or it races a concurrent lock holder.
|
|
1704
|
-
const latest = await this.loadCompacted();
|
|
1724
|
+
const latest = await this.loadCompacted(true);
|
|
1705
1725
|
for (const [ns, fresh] of latest) {
|
|
1706
1726
|
const current = rebuilt.get(ns);
|
|
1707
1727
|
if (!current) {
|
|
@@ -1897,53 +1917,96 @@ export class NamespaceCatalog {
|
|
|
1897
1917
|
if (prior.parentNamespace !== undefined) merged.parentNamespace = prior.parentNamespace;
|
|
1898
1918
|
return merged;
|
|
1899
1919
|
}
|
|
1900
|
-
|
|
1901
1920
|
// ── Persistence ──────────────────────────────────────────────────────────
|
|
1902
1921
|
|
|
1903
1922
|
/** Load the JSONL log and fold it into current state (last-record-wins). */
|
|
1904
|
-
private async loadCompacted(): Promise<Map<string, NamespaceRecord>> {
|
|
1923
|
+
private async loadCompacted(forceFresh = false): Promise<Map<string, NamespaceRecord>> {
|
|
1905
1924
|
const records = new Map<string, NamespaceRecord>();
|
|
1906
|
-
let
|
|
1925
|
+
let handle;
|
|
1907
1926
|
try {
|
|
1908
|
-
|
|
1927
|
+
handle = await open(this.catalogPath, "r");
|
|
1909
1928
|
} catch {
|
|
1929
|
+
this.compactedCache = undefined;
|
|
1910
1930
|
return records;
|
|
1911
1931
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1932
|
+
|
|
1933
|
+
let identity: CatalogFileIdentity | undefined;
|
|
1934
|
+
try {
|
|
1935
|
+
const fileStat = await handle.stat();
|
|
1936
|
+
identity = {
|
|
1937
|
+
size: fileStat.size,
|
|
1938
|
+
mtimeMs: fileStat.mtimeMs,
|
|
1939
|
+
ctimeMs: fileStat.ctimeMs,
|
|
1940
|
+
ino: fileStat.ino,
|
|
1941
|
+
};
|
|
1942
|
+
if (
|
|
1943
|
+
!forceFresh &&
|
|
1944
|
+
this.compactedCache &&
|
|
1945
|
+
this.compactedCache.identity.size === identity.size &&
|
|
1946
|
+
this.compactedCache.identity.mtimeMs === identity.mtimeMs &&
|
|
1947
|
+
this.compactedCache.identity.ctimeMs === identity.ctimeMs &&
|
|
1948
|
+
this.compactedCache.identity.ino === identity.ino
|
|
1949
|
+
) {
|
|
1950
|
+
await handle.close().catch(() => undefined);
|
|
1951
|
+
return this.cloneCompactedRecords(this.compactedCache.records);
|
|
1921
1952
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
//
|
|
1925
|
-
// Touches run on PER-PROCESS write chains, so two processes (a gateway write
|
|
1926
|
-
// racing a CLI/second-server read or maintenance touch) can each load the
|
|
1927
|
-
// same prior record and append a full snapshot. Plain last-record-wins
|
|
1928
|
-
// compaction would then discard the earlier snapshot's `lastReadAt` /
|
|
1929
|
-
// `lastWriteAt` / `lastMaintenanceAt`, erasing a real touch and skewing
|
|
1930
|
-
// `writtenSince`. We instead take the LATER record as the base (most recent
|
|
1931
|
-
// identity/disk-derived state) and fold in the MAX of each touch field from
|
|
1932
|
-
// both, so no cross-process touch recency is lost without locking the hot
|
|
1933
|
-
// touch path. A destructive overwrite of real memory is never at stake here
|
|
1934
|
-
// — only best-effort recency metadata.
|
|
1935
|
-
const prior = records.get(record.namespace);
|
|
1936
|
-
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
1953
|
+
} catch {
|
|
1954
|
+
// A failed fstat cannot prove freshness. Read and parse the open file, but
|
|
1955
|
+
// do not cache the result without a trustworthy identity.
|
|
1937
1956
|
}
|
|
1938
|
-
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
this.onCatalogReadForTest?.();
|
|
1960
|
+
const raw = await handle.readFile("utf8");
|
|
1961
|
+
for (const line of raw.split("\n")) {
|
|
1962
|
+
const trimmed = line.trim();
|
|
1963
|
+
if (trimmed.length === 0) continue;
|
|
1964
|
+
let parsed: unknown;
|
|
1965
|
+
try {
|
|
1966
|
+
parsed = JSON.parse(trimmed);
|
|
1967
|
+
} catch {
|
|
1968
|
+
// Skip corrupt lines (CLAUDE.md rule #18 robustness).
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
const record = coerceRecord(parsed);
|
|
1972
|
+
if (!record) continue;
|
|
1973
|
+
// Preserve each touch field across cross-process full-snapshot appends.
|
|
1974
|
+
// Field-level touch merge preserves the newest value for every touch
|
|
1975
|
+
// field when full-snapshot appends from separate processes interleave.
|
|
1976
|
+
const prior = records.get(record.namespace);
|
|
1977
|
+
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
1978
|
+
}
|
|
1979
|
+
} catch {
|
|
1980
|
+
this.compactedCache = undefined;
|
|
1981
|
+
return new Map();
|
|
1982
|
+
} finally {
|
|
1983
|
+
await handle.close().catch(() => undefined);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
this.compactedCache = identity ? { identity, records } : undefined;
|
|
1987
|
+
return this.cloneCompactedRecords(records);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
private cloneCompactedRecords(
|
|
1991
|
+
records: Map<string, NamespaceRecord>,
|
|
1992
|
+
): Map<string, NamespaceRecord> {
|
|
1993
|
+
return new Map(
|
|
1994
|
+
[...records].map(([namespace, record]) => [
|
|
1995
|
+
namespace,
|
|
1996
|
+
{
|
|
1997
|
+
...record,
|
|
1998
|
+
lastMaintenanceAt: record.lastMaintenanceAt
|
|
1999
|
+
? { ...record.lastMaintenanceAt }
|
|
2000
|
+
: undefined,
|
|
2001
|
+
},
|
|
2002
|
+
]),
|
|
2003
|
+
);
|
|
1939
2004
|
}
|
|
1940
2005
|
|
|
1941
2006
|
/**
|
|
1942
2007
|
* Serialize an arbitrary read-modify-write critical section through the single
|
|
1943
|
-
* per-instance chain. Every catalog mutation
|
|
1944
|
-
*
|
|
1945
|
-
* reads the latest persisted state before appending, and a rebuild rewrite
|
|
1946
|
-
* cannot interleave with a touch's append.
|
|
2008
|
+
* per-instance chain. Every catalog mutation and cached catalog read runs
|
|
2009
|
+
* through this queue, so cache validation and updates cannot race in-process.
|
|
1947
2010
|
*
|
|
1948
2011
|
* Issue #1524 adoption: delegates to the shared `MutationSerializer` (stored
|
|
1949
2012
|
* as `criticalSection`). The util owns the rejection-recovery invariant
|
|
@@ -1964,8 +2027,13 @@ export class NamespaceCatalog {
|
|
|
1964
2027
|
*/
|
|
1965
2028
|
private async appendUnchained(record: NamespaceRecord): Promise<void> {
|
|
1966
2029
|
const line = serializeRecord(record) + "\n";
|
|
2030
|
+
const lineByteLength = Buffer.byteLength(line, "utf8");
|
|
1967
2031
|
await mkdir(this.stateDir, { recursive: true });
|
|
1968
2032
|
await appendFile(this.catalogPath, line, "utf8");
|
|
2033
|
+
if (this.onAfterCatalogAppendForTest) {
|
|
2034
|
+
await this.onAfterCatalogAppendForTest();
|
|
2035
|
+
}
|
|
2036
|
+
await this.refreshCacheAfterAppend(record, lineByteLength);
|
|
1969
2037
|
}
|
|
1970
2038
|
|
|
1971
2039
|
/**
|
|
@@ -1982,6 +2050,53 @@ export class NamespaceCatalog {
|
|
|
1982
2050
|
const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1983
2051
|
await writeFile(tmp, body, "utf8");
|
|
1984
2052
|
await rename(tmp, this.catalogPath);
|
|
2053
|
+
await this.refreshCacheIdentity(new Map(records.map((record) => [record.namespace, record])));
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
private async refreshCacheAfterAppend(
|
|
2057
|
+
record: NamespaceRecord,
|
|
2058
|
+
lineByteLength: number,
|
|
2059
|
+
): Promise<void> {
|
|
2060
|
+
const cached = this.compactedCache;
|
|
2061
|
+
if (!cached) return;
|
|
2062
|
+
const expectedSize = cached.identity.size + lineByteLength;
|
|
2063
|
+
try {
|
|
2064
|
+
const fileStat = await stat(this.catalogPath);
|
|
2065
|
+
if (fileStat.size !== expectedSize || fileStat.ino !== cached.identity.ino) {
|
|
2066
|
+
this.compactedCache = undefined;
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const prior = cached.records.get(record.namespace);
|
|
2070
|
+
cached.records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
2071
|
+
this.compactedCache = {
|
|
2072
|
+
identity: {
|
|
2073
|
+
size: fileStat.size,
|
|
2074
|
+
mtimeMs: fileStat.mtimeMs,
|
|
2075
|
+
ctimeMs: fileStat.ctimeMs,
|
|
2076
|
+
ino: fileStat.ino,
|
|
2077
|
+
},
|
|
2078
|
+
records: cached.records,
|
|
2079
|
+
};
|
|
2080
|
+
} catch {
|
|
2081
|
+
this.compactedCache = undefined;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
private async refreshCacheIdentity(records: Map<string, NamespaceRecord>): Promise<void> {
|
|
2086
|
+
try {
|
|
2087
|
+
const fileStat = await stat(this.catalogPath);
|
|
2088
|
+
this.compactedCache = {
|
|
2089
|
+
identity: {
|
|
2090
|
+
size: fileStat.size,
|
|
2091
|
+
mtimeMs: fileStat.mtimeMs,
|
|
2092
|
+
ctimeMs: fileStat.ctimeMs,
|
|
2093
|
+
ino: fileStat.ino,
|
|
2094
|
+
},
|
|
2095
|
+
records,
|
|
2096
|
+
};
|
|
2097
|
+
} catch {
|
|
2098
|
+
this.compactedCache = undefined;
|
|
2099
|
+
}
|
|
1985
2100
|
}
|
|
1986
2101
|
}
|
|
1987
2102
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function normalizeNamespaceIdentity(namespace: string): string {
|
|
2
|
-
return namespace
|
|
1
|
+
export function normalizeNamespaceIdentity(namespace: string | null | undefined): string {
|
|
2
|
+
return namespace?.trim() ?? "";
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function namespaceIdentityToken(namespace: string): string {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
|
|
5
|
+
import { isEncryptedFile, MAGIC_HEADER_SIZE } from "../secure-store/secure-fs.js";
|
|
6
|
+
import type { MemoryActionEvent } from "../types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stream plaintext files by line while preserving authenticated whole-file
|
|
10
|
+
* reads for encrypted files.
|
|
11
|
+
*/
|
|
12
|
+
export async function* readMaybeEncryptedLines(
|
|
13
|
+
filePath: string,
|
|
14
|
+
readEncryptedFile: () => Promise<string>,
|
|
15
|
+
): AsyncGenerator<string> {
|
|
16
|
+
const file = await open(filePath, "r");
|
|
17
|
+
const header = Buffer.alloc(MAGIC_HEADER_SIZE);
|
|
18
|
+
try {
|
|
19
|
+
const { bytesRead } = await file.read(header, 0, header.length, 0);
|
|
20
|
+
if (isEncryptedFile(header.subarray(0, bytesRead))) {
|
|
21
|
+
yield* (await readEncryptedFile()).split("\n");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const input = createReadStream(filePath, {
|
|
26
|
+
autoClose: false,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
fd: file.fd,
|
|
29
|
+
start: 0,
|
|
30
|
+
});
|
|
31
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
32
|
+
try {
|
|
33
|
+
for await (const line of lines) yield line;
|
|
34
|
+
} finally {
|
|
35
|
+
lines.close();
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
await file.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readMemoryActionEventRowsFromLines(
|
|
43
|
+
lines: AsyncIterable<string>,
|
|
44
|
+
limit: number,
|
|
45
|
+
): Promise<Array<{ line: number; event: MemoryActionEvent }>> {
|
|
46
|
+
const out: Array<{ line: number; event: MemoryActionEvent }> = [];
|
|
47
|
+
let writeIndex = 0;
|
|
48
|
+
let lineNumber = 0;
|
|
49
|
+
for await (const row of lines) {
|
|
50
|
+
lineNumber += 1;
|
|
51
|
+
const line = row.trim();
|
|
52
|
+
if (!line) continue;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(line) as Partial<MemoryActionEvent>;
|
|
55
|
+
const outcome = parsed.outcome === "applied" || parsed.outcome === "skipped" || parsed.outcome === "failed"
|
|
56
|
+
? parsed.outcome
|
|
57
|
+
: null;
|
|
58
|
+
if (
|
|
59
|
+
typeof parsed.timestamp === "string" &&
|
|
60
|
+
typeof parsed.action === "string" &&
|
|
61
|
+
outcome !== null
|
|
62
|
+
) {
|
|
63
|
+
const entry = {
|
|
64
|
+
line: lineNumber,
|
|
65
|
+
event: { ...parsed, outcome } as MemoryActionEvent,
|
|
66
|
+
};
|
|
67
|
+
if (out.length < limit) {
|
|
68
|
+
out.push(entry);
|
|
69
|
+
} else {
|
|
70
|
+
out[writeIndex] = entry;
|
|
71
|
+
writeIndex = (writeIndex + 1) % limit;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore malformed rows (fail-open).
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return writeIndex === 0 ? out : [...out.slice(writeIndex), ...out.slice(0, writeIndex)];
|
|
79
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
|
|
3
|
+
const PROJECTION_FALLBACK_WARN_INTERVAL_MS = 5 * 60_000;
|
|
4
|
+
const projectionFallbackWarnedAt = new Map<string, number>();
|
|
5
|
+
|
|
6
|
+
export function assertMemoryFrontmatterId(
|
|
7
|
+
fm: { id: string; category?: string; created?: string; entityRef?: string },
|
|
8
|
+
): void {
|
|
9
|
+
if (typeof fm.id !== "string" || fm.id.trim().length === 0) {
|
|
10
|
+
const context = [
|
|
11
|
+
fm.category ? `category=${fm.category}` : null,
|
|
12
|
+
fm.created ? `created=${fm.created}` : null,
|
|
13
|
+
fm.entityRef ? `entityRef=${fm.entityRef}` : null,
|
|
14
|
+
].filter(Boolean).join(" ");
|
|
15
|
+
throw new Error(
|
|
16
|
+
`memory frontmatter id must not be blank${context ? ` (${context})` : ""}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function warnProjectionFallback(memoryDir: string, consumer: string): null {
|
|
22
|
+
const key = `${memoryDir}\0${consumer}`;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const warnedAt = projectionFallbackWarnedAt.get(key) ?? 0;
|
|
25
|
+
if (now - warnedAt >= PROJECTION_FALLBACK_WARN_INTERVAL_MS) {
|
|
26
|
+
projectionFallbackWarnedAt.set(key, now);
|
|
27
|
+
log.warn(`storage.${consumer}: memory projection absent or empty; falling back to full corpus`);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { StorageManager } from "./storage.js";
|
|
8
|
+
import { encryptFileBody, filePathAad } from "./secure-store/secure-fs.js";
|
|
9
|
+
import type { MemoryLifecycleEvent } from "./types.js";
|
|
10
|
+
|
|
11
|
+
function lifecycleEvent(
|
|
12
|
+
eventId: string,
|
|
13
|
+
memoryId: string,
|
|
14
|
+
timestamp: string,
|
|
15
|
+
): MemoryLifecycleEvent {
|
|
16
|
+
return {
|
|
17
|
+
eventId,
|
|
18
|
+
memoryId,
|
|
19
|
+
eventType: "created",
|
|
20
|
+
timestamp,
|
|
21
|
+
actor: "test",
|
|
22
|
+
ruleVersion: "1",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function withLifecycleLedger(
|
|
27
|
+
rows: string[],
|
|
28
|
+
run: (storage: StorageManager, ledgerPath: string) => Promise<void>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-lifecycle-stream-"));
|
|
31
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
32
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
33
|
+
await writeFile(ledgerPath, `${rows.join("\n")}\n`, "utf8");
|
|
34
|
+
try {
|
|
35
|
+
await run(new StorageManager(memoryDir), ledgerPath);
|
|
36
|
+
} finally {
|
|
37
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("readAllMemoryLifecycleEvents streams plaintext rows and preserves fail-open sorting", async () => {
|
|
42
|
+
const later = lifecycleEvent("event-z", "memory-b", "2026-01-03T00:00:00.000Z");
|
|
43
|
+
const earlier = lifecycleEvent("event-a", "memory-a", "2026-01-02T00:00:00.000Z");
|
|
44
|
+
const earliest = lifecycleEvent("event-b", "memory-a", "2026-01-01T00:00:00.000Z");
|
|
45
|
+
|
|
46
|
+
await withLifecycleLedger([
|
|
47
|
+
JSON.stringify(later),
|
|
48
|
+
"{malformed-json",
|
|
49
|
+
JSON.stringify({ ...earlier, actor: 42 }),
|
|
50
|
+
JSON.stringify(earlier),
|
|
51
|
+
" ",
|
|
52
|
+
JSON.stringify(earliest),
|
|
53
|
+
], async (storage) => {
|
|
54
|
+
let secureWholeFileReads = 0;
|
|
55
|
+
const privateStorage = storage as unknown as {
|
|
56
|
+
readStorageSecureFile(filePath: string): Promise<string>;
|
|
57
|
+
};
|
|
58
|
+
privateStorage.readStorageSecureFile = async () => {
|
|
59
|
+
secureWholeFileReads += 1;
|
|
60
|
+
throw new Error("plaintext lifecycle ledger must not use whole-file reads");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const events = await storage.readAllMemoryLifecycleEvents();
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(events, [earliest, earlier, later]);
|
|
66
|
+
assert.equal(secureWholeFileReads, 0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("readMemoryLifecycleEvents applies its limit after streamed lifecycle sorting", async () => {
|
|
71
|
+
const first = lifecycleEvent("event-1", "memory-a", "2026-01-01T00:00:00.000Z");
|
|
72
|
+
const second = lifecycleEvent("event-2", "memory-a", "2026-01-02T00:00:00.000Z");
|
|
73
|
+
const third = lifecycleEvent("event-3", "memory-b", "2026-01-01T00:00:00.000Z");
|
|
74
|
+
|
|
75
|
+
await withLifecycleLedger([
|
|
76
|
+
JSON.stringify(third),
|
|
77
|
+
JSON.stringify(first),
|
|
78
|
+
JSON.stringify(second),
|
|
79
|
+
], async (storage) => {
|
|
80
|
+
assert.deepEqual(await storage.readMemoryLifecycleEvents(2), [second, third]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("readAllMemoryLifecycleEvents keeps the authenticated encrypted-file fallback", async () => {
|
|
85
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-lifecycle-encrypted-"));
|
|
86
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
87
|
+
const event = lifecycleEvent("event-encrypted", "memory-a", "2026-01-01T00:00:00.000Z");
|
|
88
|
+
const key = Buffer.alloc(32, 7);
|
|
89
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
90
|
+
await writeFile(
|
|
91
|
+
ledgerPath,
|
|
92
|
+
encryptFileBody(`${JSON.stringify(event)}\n`, key, filePathAad(ledgerPath, memoryDir)),
|
|
93
|
+
);
|
|
94
|
+
try {
|
|
95
|
+
const storage = new StorageManager(memoryDir);
|
|
96
|
+
storage.setSecureStoreKey(key);
|
|
97
|
+
assert.deepEqual(await storage.readAllMemoryLifecycleEvents(), [event]);
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("readMemoryActionEventRows streams the action ledger and keeps source line numbers", async () => {
|
|
104
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-action-stream-"));
|
|
105
|
+
const ledgerPath = path.join(memoryDir, "state", "memory-actions.jsonl");
|
|
106
|
+
const first = { timestamp: "2026-01-01T00:00:00.000Z", action: "store_note", outcome: "applied" };
|
|
107
|
+
const second = { timestamp: "2026-01-02T00:00:00.000Z", action: "discard", outcome: "skipped" };
|
|
108
|
+
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
109
|
+
await writeFile(
|
|
110
|
+
ledgerPath,
|
|
111
|
+
`${JSON.stringify(first)}\n{malformed-json\n${JSON.stringify(second)}\n`,
|
|
112
|
+
"utf8",
|
|
113
|
+
);
|
|
114
|
+
try {
|
|
115
|
+
const storage = new StorageManager(memoryDir);
|
|
116
|
+
let secureWholeFileReads = 0;
|
|
117
|
+
const privateStorage = storage as unknown as {
|
|
118
|
+
readStorageSecureFile(filePath: string): Promise<string>;
|
|
119
|
+
};
|
|
120
|
+
privateStorage.readStorageSecureFile = async () => {
|
|
121
|
+
secureWholeFileReads += 1;
|
|
122
|
+
throw new Error("plaintext action ledger must not use whole-file reads");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
assert.deepEqual(await storage.readMemoryActionEventRows(1), [{ line: 3, event: second }]);
|
|
126
|
+
assert.equal(secureWholeFileReads, 0);
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -4,7 +4,12 @@ import { createHash } from "node:crypto"
|
|
|
4
4
|
import { normalizeContent, computeContentHash } from "./content-hash.js";;
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { log } from "./logger.js";
|
|
7
|
+
import { assertMemoryFrontmatterId, warnProjectionFallback } from "./storage-guards.js";
|
|
7
8
|
import { MemoryReadStore } from "./storage/memory-read-store.js";
|
|
9
|
+
import {
|
|
10
|
+
readMaybeEncryptedLines,
|
|
11
|
+
readMemoryActionEventRowsFromLines,
|
|
12
|
+
} from "./storage/secure-line-reader.js";
|
|
8
13
|
import { selfDeps } from "./orchestration/self-deps.js";
|
|
9
14
|
import { EntityStore } from "./storage/entity-store.js";
|
|
10
15
|
import { IdentityContinuityStore } from "./storage/identity-continuity-store.js";
|
|
@@ -304,6 +309,7 @@ function stripCitationMarkersForHashRemoval(value: string, template: string): st
|
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
function serializeFrontmatter(fm: MemoryFrontmatter): string {
|
|
312
|
+
assertMemoryFrontmatterId(fm);
|
|
307
313
|
const lines = [
|
|
308
314
|
"---",
|
|
309
315
|
`id: ${fm.id}`,
|
|
@@ -5180,38 +5186,16 @@ export class StorageManager {
|
|
|
5180
5186
|
|
|
5181
5187
|
async readMemoryActionEventRows(limit: number = 200): Promise<Array<{ line: number; event: MemoryActionEvent }>> {
|
|
5182
5188
|
const cappedLimit = Math.max(0, Math.floor(limit));
|
|
5183
|
-
if (cappedLimit === 0) return [];
|
|
5189
|
+
if (cappedLimit === 0 || Number.isNaN(cappedLimit)) return [];
|
|
5184
5190
|
|
|
5185
5191
|
try {
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
const parsed = JSON.parse(line) as Partial<MemoryActionEvent>;
|
|
5194
|
-
const outcome = parsed.outcome === "applied" || parsed.outcome === "skipped" || parsed.outcome === "failed"
|
|
5195
|
-
? parsed.outcome
|
|
5196
|
-
: null;
|
|
5197
|
-
if (
|
|
5198
|
-
typeof parsed.timestamp === "string" &&
|
|
5199
|
-
typeof parsed.action === "string" &&
|
|
5200
|
-
outcome !== null
|
|
5201
|
-
) {
|
|
5202
|
-
out.push({
|
|
5203
|
-
line: i + 1,
|
|
5204
|
-
event: {
|
|
5205
|
-
...parsed,
|
|
5206
|
-
outcome,
|
|
5207
|
-
} as MemoryActionEvent,
|
|
5208
|
-
});
|
|
5209
|
-
}
|
|
5210
|
-
} catch {
|
|
5211
|
-
// Ignore malformed rows (fail-open).
|
|
5212
|
-
}
|
|
5213
|
-
}
|
|
5214
|
-
return out.reverse();
|
|
5192
|
+
return await readMemoryActionEventRowsFromLines(
|
|
5193
|
+
readMaybeEncryptedLines(
|
|
5194
|
+
this.memoryActionsPath,
|
|
5195
|
+
() => this.readStorageSecureFile(this.memoryActionsPath),
|
|
5196
|
+
),
|
|
5197
|
+
cappedLimit,
|
|
5198
|
+
);
|
|
5215
5199
|
} catch (err) {
|
|
5216
5200
|
if (err instanceof SecureStoreLockedError) throw err;
|
|
5217
5201
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
@@ -5221,10 +5205,11 @@ export class StorageManager {
|
|
|
5221
5205
|
|
|
5222
5206
|
async readAllMemoryLifecycleEvents(): Promise<MemoryLifecycleEvent[]> {
|
|
5223
5207
|
try {
|
|
5224
|
-
const raw = await this.readStorageSecureFile(this.memoryLifecycleLedgerPath);
|
|
5225
5208
|
const out: MemoryLifecycleEvent[] = [];
|
|
5226
|
-
const
|
|
5227
|
-
|
|
5209
|
+
for await (const line of readMaybeEncryptedLines(
|
|
5210
|
+
this.memoryLifecycleLedgerPath,
|
|
5211
|
+
() => this.readStorageSecureFile(this.memoryLifecycleLedgerPath),
|
|
5212
|
+
)) {
|
|
5228
5213
|
const row = line.trim();
|
|
5229
5214
|
if (!row) continue;
|
|
5230
5215
|
try {
|
|
@@ -6037,8 +6022,8 @@ export class StorageManager {
|
|
|
6037
6022
|
lastAccessed: entry.lastAccessed,
|
|
6038
6023
|
};
|
|
6039
6024
|
|
|
6040
|
-
const fileContent = `${serializeFrontmatter(newFm)}\n\n${memory.content}\n`;
|
|
6041
6025
|
try {
|
|
6026
|
+
const fileContent = `${serializeFrontmatter(newFm)}\n\n${memory.content}\n`;
|
|
6042
6027
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
6043
6028
|
updated++;
|
|
6044
6029
|
} catch (err) {
|
|
@@ -6087,8 +6072,7 @@ export class StorageManager {
|
|
|
6087
6072
|
|
|
6088
6073
|
async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
|
|
6089
6074
|
const projected = readProjectedMemoryState(this.baseDir, id);
|
|
6090
|
-
if (projected) return projected;
|
|
6091
|
-
|
|
6075
|
+
if (projected) return projected; warnProjectionFallback(this.baseDir, "getProjectedMemoryState");
|
|
6092
6076
|
const active = await this.getMemoryById(id);
|
|
6093
6077
|
if (active) return this.toProjectedCurrentState(active, "active");
|
|
6094
6078
|
|
|
@@ -6101,7 +6085,8 @@ export class StorageManager {
|
|
|
6101
6085
|
async browseProjectedMemories(
|
|
6102
6086
|
options: ProjectedMemoryBrowseOptions,
|
|
6103
6087
|
): Promise<ProjectedMemoryBrowsePage | null> {
|
|
6104
|
-
return readProjectedMemoryBrowse(this.baseDir, options)
|
|
6088
|
+
return readProjectedMemoryBrowse(this.baseDir, options)
|
|
6089
|
+
?? warnProjectionFallback(this.baseDir, "browseProjectedMemories");
|
|
6105
6090
|
}
|
|
6106
6091
|
|
|
6107
6092
|
async getProjectedGovernanceRecord(): Promise<ReturnType<typeof readProjectedGovernanceRecord>> {
|
|
@@ -6141,8 +6126,7 @@ export class StorageManager {
|
|
|
6141
6126
|
if (cappedLimit === 0) return [];
|
|
6142
6127
|
|
|
6143
6128
|
const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
|
|
6144
|
-
if (projected && projected.length > 0) return projected;
|
|
6145
|
-
|
|
6129
|
+
if (projected && projected.length > 0) return projected; warnProjectionFallback(this.baseDir, "getMemoryTimeline");
|
|
6146
6130
|
const events = await this.readAllMemoryLifecycleEvents();
|
|
6147
6131
|
return events.filter((event) => event.memoryId === memoryId).slice(-cappedLimit);
|
|
6148
6132
|
}
|
|
@@ -6399,9 +6383,8 @@ export class StorageManager {
|
|
|
6399
6383
|
updated: now,
|
|
6400
6384
|
};
|
|
6401
6385
|
|
|
6402
|
-
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${memory.content}\n`;
|
|
6403
|
-
|
|
6404
6386
|
try {
|
|
6387
|
+
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${memory.content}\n`;
|
|
6405
6388
|
await this.writeStorageSecureFile(memory.path, fileContent);
|
|
6406
6389
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.archiveMemories", {
|
|
6407
6390
|
memoryId: id,
|