@remnic/core 9.51.0 → 9.52.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 +6 -5
- package/dist/access-admin-ops-surface.js +16 -16
- package/dist/access-authorization-probe.d.ts +6 -5
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +39 -39
- package/dist/access-extraction-force-flush.d.ts +6 -5
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-lcm-compaction.d.ts +6 -5
- package/dist/access-http-lifecycle-flush.d.ts +6 -5
- package/dist/access-http-offline-stream.d.ts +6 -5
- package/dist/access-http-query.js +16 -16
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +22 -22
- package/dist/access-identity-continuity-surface.d.ts +5 -4
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +20 -20
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-offline-manifest.d.ts +5 -4
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.d.ts +9 -8
- package/dist/access-operations.js +19 -19
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.d.ts +6 -5
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-BY0KJ44c.d.ts → access-service-Cbk3bNbD.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +6 -5
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-merge-XMFBXVDP.js → capsule-merge-HCJJMLPS.js} +4 -4
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-PWN32TAG.js → chunk-2ASAS3CQ.js} +2 -2
- package/dist/{chunk-WQM326GS.js → chunk-2LY6GIIE.js} +2 -2
- package/dist/{chunk-JL43BVDE.js → chunk-2OIQDTDC.js} +2 -2
- package/dist/{chunk-33SV3E43.js → chunk-3OY7KXHM.js} +3 -3
- package/dist/{chunk-AIUBBHSF.js → chunk-5UZ6UCOZ.js} +3 -3
- package/dist/chunk-5WITETWU.js +591 -0
- package/dist/chunk-5WITETWU.js.map +1 -0
- package/dist/{chunk-UT5BV5OG.js → chunk-7HXZJAID.js} +2 -2
- package/dist/{chunk-M7BXDKPR.js → chunk-7M6NYQWB.js} +3 -3
- package/dist/{chunk-TPTECFWR.js → chunk-B6LZ4IBH.js} +3 -3
- package/dist/{chunk-J5LLX6IL.js → chunk-BF6SXNQJ.js} +10 -10
- package/dist/{chunk-4PC4PRJE.js → chunk-BKIFTXHR.js} +2 -2
- package/dist/{chunk-VVG2BOKA.js → chunk-BMEYMKWX.js} +2 -2
- package/dist/{chunk-DCZBSE4F.js → chunk-D5YOZ66N.js} +18 -18
- package/dist/{chunk-BHXYAVUM.js → chunk-DLG5V6ND.js} +2 -2
- package/dist/{chunk-52UKFTUK.js → chunk-F4EU6VUC.js} +2 -2
- package/dist/{chunk-M4A44LUA.js → chunk-GSTIBXBW.js} +2 -2
- package/dist/{chunk-W5XVQFLM.js → chunk-GXFSQ6SD.js} +2 -2
- package/dist/{chunk-XADV5IZ3.js → chunk-HDIUYMSB.js} +5 -5
- package/dist/{chunk-FXTYIA46.js → chunk-J272WKX2.js} +2 -2
- package/dist/{chunk-ENTCVTQR.js → chunk-JJRVPWFY.js} +11 -1
- package/dist/chunk-JJRVPWFY.js.map +1 -0
- package/dist/{chunk-JYL3BVVZ.js → chunk-JSEJH2YQ.js} +2 -2
- package/dist/{chunk-5UDMFE3Q.js → chunk-MLXZD4WM.js} +2 -2
- package/dist/{chunk-PUSLMZBQ.js → chunk-NOEMGE56.js} +2 -2
- package/dist/{chunk-C5QLI33R.js → chunk-QINJITDS.js} +2 -2
- package/dist/{chunk-45BRYON6.js → chunk-QPOHMO6M.js} +2 -2
- package/dist/{chunk-ZQOCRIGA.js → chunk-QYN5PXDZ.js} +3 -3
- package/dist/{chunk-PA45XCUD.js → chunk-RANKJI5T.js} +2 -2
- package/dist/{chunk-ZHY5TMJG.js → chunk-RMHMUUBW.js} +2392 -628
- package/dist/chunk-RMHMUUBW.js.map +1 -0
- package/dist/{chunk-37ZO6LLJ.js → chunk-RYLEDKQ2.js} +2 -2
- package/dist/{chunk-CV6BBVYV.js → chunk-TEMSWHQD.js} +2 -2
- package/dist/{chunk-QGR6OVGT.js → chunk-VFSAWRMZ.js} +2 -2
- package/dist/{chunk-NLK2VZYZ.js → chunk-VXCNQYTC.js} +2 -2
- package/dist/{chunk-AZ2QBGYB.js → chunk-W7Y5JDVB.js} +1 -1
- package/dist/{chunk-VBHPOWBJ.js → chunk-WD5B5AIE.js} +1051 -591
- package/dist/chunk-WD5B5AIE.js.map +1 -0
- package/dist/{chunk-OYESWJ43.js → chunk-WLRD2TXF.js} +15 -10
- package/dist/chunk-WLRD2TXF.js.map +1 -0
- package/dist/{chunk-4F6X3RXW.js → chunk-X57M2Q4I.js} +2 -2
- package/dist/{chunk-6MTSFX7A.js → chunk-X7XEDOSL.js} +8 -8
- package/dist/{chunk-UEVLYVNW.js → chunk-YXKVLYXU.js} +2 -2
- package/dist/{chunk-OQAL6SIR.js → chunk-ZMSZVISA.js} +2 -2
- package/dist/{cli-CnOkH9hA.d.ts → cli-DMAFI0qt.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +33 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/consolidation-undo.js +3 -3
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +1 -1
- package/dist/convergence-refresh.js +9 -9
- package/dist/corpus-watermark.js +9 -9
- package/dist/dependency-propagation-delivery-D63r24pH.d.ts +41 -0
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/explicit-capture.d.ts +5 -4
- package/dist/explicit-capture.js +4 -4
- package/dist/external-wiki-access.d.ts +6 -5
- package/dist/external-wiki-access.js +17 -17
- package/dist/external-wiki-mcp-tools.d.ts +6 -5
- package/dist/extraction.js +7 -7
- package/dist/{forget-UJDJHG6R.js → forget-HDS4N6CG.js} +3 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +687 -686
- package/dist/index.js +39 -39
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/{maintenance-CKwxpdRx.d.ts → maintenance-kBII2qUI.d.ts} +3 -2
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-doctor-corpus.js +10 -10
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-BshGam-o.d.ts → orchestrator-DHy5LtTy.d.ts} +20 -35
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +39 -39
- package/dist/page-versioning.js +3 -3
- package/dist/schemas.d.ts +138 -138
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/{service-BIgVM7fL.d.ts → service-PT2_z7xm.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/{storage-CVfGWqCI.d.ts → storage-BmVudhk4.d.ts} +46 -27
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -6
- package/dist/temporal-supersession.d.ts +51 -26
- package/dist/temporal-supersession.js +3 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/lifecycle/tombstones.ts +30 -0
- package/src/orchestration/consolidation-run.ts +324 -106
- package/src/orchestration/contradiction-linking-coordinator.ts +237 -64
- package/src/orchestration/dependency-propagation-delivery.test.ts +2353 -0
- package/src/orchestration/dependency-propagation-delivery.ts +1143 -0
- package/src/orchestration/dependency-propagation-queue-state.ts +220 -0
- package/src/orchestration/dependency-propagation-recovery-replay.ts +237 -0
- package/src/orchestration/dependency-propagation-replay-reconciliation.ts +73 -0
- package/src/orchestration/dependency-propagation-triggers.test.ts +1069 -11
- package/src/orchestration/dependency-propagation.test.ts +82 -31
- package/src/orchestration/dependency-propagation.ts +55 -17
- package/src/orchestration/extraction-persist-deps.ts +2 -2
- package/src/orchestration/extraction-persist.ts +14 -15
- package/src/orchestration/extraction-queue-coordinator.ts +23 -1
- package/src/orchestration/extraction-shutdown.ts +29 -0
- package/src/orchestration/maintenance.ts +13 -7
- package/src/orchestration/turn-ingestion.ts +5 -1
- package/src/orchestrator-background-writes.test.ts +265 -1
- package/src/orchestrator-extraction-queue.test.ts +39 -0
- package/src/orchestrator.ts +59 -58
- package/src/storage/committed-invalidation.ts +34 -0
- package/src/storage/deletion-revision-store.ts +529 -0
- package/src/storage/supersession-audit.ts +41 -0
- package/src/storage/supersession-side-effects.ts +52 -25
- package/src/storage-contract/failure-semantics.test.ts +12 -0
- package/src/storage-contract/round-trip.test.ts +184 -2
- package/src/storage-deletion-revisions.test.ts +213 -1
- package/src/storage-frontmatter-cas.test.ts +141 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +372 -0
- package/src/storage-replay.test.ts +149 -0
- package/src/storage.ts +345 -358
- package/src/temporal-supersession.test.ts +431 -2
- package/src/temporal-supersession.ts +567 -107
- package/src/testing/subjects/dependency-propagation.test.ts +179 -38
- package/dist/chunk-ENTCVTQR.js.map +0 -1
- package/dist/chunk-EWMTOVMY.js +0 -484
- package/dist/chunk-EWMTOVMY.js.map +0 -1
- package/dist/chunk-OYESWJ43.js.map +0 -1
- package/dist/chunk-VBHPOWBJ.js.map +0 -1
- package/dist/chunk-ZHY5TMJG.js.map +0 -1
- /package/dist/{capsule-merge-XMFBXVDP.js.map → capsule-merge-HCJJMLPS.js.map} +0 -0
- /package/dist/{chunk-PWN32TAG.js.map → chunk-2ASAS3CQ.js.map} +0 -0
- /package/dist/{chunk-WQM326GS.js.map → chunk-2LY6GIIE.js.map} +0 -0
- /package/dist/{chunk-JL43BVDE.js.map → chunk-2OIQDTDC.js.map} +0 -0
- /package/dist/{chunk-33SV3E43.js.map → chunk-3OY7KXHM.js.map} +0 -0
- /package/dist/{chunk-AIUBBHSF.js.map → chunk-5UZ6UCOZ.js.map} +0 -0
- /package/dist/{chunk-UT5BV5OG.js.map → chunk-7HXZJAID.js.map} +0 -0
- /package/dist/{chunk-M7BXDKPR.js.map → chunk-7M6NYQWB.js.map} +0 -0
- /package/dist/{chunk-TPTECFWR.js.map → chunk-B6LZ4IBH.js.map} +0 -0
- /package/dist/{chunk-J5LLX6IL.js.map → chunk-BF6SXNQJ.js.map} +0 -0
- /package/dist/{chunk-4PC4PRJE.js.map → chunk-BKIFTXHR.js.map} +0 -0
- /package/dist/{chunk-VVG2BOKA.js.map → chunk-BMEYMKWX.js.map} +0 -0
- /package/dist/{chunk-DCZBSE4F.js.map → chunk-D5YOZ66N.js.map} +0 -0
- /package/dist/{chunk-BHXYAVUM.js.map → chunk-DLG5V6ND.js.map} +0 -0
- /package/dist/{chunk-52UKFTUK.js.map → chunk-F4EU6VUC.js.map} +0 -0
- /package/dist/{chunk-M4A44LUA.js.map → chunk-GSTIBXBW.js.map} +0 -0
- /package/dist/{chunk-W5XVQFLM.js.map → chunk-GXFSQ6SD.js.map} +0 -0
- /package/dist/{chunk-XADV5IZ3.js.map → chunk-HDIUYMSB.js.map} +0 -0
- /package/dist/{chunk-FXTYIA46.js.map → chunk-J272WKX2.js.map} +0 -0
- /package/dist/{chunk-JYL3BVVZ.js.map → chunk-JSEJH2YQ.js.map} +0 -0
- /package/dist/{chunk-5UDMFE3Q.js.map → chunk-MLXZD4WM.js.map} +0 -0
- /package/dist/{chunk-PUSLMZBQ.js.map → chunk-NOEMGE56.js.map} +0 -0
- /package/dist/{chunk-C5QLI33R.js.map → chunk-QINJITDS.js.map} +0 -0
- /package/dist/{chunk-45BRYON6.js.map → chunk-QPOHMO6M.js.map} +0 -0
- /package/dist/{chunk-ZQOCRIGA.js.map → chunk-QYN5PXDZ.js.map} +0 -0
- /package/dist/{chunk-PA45XCUD.js.map → chunk-RANKJI5T.js.map} +0 -0
- /package/dist/{chunk-37ZO6LLJ.js.map → chunk-RYLEDKQ2.js.map} +0 -0
- /package/dist/{chunk-CV6BBVYV.js.map → chunk-TEMSWHQD.js.map} +0 -0
- /package/dist/{chunk-QGR6OVGT.js.map → chunk-VFSAWRMZ.js.map} +0 -0
- /package/dist/{chunk-NLK2VZYZ.js.map → chunk-VXCNQYTC.js.map} +0 -0
- /package/dist/{chunk-AZ2QBGYB.js.map → chunk-W7Y5JDVB.js.map} +0 -0
- /package/dist/{chunk-4F6X3RXW.js.map → chunk-X57M2Q4I.js.map} +0 -0
- /package/dist/{chunk-6MTSFX7A.js.map → chunk-X7XEDOSL.js.map} +0 -0
- /package/dist/{chunk-UEVLYVNW.js.map → chunk-YXKVLYXU.js.map} +0 -0
- /package/dist/{chunk-OQAL6SIR.js.map → chunk-ZMSZVISA.js.map} +0 -0
- /package/dist/{forget-UJDJHG6R.js.map → forget-HDS4N6CG.js.map} +0 -0
package/src/storage.ts
CHANGED
|
@@ -4,26 +4,27 @@ import {
|
|
|
4
4
|
readdir,
|
|
5
5
|
readFile,
|
|
6
6
|
realpath,
|
|
7
|
-
rename,
|
|
8
7
|
stat,
|
|
9
8
|
writeFile,
|
|
10
9
|
mkdir,
|
|
11
10
|
unlink,
|
|
12
11
|
appendFile,
|
|
13
12
|
open,
|
|
14
|
-
type FileHandle,
|
|
15
13
|
} from "node:fs/promises";
|
|
16
|
-
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync
|
|
17
|
-
import { createHash
|
|
14
|
+
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
18
16
|
import { normalizeContent, computeContentHash } from "./content-hash.js";
|
|
19
17
|
import { raceAbort } from "./abort-error.js";
|
|
20
18
|
import { checkCorpusReadAbort, type CorpusReadOptions } from "./corpus-read-cancellation.js";
|
|
21
19
|
import { selectArtifactMatches, type ArtifactSearchOptions } from "./artifact-search.js";
|
|
22
20
|
import path from "node:path";
|
|
21
|
+
import { resolveSafeStoragePath } from "./storage-paths.js";
|
|
23
22
|
import { log } from "./logger.js";
|
|
24
23
|
import { createMemorySnapshot } from "./memory-snapshot.js";
|
|
25
24
|
import { assertMemoryFrontmatterId, warnProjectionFallback } from "./storage-guards.js";
|
|
26
25
|
import { MemoryReadStore } from "./storage/memory-read-store.js";
|
|
26
|
+
import { hasSupersessionAudit } from "./storage/supersession-audit.js";
|
|
27
|
+
import { runCommittedInvalidation } from "./storage/committed-invalidation.js";
|
|
27
28
|
import { renderProfileWithLastUpdated } from "./storage/profile-header.js";
|
|
28
29
|
import { readMaybeEncryptedLines, readMemoryActionEventRowsFromLines } from "./storage/secure-line-reader.js";
|
|
29
30
|
import {
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
} from "./storage/memory-lifecycle-ledger-access.js";
|
|
41
42
|
import { selfDeps } from "./orchestration/self-deps.js";
|
|
42
43
|
import { EntityStore } from "./storage/entity-store.js";
|
|
44
|
+
import { DeletionRevisionStore, invalidationCommitFingerprint } from "./storage/deletion-revision-store.js";
|
|
43
45
|
import { IdentityContinuityStore } from "./storage/identity-continuity-store.js";
|
|
44
46
|
import * as entityMigration from "./storage/entity-canonical-id-migration.js";
|
|
45
47
|
import * as entityRefs from "./storage/entity-canonical-id-references.js";
|
|
@@ -57,7 +59,6 @@ export { normalizeEntityName } from "./entity-id-normalization.js";
|
|
|
57
59
|
import { isErrnoCode } from "./utils/errno.js";
|
|
58
60
|
import { getCategoryDir, categoryDirName } from "./utils/category-dir.js";
|
|
59
61
|
import { decodeYamlScalar } from "./utils/yaml-scalar.js";
|
|
60
|
-
import { withHeldFileLock, type HeldFileLockController } from "./utils/serialize-mutations.js";
|
|
61
62
|
import { qmdCollectionPathParts, qmdResultPathCandidates } from "./orchestration/qmd-result-resolver.js";
|
|
62
63
|
import {
|
|
63
64
|
clearMemoryCache,
|
|
@@ -250,36 +251,6 @@ type OfflineSyncDigestCacheEntry = {
|
|
|
250
251
|
bytes: number;
|
|
251
252
|
};
|
|
252
253
|
|
|
253
|
-
type DeletionRevisionMetadata = {
|
|
254
|
-
version: 1;
|
|
255
|
-
deletions: Array<{ path: string; mtimeMs: number }>;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const DELETION_REVISION_MAX_MTIME_MS = 8_640_000_000_000_000;
|
|
259
|
-
const DELETION_REVISION_LOCK_STALE_MS = 60_000;
|
|
260
|
-
const DELETION_REVISION_LOCK_MAX_WAIT_MS = 120_000;
|
|
261
|
-
|
|
262
|
-
function isValidDeletionRevisionPath(value: unknown): value is string {
|
|
263
|
-
return (
|
|
264
|
-
typeof value === "string" &&
|
|
265
|
-
value.length > 0 &&
|
|
266
|
-
!value.includes("\\") &&
|
|
267
|
-
!value.includes("\0") &&
|
|
268
|
-
!path.posix.isAbsolute(value) &&
|
|
269
|
-
path.posix.normalize(value) === value &&
|
|
270
|
-
value !== "." &&
|
|
271
|
-
value !== ".." &&
|
|
272
|
-
!value.startsWith("../")
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function deletionRevisionPathIdentity(value: string): string {
|
|
277
|
-
return value
|
|
278
|
-
.normalize("NFC")
|
|
279
|
-
.toUpperCase()
|
|
280
|
-
.toLowerCase()
|
|
281
|
-
.replace(/\u00df/g, "ss");
|
|
282
|
-
}
|
|
283
254
|
|
|
284
255
|
export interface ReextractJobRequest {
|
|
285
256
|
memoryId: string;
|
|
@@ -1757,6 +1728,7 @@ export type SealedWriteExtras = Omit<
|
|
|
1757
1728
|
|
|
1758
1729
|
export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
1759
1730
|
private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
|
|
1731
|
+
private readonly deletionRevisionStore: DeletionRevisionStore;
|
|
1760
1732
|
private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
|
|
1761
1733
|
private projectionLedgerLagManager = new ProjectionLedgerLagManager();
|
|
1762
1734
|
private static readonly loadedMemorySnapshots = new WeakMap<MemoryFile, string>();
|
|
@@ -2227,6 +2199,13 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2227
2199
|
hotMemoriesCacheEnabledOverride?: boolean
|
|
2228
2200
|
) {
|
|
2229
2201
|
super();
|
|
2202
|
+
this.deletionRevisionStore = new DeletionRevisionStore({
|
|
2203
|
+
baseDir,
|
|
2204
|
+
deletionRevisionMetadataPath: path.join(baseDir, ".offline-sync", "deletion-revisions.v1.json"),
|
|
2205
|
+
invalidationCommitMetadataPath: path.join(baseDir, ".offline-sync", "invalidation-commits.v1.json"),
|
|
2206
|
+
deletionRevisionLockPath: path.join(baseDir, ".offline-sync", "deletion-revisions.v1.json.lock"),
|
|
2207
|
+
assertManagedStoragePath: (filePath, method) => this.assertManagedStoragePath(filePath, method),
|
|
2208
|
+
});
|
|
2230
2209
|
this.hotMemoriesCacheEnabled =
|
|
2231
2210
|
hotMemoriesCacheEnabledOverride ??
|
|
2232
2211
|
StorageManager.hotMemoriesCacheDefaultByDir.get(baseDir) ??
|
|
@@ -2644,6 +2623,26 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2644
2623
|
private readStorageSecureFile(filePath: string): Promise<string> {
|
|
2645
2624
|
return readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
|
|
2646
2625
|
}
|
|
2626
|
+
async readDependencyPropagationQueueFile(filePath: string): Promise<string> {
|
|
2627
|
+
const target = await resolveSafeStoragePath(
|
|
2628
|
+
this.baseDir,
|
|
2629
|
+
path.relative(
|
|
2630
|
+
this.baseDir,
|
|
2631
|
+
this.assertManagedStoragePath(filePath, "storage.readDependencyPropagationQueueFile"),
|
|
2632
|
+
),
|
|
2633
|
+
);
|
|
2634
|
+
return this.readStorageSecureFile(target);
|
|
2635
|
+
}
|
|
2636
|
+
async writeDependencyPropagationQueueFile(filePath: string, content: string): Promise<void> {
|
|
2637
|
+
const target = await resolveSafeStoragePath(
|
|
2638
|
+
this.baseDir,
|
|
2639
|
+
path.relative(
|
|
2640
|
+
this.baseDir,
|
|
2641
|
+
this.assertManagedStoragePath(filePath, "storage.writeDependencyPropagationQueueFile"),
|
|
2642
|
+
),
|
|
2643
|
+
);
|
|
2644
|
+
return this.writeStorageSecureFile(target, content);
|
|
2645
|
+
}
|
|
2647
2646
|
protected writeStorageSecureFile(filePath: string, content: string | Buffer, forceEncrypt = false): Promise<void> {
|
|
2648
2647
|
const writeKey = this.resolveWriteKey(forceEncrypt);
|
|
2649
2648
|
return writeMaybeEncryptedFile(filePath, content, writeKey, {}, this.baseDir).then(() => {
|
|
@@ -2664,216 +2663,30 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
2664
2663
|
}
|
|
2665
2664
|
return resolved;
|
|
2666
2665
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
let parsed: unknown;
|
|
2670
|
-
try {
|
|
2671
|
-
parsed = JSON.parse(raw);
|
|
2672
|
-
} catch {
|
|
2673
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2674
|
-
}
|
|
2675
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2676
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2677
|
-
}
|
|
2678
|
-
const root = parsed as Record<string, unknown>;
|
|
2679
|
-
if (
|
|
2680
|
-
Object.keys(root).sort().join(",") !== "deletions,version" ||
|
|
2681
|
-
root.version !== 1 ||
|
|
2682
|
-
!Array.isArray(root.deletions)
|
|
2683
|
-
) {
|
|
2684
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2685
|
-
}
|
|
2686
|
-
const revisions = new Map<string, number>();
|
|
2687
|
-
const pathByIdentity = new Map<string, string>();
|
|
2688
|
-
for (const rawEntry of root.deletions) {
|
|
2689
|
-
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
2690
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2691
|
-
}
|
|
2692
|
-
const entry = rawEntry as Record<string, unknown>;
|
|
2693
|
-
if (
|
|
2694
|
-
Object.keys(entry).sort().join(",") !== "mtimeMs,path" ||
|
|
2695
|
-
!isValidDeletionRevisionPath(entry.path) ||
|
|
2696
|
-
typeof entry.mtimeMs !== "number" ||
|
|
2697
|
-
!Number.isFinite(entry.mtimeMs) ||
|
|
2698
|
-
entry.mtimeMs < 0 ||
|
|
2699
|
-
entry.mtimeMs > DELETION_REVISION_MAX_MTIME_MS ||
|
|
2700
|
-
revisions.has(entry.path)
|
|
2701
|
-
) {
|
|
2702
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2703
|
-
}
|
|
2704
|
-
const identity = deletionRevisionPathIdentity(entry.path);
|
|
2705
|
-
if (pathByIdentity.has(identity)) {
|
|
2706
|
-
throw new Error("Deletion revision metadata is invalid.");
|
|
2707
|
-
}
|
|
2708
|
-
pathByIdentity.set(identity, entry.path);
|
|
2709
|
-
revisions.set(entry.path, entry.mtimeMs);
|
|
2710
|
-
}
|
|
2711
|
-
return new Map([...revisions.entries()].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
private async readDeletionRevisionMetadata(): Promise<Map<string, number>> {
|
|
2715
|
-
let raw: string;
|
|
2716
|
-
try {
|
|
2717
|
-
raw = await readFile(this.deletionRevisionMetadataPath, "utf8");
|
|
2718
|
-
} catch (error) {
|
|
2719
|
-
if (isErrnoCode(error, "ENOENT")) return new Map();
|
|
2720
|
-
throw new Error("Deletion revision metadata is unavailable.");
|
|
2721
|
-
}
|
|
2722
|
-
return this.parseDeletionRevisionMetadata(raw);
|
|
2666
|
+
async readDeletionRevisions(): Promise<ReadonlyMap<string, number>> {
|
|
2667
|
+
return this.deletionRevisionStore.readDeletionRevisions();
|
|
2723
2668
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
): Promise<void> {
|
|
2729
|
-
const deletions = [...revisions.entries()]
|
|
2730
|
-
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
2731
|
-
.map(([entryPath, mtimeMs]) => ({ path: entryPath, mtimeMs }));
|
|
2732
|
-
const metadata: DeletionRevisionMetadata = { version: 1, deletions };
|
|
2733
|
-
const temporaryPath = `${this.deletionRevisionMetadataPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
2734
|
-
let handle: FileHandle | null = null;
|
|
2735
|
-
try {
|
|
2736
|
-
handle = await open(temporaryPath, "wx", 0o600);
|
|
2737
|
-
await handle.writeFile(`${JSON.stringify(metadata)}\n`, "utf8");
|
|
2738
|
-
await handle.sync();
|
|
2739
|
-
await handle.close();
|
|
2740
|
-
handle = null;
|
|
2741
|
-
if (!(await lock.refresh())) {
|
|
2742
|
-
throw new Error("Deletion revision metadata lock was lost.");
|
|
2743
|
-
}
|
|
2744
|
-
await rename(temporaryPath, this.deletionRevisionMetadataPath);
|
|
2745
|
-
} finally {
|
|
2746
|
-
if (handle !== null) await handle.close().catch(() => undefined);
|
|
2747
|
-
await unlink(temporaryPath).catch((error: unknown) => {
|
|
2748
|
-
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
2749
|
-
});
|
|
2750
|
-
}
|
|
2669
|
+
async hasCommittedInvalidation(
|
|
2670
|
+
memory: Pick<MemoryFile, "content" | "frontmatter">,
|
|
2671
|
+
): Promise<boolean> {
|
|
2672
|
+
return this.deletionRevisionStore.hasCommittedInvalidation(memory);
|
|
2751
2673
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
return withHeldFileLock(
|
|
2755
|
-
this.deletionRevisionLockPath,
|
|
2756
|
-
{
|
|
2757
|
-
staleMs: DELETION_REVISION_LOCK_STALE_MS,
|
|
2758
|
-
maxWaitMs: DELETION_REVISION_LOCK_MAX_WAIT_MS,
|
|
2759
|
-
},
|
|
2760
|
-
async (acquired, lock) => {
|
|
2761
|
-
if (!acquired) throw new Error("Deletion revision metadata lock is unavailable.");
|
|
2762
|
-
return task(lock);
|
|
2763
|
-
}
|
|
2764
|
-
);
|
|
2674
|
+
private async recordCommittedInvalidation(memory: MemoryFile): Promise<void> {
|
|
2675
|
+
return this.deletionRevisionStore.recordCommittedInvalidation(memory);
|
|
2765
2676
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2677
|
+
private async clearCommittedInvalidation(
|
|
2678
|
+
memory: Pick<MemoryFile, "content" | "frontmatter">,
|
|
2679
|
+
): Promise<void> {
|
|
2680
|
+
return this.deletionRevisionStore.clearCommittedInvalidation(memory);
|
|
2769
2681
|
}
|
|
2770
|
-
|
|
2771
2682
|
async recordReplicatedDeletionRevision(filePath: string, mtimeMs: number): Promise<void> {
|
|
2772
|
-
|
|
2773
|
-
if (
|
|
2774
|
-
typeof mtimeMs !== "number" ||
|
|
2775
|
-
!Number.isFinite(mtimeMs) ||
|
|
2776
|
-
mtimeMs < 0 ||
|
|
2777
|
-
mtimeMs > DELETION_REVISION_MAX_MTIME_MS
|
|
2778
|
-
) {
|
|
2779
|
-
throw new Error("Deletion revision timestamp is invalid.");
|
|
2780
|
-
}
|
|
2781
|
-
const relativePath = path.relative(this.baseDir, target).split(path.sep).join("/");
|
|
2782
|
-
if (!isValidDeletionRevisionPath(relativePath)) {
|
|
2783
|
-
throw new Error("Deletion revision path is invalid.");
|
|
2784
|
-
}
|
|
2785
|
-
await this.withDeletionRevisionLock(async (lock) => {
|
|
2786
|
-
const revisions = await this.readDeletionRevisionMetadata();
|
|
2787
|
-
const identity = deletionRevisionPathIdentity(relativePath);
|
|
2788
|
-
let existingPath: string | undefined;
|
|
2789
|
-
for (const candidatePath of revisions.keys()) {
|
|
2790
|
-
if (deletionRevisionPathIdentity(candidatePath) === identity) {
|
|
2791
|
-
existingPath = candidatePath;
|
|
2792
|
-
break;
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
const existingMtimeMs = existingPath === undefined ? undefined : revisions.get(existingPath);
|
|
2796
|
-
if (existingMtimeMs !== undefined && existingMtimeMs >= mtimeMs) return;
|
|
2797
|
-
|
|
2798
|
-
const updated = new Map(revisions);
|
|
2799
|
-
if (existingPath !== undefined) updated.delete(existingPath);
|
|
2800
|
-
updated.set(relativePath, mtimeMs);
|
|
2801
|
-
await this.writeDeletionRevisionMetadata(updated, lock);
|
|
2802
|
-
});
|
|
2683
|
+
return this.deletionRevisionStore.recordReplicatedDeletionRevision(filePath, mtimeMs);
|
|
2803
2684
|
}
|
|
2804
|
-
|
|
2805
2685
|
protected async writeManagedStorageFile(filePath: string, write: () => Promise<void>): Promise<void> {
|
|
2806
|
-
|
|
2807
|
-
const relativePath = path.relative(this.baseDir, target).split(path.sep).join("/");
|
|
2808
|
-
if (!isValidDeletionRevisionPath(relativePath)) {
|
|
2809
|
-
throw new Error("Deletion revision path is invalid.");
|
|
2810
|
-
}
|
|
2811
|
-
await this.withDeletionRevisionLock(async (lock) => {
|
|
2812
|
-
const before = await this.readDeletionRevisionMetadata();
|
|
2813
|
-
const identity = deletionRevisionPathIdentity(relativePath);
|
|
2814
|
-
const existingPath = [...before.keys()].find(
|
|
2815
|
-
(candidatePath) => deletionRevisionPathIdentity(candidatePath) === identity
|
|
2816
|
-
);
|
|
2817
|
-
await write();
|
|
2818
|
-
if (existingPath === undefined) return;
|
|
2819
|
-
const updated = new Map(before);
|
|
2820
|
-
updated.delete(existingPath);
|
|
2821
|
-
await this.writeDeletionRevisionMetadata(updated, lock);
|
|
2822
|
-
});
|
|
2686
|
+
return this.deletionRevisionStore.writeManagedStorageFile(filePath, write);
|
|
2823
2687
|
}
|
|
2824
|
-
|
|
2825
2688
|
protected async deleteManagedStorageFile(filePath: string, deletionMtimeMs?: number | null): Promise<boolean> {
|
|
2826
|
-
|
|
2827
|
-
if (
|
|
2828
|
-
deletionMtimeMs !== undefined &&
|
|
2829
|
-
deletionMtimeMs !== null &&
|
|
2830
|
-
(typeof deletionMtimeMs !== "number" ||
|
|
2831
|
-
!Number.isFinite(deletionMtimeMs) ||
|
|
2832
|
-
deletionMtimeMs < 0 ||
|
|
2833
|
-
deletionMtimeMs > DELETION_REVISION_MAX_MTIME_MS)
|
|
2834
|
-
) {
|
|
2835
|
-
throw new Error("Deletion revision timestamp is invalid.");
|
|
2836
|
-
}
|
|
2837
|
-
return this.withDeletionRevisionLock(async (lock) => {
|
|
2838
|
-
try {
|
|
2839
|
-
await lstat(target);
|
|
2840
|
-
} catch (error) {
|
|
2841
|
-
if (isErrnoCode(error, "ENOENT")) return false;
|
|
2842
|
-
throw error;
|
|
2843
|
-
}
|
|
2844
|
-
const relativePath = path.relative(this.baseDir, target).split(path.sep).join("/");
|
|
2845
|
-
if (!isValidDeletionRevisionPath(relativePath)) {
|
|
2846
|
-
throw new Error("Deletion revision path is invalid.");
|
|
2847
|
-
}
|
|
2848
|
-
const revision = deletionMtimeMs === null ? undefined : (deletionMtimeMs ?? Date.now());
|
|
2849
|
-
const before = await this.readDeletionRevisionMetadata();
|
|
2850
|
-
const identity = deletionRevisionPathIdentity(relativePath);
|
|
2851
|
-
let existingPath: string | undefined;
|
|
2852
|
-
for (const candidatePath of before.keys()) {
|
|
2853
|
-
if (deletionRevisionPathIdentity(candidatePath) === identity) {
|
|
2854
|
-
existingPath = candidatePath;
|
|
2855
|
-
break;
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
const existing = existingPath === undefined ? undefined : before.get(existingPath);
|
|
2859
|
-
const changed =
|
|
2860
|
-
(existingPath !== undefined && existingPath !== relativePath) ||
|
|
2861
|
-
(revision === undefined ? existing !== undefined : existing !== revision);
|
|
2862
|
-
if (changed) {
|
|
2863
|
-
const updated = new Map(before);
|
|
2864
|
-
if (existingPath !== undefined) updated.delete(existingPath);
|
|
2865
|
-
if (revision !== undefined) updated.set(relativePath, revision);
|
|
2866
|
-
await this.writeDeletionRevisionMetadata(updated, lock);
|
|
2867
|
-
}
|
|
2868
|
-
try {
|
|
2869
|
-
await unlink(target);
|
|
2870
|
-
return true;
|
|
2871
|
-
} catch (error) {
|
|
2872
|
-
if (changed) await this.writeDeletionRevisionMetadata(before, lock);
|
|
2873
|
-
if (isErrnoCode(error, "ENOENT")) return false;
|
|
2874
|
-
throw error;
|
|
2875
|
-
}
|
|
2876
|
-
});
|
|
2689
|
+
return this.deletionRevisionStore.deleteManagedStorageFile(filePath, deletionMtimeMs);
|
|
2877
2690
|
}
|
|
2878
2691
|
|
|
2879
2692
|
async readOfflineSyncFile(filePath: string): Promise<Buffer> {
|
|
@@ -3159,12 +2972,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3159
2972
|
private get offlineSyncDigestCachePath(): string {
|
|
3160
2973
|
return path.join(this.baseDir, ".offline-sync", "digest-cache.v1.json");
|
|
3161
2974
|
}
|
|
3162
|
-
private get deletionRevisionMetadataPath(): string {
|
|
3163
|
-
return path.join(this.baseDir, ".offline-sync", "deletion-revisions.v1.json");
|
|
3164
|
-
}
|
|
3165
|
-
private get deletionRevisionLockPath(): string {
|
|
3166
|
-
return `${this.deletionRevisionMetadataPath}.lock`;
|
|
3167
|
-
}
|
|
3168
2975
|
private get entitySynthesisQueuePath(): string {
|
|
3169
2976
|
return path.join(this.stateDir, "entity-synthesis-queue.json");
|
|
3170
2977
|
}
|
|
@@ -3279,8 +3086,8 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3279
3086
|
rawContent: string;
|
|
3280
3087
|
entityRef?: string;
|
|
3281
3088
|
supersessionKey?: string;
|
|
3089
|
+
operationKey?: string;
|
|
3282
3090
|
createdAt?: string;
|
|
3283
|
-
/** Canonical contentHash from the retired memory's frontmatter (#1579). */
|
|
3284
3091
|
contentHash?: string;
|
|
3285
3092
|
}): Promise<string | null> {
|
|
3286
3093
|
if (!this.tombstonesConfig.enabled) return null;
|
|
@@ -3305,6 +3112,23 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3305
3112
|
return null;
|
|
3306
3113
|
}
|
|
3307
3114
|
}
|
|
3115
|
+
async hasExactTombstone(input: {
|
|
3116
|
+
sourceMemoryId: string;
|
|
3117
|
+
contentHash?: string;
|
|
3118
|
+
entityRef?: string;
|
|
3119
|
+
supersessionKey?: string;
|
|
3120
|
+
createdAt?: string;
|
|
3121
|
+
operationKey?: string;
|
|
3122
|
+
}): Promise<boolean> {
|
|
3123
|
+
if (!this.tombstonesConfig.enabled) return false;
|
|
3124
|
+
try {
|
|
3125
|
+
const store = await this.getTombstoneStore();
|
|
3126
|
+
return await store.hasExactEntry(input);
|
|
3127
|
+
} catch (err) {
|
|
3128
|
+
log.warn(`exact tombstone proof failed (memory=${input.sourceMemoryId}): ${err}`);
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3308
3132
|
|
|
3309
3133
|
/**
|
|
3310
3134
|
* Revoke a tombstone (re-allow the content). Used by the review-queue
|
|
@@ -4597,22 +4421,17 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4597
4421
|
clearInFlightReads();
|
|
4598
4422
|
StorageManager.questionsCache.clear();
|
|
4599
4423
|
StorageManager.coldMemoriesCache.clear(); // also wipe the cold-scan TTL cache
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4424
|
+
StorageManager.artifactWriteVersionByDir.clear();
|
|
4425
|
+
StorageManager.memoryStatusVersionByDir.clear();
|
|
4426
|
+
StorageManager.memoryCorpusVersionByDir.clear();
|
|
4427
|
+
StorageManager.entityMutationVersionByDir.clear();
|
|
4428
|
+
StorageManager.coldWriteVersionByDir.clear();
|
|
4603
4429
|
clearMemoryCache();
|
|
4604
|
-
// Reset the hot-cache config statics to construction defaults so each test
|
|
4605
|
-
// starts from a clean slate (issue #1902). resetStaticCaches() runs before
|
|
4606
|
-
// every contract test; without this the per-dir maps and the first-writer
|
|
4607
|
-
// process-wide seed would leak across tests.
|
|
4608
4430
|
StorageManager.hotMemoriesCacheDefaultByDir.clear();
|
|
4609
4431
|
StorageManager.hotMemoriesCacheTtlByDir.clear();
|
|
4610
4432
|
StorageManager.hotMemoriesCacheDefault = true;
|
|
4611
4433
|
StorageManager.hotMemoriesCacheTtlMs = 60_000;
|
|
4612
4434
|
StorageManager.hotMemoriesCacheProcessDefaultSeeded = false;
|
|
4613
|
-
// Reset the #1904 scope-invalidation gate so a legacy-mode (=false) test
|
|
4614
|
-
// dir cannot leak its setting into a later test constructing over a reused
|
|
4615
|
-
// temp path.
|
|
4616
4435
|
StorageManager.scopedCacheInvalidationByDir.clear();
|
|
4617
4436
|
}
|
|
4618
4437
|
|
|
@@ -5047,15 +4866,18 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5047
4866
|
);
|
|
5048
4867
|
}
|
|
5049
4868
|
} catch (err) {
|
|
5050
|
-
//
|
|
5051
|
-
//
|
|
4869
|
+
// A bad archive member must not hide readable siblings. A locked
|
|
4870
|
+
// secure store remains authoritative even for one member.
|
|
5052
4871
|
if (err instanceof SecureStoreLockedError) throw err;
|
|
5053
|
-
|
|
4872
|
+
if (!isErrnoCode(err, "ENOENT")) {
|
|
4873
|
+
log.warn(`skipping unreadable archived memory ${fullPath}: ${err}`);
|
|
4874
|
+
}
|
|
5054
4875
|
}
|
|
5055
4876
|
}
|
|
5056
4877
|
}
|
|
5057
|
-
} catch {
|
|
5058
|
-
|
|
4878
|
+
} catch (error) {
|
|
4879
|
+
if (error instanceof SecureStoreLockedError) throw error;
|
|
4880
|
+
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
5059
4881
|
}
|
|
5060
4882
|
};
|
|
5061
4883
|
|
|
@@ -5383,39 +5205,50 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5383
5205
|
return null;
|
|
5384
5206
|
}
|
|
5385
5207
|
|
|
5386
|
-
async invalidateMemory(
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5208
|
+
async invalidateMemory(
|
|
5209
|
+
id: string,
|
|
5210
|
+
expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter"> & Partial<Pick<MemoryFile, "path">>,
|
|
5211
|
+
options?: { recordCommitProof?: boolean },
|
|
5212
|
+
): Promise<boolean> {
|
|
5213
|
+
const memory = expectedSnapshot?.path
|
|
5214
|
+
? await this.readMemoryByPath(expectedSnapshot.path)
|
|
5215
|
+
: (await this.readAllMemories()).find((candidate) => candidate.frontmatter.id === id) ?? null;
|
|
5216
|
+
if (!memory || memory.frontmatter.id !== id) return false;
|
|
5390
5217
|
|
|
5391
5218
|
return await this.runTombstoneBlockedInvalidation(memory, async (current, rebuildMarker, markDurable) => {
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5219
|
+
if (
|
|
5220
|
+
expectedSnapshot &&
|
|
5221
|
+
invalidationCommitFingerprint(current) !== invalidationCommitFingerprint(expectedSnapshot)
|
|
5222
|
+
) {
|
|
5223
|
+
return false;
|
|
5224
|
+
}
|
|
5225
|
+
return runCommittedInvalidation({
|
|
5226
|
+
memoryId: id,
|
|
5227
|
+
recordProof: options?.recordCommitProof === true ? () => this.recordCommittedInvalidation(current) : undefined,
|
|
5228
|
+
clearProof: options?.recordCommitProof === true ? () => this.clearCommittedInvalidation(current) : undefined,
|
|
5229
|
+
deleteMemory: () => archive.recordArchiveDelete(this.baseDir, current.path, () =>
|
|
5230
|
+
this.deleteManagedStorageFile(current.path)),
|
|
5231
|
+
afterDelete: async () => {
|
|
5232
|
+
markDurable();
|
|
5233
|
+
markProjectedMemoryPathInvalid(this.baseDir, id);
|
|
5234
|
+
this.invalidateAllMemoriesCache();
|
|
5235
|
+
if (this.isColdOrArchiveTierPath(current.path)) this.invalidateColdMemoriesCache();
|
|
5236
|
+
await this.rebuildTombstoneBlockedCaptureAfterInvalidation(rebuildMarker);
|
|
5237
|
+
this.bumpMemoryStatusVersion();
|
|
5238
|
+
log.debug(`invalidated memory ${id}`);
|
|
5239
|
+
},
|
|
5240
|
+
});
|
|
5402
5241
|
});
|
|
5403
5242
|
}
|
|
5404
5243
|
|
|
5405
|
-
async
|
|
5406
|
-
|
|
5244
|
+
private async updateMemoryFromCurrent(
|
|
5245
|
+
memory: MemoryFile,
|
|
5407
5246
|
newContent: string,
|
|
5408
|
-
options?: { supersedes?: string; lineage?: string[]; actor?: string; sourceConnector?: string }
|
|
5247
|
+
options?: { supersedes?: string; lineage?: string[]; actor?: string; sourceConnector?: string },
|
|
5409
5248
|
): Promise<boolean> {
|
|
5410
|
-
const memories = await this.readAllMemories();
|
|
5411
|
-
const memory = memories.find((m) => m.frontmatter.id === id);
|
|
5412
|
-
if (!memory) return false;
|
|
5413
|
-
|
|
5414
5249
|
const mergedLineage = [...(memory.frontmatter.lineage ?? []), ...(options?.lineage ?? [])].filter(
|
|
5415
5250
|
(v, i, a) => a.indexOf(v) === i
|
|
5416
|
-
);
|
|
5417
|
-
|
|
5418
|
-
// Whole-record rewrite — inherited-entityRef rule (issue #2213).
|
|
5251
|
+
);
|
|
5419
5252
|
const refIdsAtWrite = this.currentHistoricalIds();
|
|
5420
5253
|
const updated: MemoryFrontmatter = entityRefs.canonicalizeEntityRefOption(
|
|
5421
5254
|
{
|
|
@@ -5431,11 +5264,12 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5431
5264
|
);
|
|
5432
5265
|
const sanitized = sanitizeMemoryContent(newContent);
|
|
5433
5266
|
if (!sanitized.clean) {
|
|
5434
|
-
log.warn(`updated memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
|
|
5267
|
+
log.warn(`updated memory content sanitized for ${memory.frontmatter.id}; violations=${sanitized.violations.join(", ")}`);
|
|
5435
5268
|
}
|
|
5436
5269
|
const fileContent = `${serializeFrontmatter(updated)}\n\n${sanitized.text}\n`;
|
|
5437
5270
|
await this.writeTombstoneBlockedUpdate(memory, fileContent, updated, sanitized.text, async () => {
|
|
5438
5271
|
this.invalidateAllMemoriesCache();
|
|
5272
|
+
if (this.isColdOrArchiveTierPath(memory.path)) this.invalidateColdMemoriesCache();
|
|
5439
5273
|
});
|
|
5440
5274
|
if (typeof memory.frontmatter.entityRef === "string") {
|
|
5441
5275
|
await this.entityRefRepair.repair(
|
|
@@ -5449,7 +5283,7 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5449
5283
|
}
|
|
5450
5284
|
await this.patchHotMemoriesCache({ addedPath: memory.path });
|
|
5451
5285
|
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.updateMemory", {
|
|
5452
|
-
memoryId: id,
|
|
5286
|
+
memoryId: memory.frontmatter.id,
|
|
5453
5287
|
eventType: "updated",
|
|
5454
5288
|
timestamp: updated.updated,
|
|
5455
5289
|
actor: options?.actor ?? "storage.updateMemory",
|
|
@@ -5460,10 +5294,48 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5460
5294
|
...(updated.lineage ?? []).filter(Boolean),
|
|
5461
5295
|
],
|
|
5462
5296
|
});
|
|
5463
|
-
log.debug(`updated memory ${id}`);
|
|
5297
|
+
log.debug(`updated memory ${memory.frontmatter.id}`);
|
|
5464
5298
|
return true;
|
|
5465
5299
|
}
|
|
5466
5300
|
|
|
5301
|
+
async updateMemory(
|
|
5302
|
+
id: string,
|
|
5303
|
+
newContent: string,
|
|
5304
|
+
options?: { supersedes?: string; lineage?: string[]; actor?: string; sourceConnector?: string }
|
|
5305
|
+
): Promise<boolean> {
|
|
5306
|
+
const memory = (await this.readAllMemories()).find((candidate) => candidate.frontmatter.id === id);
|
|
5307
|
+
if (!memory) return false;
|
|
5308
|
+
return await this.updateMemoryFromCurrent(memory, newContent, options);
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
async updateMemoryIfUnchanged(
|
|
5312
|
+
expected: MemoryFile,
|
|
5313
|
+
newContent: string,
|
|
5314
|
+
options?: { supersedes?: string; lineage?: string[]; actor?: string; sourceConnector?: string }
|
|
5315
|
+
): Promise<boolean> {
|
|
5316
|
+
const sanitized = sanitizeMemoryContent(newContent);
|
|
5317
|
+
const oldIdentity = buildExplicitCaptureDedupKey(
|
|
5318
|
+
expected.content,
|
|
5319
|
+
expected.frontmatter.category,
|
|
5320
|
+
expected.frontmatter.sourceConnector
|
|
5321
|
+
);
|
|
5322
|
+
const newIdentity = buildExplicitCaptureDedupKey(
|
|
5323
|
+
sanitized.text,
|
|
5324
|
+
expected.frontmatter.category,
|
|
5325
|
+
expected.frontmatter.sourceConnector
|
|
5326
|
+
);
|
|
5327
|
+
return await this.withTombstoneBlockedCaptureWriteLock(
|
|
5328
|
+
async () => {
|
|
5329
|
+
const current = await this.readMemoryByPath(expected.path);
|
|
5330
|
+
if (!current || current.frontmatter.id !== expected.frontmatter.id) return false;
|
|
5331
|
+
if (invalidationCommitFingerprint(current) !== invalidationCommitFingerprint(expected)) return false;
|
|
5332
|
+
return await this.updateMemoryFromCurrent(current, newContent, options);
|
|
5333
|
+
},
|
|
5334
|
+
[buildCapturePathLockIdentity(expected.path), oldIdentity, newIdentity]
|
|
5335
|
+
);
|
|
5336
|
+
}
|
|
5337
|
+
|
|
5338
|
+
|
|
5467
5339
|
/**
|
|
5468
5340
|
* Update frontmatter fields without changing memory content.
|
|
5469
5341
|
* Returns false when the memory is not found.
|
|
@@ -5537,6 +5409,22 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
5537
5409
|
}
|
|
5538
5410
|
return true;
|
|
5539
5411
|
}
|
|
5412
|
+
/**
|
|
5413
|
+
* Update frontmatter only when the persisted memory still matches the
|
|
5414
|
+
* expected semantic snapshot.
|
|
5415
|
+
*/
|
|
5416
|
+
async writeMemoryFrontmatterIfUnchanged(
|
|
5417
|
+
expected: MemoryFile,
|
|
5418
|
+
patch: Partial<MemoryFrontmatter>,
|
|
5419
|
+
lifecycle?: MemoryLifecycleEventWriteOptions
|
|
5420
|
+
): Promise<boolean> {
|
|
5421
|
+
return await this.withTombstoneBlockedMemoryPathLock(expected.path, async (current) => {
|
|
5422
|
+
if (!current) return false;
|
|
5423
|
+
if (invalidationCommitFingerprint(current) !== invalidationCommitFingerprint(expected)) return false;
|
|
5424
|
+
return await this.writeMemoryFrontmatter(current, patch, lifecycle);
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
5427
|
+
|
|
5540
5428
|
|
|
5541
5429
|
/**
|
|
5542
5430
|
* Update frontmatter by memory ID.
|
|
@@ -7008,42 +6896,118 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7008
6896
|
.sort((a, b) => (a.frontmatter.chunkIndex ?? 0) - (b.frontmatter.chunkIndex ?? 0));
|
|
7009
6897
|
}
|
|
7010
6898
|
|
|
7011
|
-
async supersedeMemory(
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
6899
|
+
async supersedeMemory(
|
|
6900
|
+
oldMemoryId: string,
|
|
6901
|
+
newMemoryId: string,
|
|
6902
|
+
reason: string,
|
|
6903
|
+
supersessionMetadata?: Pick<MemoryFrontmatter, "supersessionCause" | "invalidatedBy">,
|
|
6904
|
+
options: {
|
|
6905
|
+
requireActive?: boolean;
|
|
6906
|
+
acceptExactReplay?: boolean;
|
|
6907
|
+
expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter"> & Partial<Pick<MemoryFile, "path">>;
|
|
6908
|
+
} = {},
|
|
6909
|
+
): Promise<boolean> {
|
|
6910
|
+
const matchesSupersession = (memory: MemoryFile): boolean =>
|
|
6911
|
+
memory.frontmatter.status === "superseded" &&
|
|
6912
|
+
memory.frontmatter.supersededBy === newMemoryId &&
|
|
6913
|
+
(supersessionMetadata === undefined ||
|
|
6914
|
+
(memory.frontmatter.supersessionCause === supersessionMetadata.supersessionCause &&
|
|
6915
|
+
memory.frontmatter.invalidatedBy === supersessionMetadata.invalidatedBy));
|
|
6916
|
+
const isExactReplay = (memory: MemoryFile): boolean =>
|
|
6917
|
+
options.acceptExactReplay === true && matchesSupersession(memory);
|
|
6918
|
+
let oldMemory: MemoryFile | undefined;
|
|
6919
|
+
if (options.expectedSnapshot?.path) {
|
|
6920
|
+
const selected = await this.readMemoryByPath(options.expectedSnapshot.path);
|
|
6921
|
+
if (!selected || selected.frontmatter.id !== oldMemoryId) {
|
|
6922
|
+
return false;
|
|
6923
|
+
}
|
|
6924
|
+
if (
|
|
6925
|
+
invalidationCommitFingerprint(selected) !== invalidationCommitFingerprint(options.expectedSnapshot) &&
|
|
6926
|
+
!isExactReplay(selected)
|
|
6927
|
+
) {
|
|
6928
|
+
return false;
|
|
6929
|
+
}
|
|
6930
|
+
oldMemory = selected;
|
|
6931
|
+
} else {
|
|
6932
|
+
const memories = [
|
|
6933
|
+
...(await this.readAllMemories()),
|
|
6934
|
+
...(await this.readAllColdMemories()),
|
|
6935
|
+
...(await this.readArchivedMemories()),
|
|
6936
|
+
];
|
|
6937
|
+
const candidates = memories.filter((memory) => memory.frontmatter.id === oldMemoryId);
|
|
6938
|
+
oldMemory =
|
|
6939
|
+
candidates.find(
|
|
6940
|
+
(memory) =>
|
|
6941
|
+
inferMemoryStatus(memory.frontmatter, toMemoryPathRel(this.baseDir, memory.path)) === "active",
|
|
6942
|
+
) ??
|
|
6943
|
+
candidates.find(isExactReplay) ??
|
|
6944
|
+
candidates[0];
|
|
6945
|
+
if (
|
|
6946
|
+
oldMemory &&
|
|
6947
|
+
options.expectedSnapshot &&
|
|
6948
|
+
invalidationCommitFingerprint(oldMemory) !== invalidationCommitFingerprint(options.expectedSnapshot) &&
|
|
6949
|
+
!isExactReplay(oldMemory)
|
|
6950
|
+
) {
|
|
6951
|
+
return false;
|
|
6952
|
+
}
|
|
7024
6953
|
}
|
|
7025
6954
|
if (!oldMemory) return false;
|
|
7026
|
-
|
|
7027
|
-
|
|
6955
|
+
const requireActiveAtWrite =
|
|
6956
|
+
options.requireActive === true ||
|
|
6957
|
+
inferMemoryStatus(oldMemory.frontmatter, toMemoryPathRel(this.baseDir, oldMemory.path)) === "active";
|
|
6958
|
+
|
|
6959
|
+
const operationId = createHash("sha256")
|
|
6960
|
+
.update(
|
|
6961
|
+
JSON.stringify({
|
|
6962
|
+
oldMemoryId,
|
|
6963
|
+
newMemoryId,
|
|
6964
|
+
reason,
|
|
6965
|
+
supersessionMetadata: supersessionMetadata ?? null,
|
|
6966
|
+
}),
|
|
6967
|
+
)
|
|
6968
|
+
.digest("hex");
|
|
6969
|
+
const initialNow = new Date().toISOString();
|
|
7028
6970
|
let currentBefore = oldMemory;
|
|
7029
6971
|
let updatedFm = oldMemory.frontmatter;
|
|
6972
|
+
let exactReplay = false;
|
|
7030
6973
|
|
|
7031
|
-
|
|
6974
|
+
try {
|
|
6975
|
+
const written = await this.withTombstoneBlockedMemoryPathLock(oldMemory.path, async (current) => {
|
|
7032
6976
|
if (current?.frontmatter.id !== oldMemoryId) return false;
|
|
6977
|
+
if (matchesSupersession(current)) {
|
|
6978
|
+
currentBefore = current;
|
|
6979
|
+
updatedFm = current.frontmatter;
|
|
6980
|
+
return options.acceptExactReplay === true ? "exact-replay" : false;
|
|
6981
|
+
}
|
|
6982
|
+
if (
|
|
6983
|
+
options.expectedSnapshot &&
|
|
6984
|
+
invalidationCommitFingerprint(current) !== invalidationCommitFingerprint(options.expectedSnapshot)
|
|
6985
|
+
) {
|
|
6986
|
+
return false;
|
|
6987
|
+
}
|
|
7033
6988
|
if (
|
|
7034
6989
|
current.frontmatter.supersededBy !== undefined ||
|
|
7035
|
-
|
|
6990
|
+
(requireActiveAtWrite &&
|
|
6991
|
+
inferMemoryStatus(current.frontmatter, toMemoryPathRel(this.baseDir, current.path)) !== "active")
|
|
7036
6992
|
) {
|
|
7037
6993
|
return false;
|
|
7038
6994
|
}
|
|
7039
6995
|
currentBefore = current;
|
|
7040
6996
|
const refIdsAtWrite = this.currentHistoricalIds();
|
|
7041
6997
|
updatedFm = entityRefs.canonicalizeEntityRefOption(
|
|
7042
|
-
{
|
|
6998
|
+
{
|
|
6999
|
+
...current.frontmatter,
|
|
7000
|
+
...supersessionMetadata,
|
|
7001
|
+
status: "superseded",
|
|
7002
|
+
supersededBy: newMemoryId,
|
|
7003
|
+
supersededAt: initialNow,
|
|
7004
|
+
updated: initialNow,
|
|
7005
|
+
},
|
|
7043
7006
|
refIdsAtWrite
|
|
7044
7007
|
);
|
|
7045
7008
|
const fileContent = `${serializeFrontmatter(updatedFm)}\n\n${current.content}\n`;
|
|
7046
7009
|
await this.writeTombstoneBlockedFrontmatter(current, fileContent, updatedFm);
|
|
7010
|
+
if (this.isColdOrArchiveTierPath(current.path)) this.invalidateColdMemoriesCache();
|
|
7047
7011
|
if (typeof current.frontmatter.entityRef === "string") {
|
|
7048
7012
|
await this.entityRefRepair.repair(
|
|
7049
7013
|
current.path,
|
|
@@ -7055,49 +7019,92 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7055
7019
|
);
|
|
7056
7020
|
}
|
|
7057
7021
|
return true;
|
|
7058
|
-
})
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7022
|
+
});
|
|
7023
|
+
if (!written) return false;
|
|
7024
|
+
exactReplay = written === "exact-replay";
|
|
7025
|
+
if (exactReplay && typeof updatedFm.entityRef === "string") {
|
|
7026
|
+
await this.entityRefRepair.repair(
|
|
7027
|
+
currentBefore.path,
|
|
7028
|
+
updatedFm,
|
|
7029
|
+
updatedFm.entityRef,
|
|
7030
|
+
this.currentHistoricalIds(),
|
|
7031
|
+
currentBefore.content,
|
|
7032
|
+
{ onFailRestore: currentBefore },
|
|
7033
|
+
);
|
|
7034
|
+
}
|
|
7035
|
+
const supersededAt = updatedFm.supersededAt ?? initialNow;
|
|
7036
|
+
const beforeState = this.summarizeLifecycleState(
|
|
7037
|
+
currentBefore.frontmatter,
|
|
7038
|
+
currentBefore.path,
|
|
7039
|
+
);
|
|
7040
|
+
const afterState = this.summarizeLifecycleState(updatedFm, currentBefore.path);
|
|
7041
|
+
const sideEffectsComplete = await runSupersessionSideEffects({
|
|
7042
|
+
oldMemoryId,
|
|
7043
|
+
newMemoryId,
|
|
7044
|
+
reason,
|
|
7045
|
+
now: supersededAt,
|
|
7046
|
+
operationId,
|
|
7047
|
+
exactReplay,
|
|
7048
|
+
currentBefore,
|
|
7049
|
+
updatedFm,
|
|
7050
|
+
citationTemplate: this.citationTemplate,
|
|
7051
|
+
correctionsDir: this.correctionsDir,
|
|
7052
|
+
readMemoryByPath: (filePath) => this.readMemoryByPath(filePath),
|
|
7053
|
+
isColdOrArchiveTierPath: (memoryPath) => this.isColdOrArchiveTierPath(memoryPath),
|
|
7054
|
+
invalidateColdMemoriesCache: () => this.invalidateColdMemoriesCache(),
|
|
7055
|
+
invalidateAllMemoriesCache: () => this.invalidateAllMemoriesCache(),
|
|
7056
|
+
bumpMemoryCorpusVersion: () => this.bumpMemoryCorpusVersion(),
|
|
7057
|
+
appendLifecycleEvent: async () => {
|
|
7058
|
+
let lifecycleEvents: MemoryLifecycleEvent[] = [];
|
|
7059
|
+
try {
|
|
7060
|
+
lifecycleEvents = await this.getMemoryTimeline(oldMemoryId, 200);
|
|
7061
|
+
} catch (error) {
|
|
7062
|
+
log.warn(`supersession lifecycle replay check failed for ${oldMemoryId}: ${error}`);
|
|
7063
|
+
}
|
|
7064
|
+
if (
|
|
7065
|
+
lifecycleEvents.some(
|
|
7066
|
+
(event) =>
|
|
7067
|
+
event.memoryId === oldMemoryId &&
|
|
7068
|
+
event.eventType === "superseded" &&
|
|
7069
|
+
(event.correlationId === operationId ||
|
|
7070
|
+
(event.correlationId === undefined &&
|
|
7071
|
+
event.reasonCode === reason &&
|
|
7072
|
+
event.timestamp === supersededAt &&
|
|
7073
|
+
event.relatedMemoryIds?.includes(newMemoryId))),
|
|
7074
|
+
)
|
|
7075
|
+
) {
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.supersedeMemory", {
|
|
7079
|
+
memoryId: oldMemoryId,
|
|
7080
|
+
eventType: "superseded",
|
|
7081
|
+
timestamp: supersededAt,
|
|
7082
|
+
actor: "storage.supersedeMemory",
|
|
7083
|
+
reasonCode: reason,
|
|
7084
|
+
before: exactReplay ? { ...beforeState, status: "active" } : beforeState,
|
|
7085
|
+
after: afterState,
|
|
7086
|
+
relatedMemoryIds: [newMemoryId],
|
|
7087
|
+
correlationId: operationId,
|
|
7088
|
+
});
|
|
7089
|
+
},
|
|
7090
|
+
bumpMemoryStatusVersion: () => this.bumpMemoryStatusVersion(),
|
|
7091
|
+
hasExactTombstone: (input) => this.hasExactTombstone(input),
|
|
7092
|
+
appendTombstone: (input) => this.appendTombstone(input),
|
|
7093
|
+
writeSealedMemory: (envelope, extras) => this.writeSealedMemory(envelope, extras),
|
|
7094
|
+
});
|
|
7095
|
+
return sideEffectsComplete || options.acceptExactReplay !== true;
|
|
7086
7096
|
|
|
7087
|
-
|
|
7097
|
+
} catch (err) {
|
|
7098
|
+
log.error(`failed to supersede memory ${oldMemoryId}:`, err);
|
|
7099
|
+
return false;
|
|
7100
|
+
}
|
|
7088
7101
|
}
|
|
7089
7102
|
|
|
7090
|
-
// ---------------------------------------------------------------------------
|
|
7091
|
-
// Memory Summarization (Phase 4A)
|
|
7092
|
-
// ---------------------------------------------------------------------------
|
|
7093
7103
|
|
|
7094
7104
|
private get summariesDir(): string {
|
|
7095
7105
|
return path.join(this.baseDir, "summaries");
|
|
7096
7106
|
}
|
|
7097
7107
|
|
|
7098
|
-
/**
|
|
7099
|
-
* Write a memory summary.
|
|
7100
|
-
*/
|
|
7101
7108
|
async writeSummary(summary: MemorySummary): Promise<void> {
|
|
7102
7109
|
await mkdir(this.summariesDir, { recursive: true });
|
|
7103
7110
|
const filePath = path.join(this.summariesDir, `${summary.id}.json`);
|
|
@@ -7105,9 +7112,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7105
7112
|
log.debug(`wrote summary ${summary.id}`);
|
|
7106
7113
|
}
|
|
7107
7114
|
|
|
7108
|
-
/**
|
|
7109
|
-
* Get all summaries.
|
|
7110
|
-
*/
|
|
7111
7115
|
async readSummaries(): Promise<MemorySummary[]> {
|
|
7112
7116
|
try {
|
|
7113
7117
|
const files = await readdir(this.summariesDir);
|
|
@@ -7128,9 +7132,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7128
7132
|
}
|
|
7129
7133
|
}
|
|
7130
7134
|
|
|
7131
|
-
/**
|
|
7132
|
-
* Archive memories (mark as archived, not delete).
|
|
7133
|
-
*/
|
|
7134
7135
|
async archiveMemories(memoryIds: string[], summaryId: string): Promise<number> {
|
|
7135
7136
|
const memories = await this.readAllMemories();
|
|
7136
7137
|
const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
@@ -7194,13 +7195,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7194
7195
|
return archived;
|
|
7195
7196
|
}
|
|
7196
7197
|
|
|
7197
|
-
// ---------------------------------------------------------------------------
|
|
7198
|
-
// Topic Extraction (Phase 4B)
|
|
7199
|
-
// ---------------------------------------------------------------------------
|
|
7200
|
-
|
|
7201
|
-
/**
|
|
7202
|
-
* Save topic scores to meta.json.
|
|
7203
|
-
*/
|
|
7204
7198
|
async saveTopics(topics: TopicScore[]): Promise<void> {
|
|
7205
7199
|
const metaPath = path.join(this.stateDir, "topics.json");
|
|
7206
7200
|
await mkdir(this.stateDir, { recursive: true });
|
|
@@ -7211,9 +7205,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7211
7205
|
log.debug(`saved ${topics.length} topic scores`);
|
|
7212
7206
|
}
|
|
7213
7207
|
|
|
7214
|
-
/**
|
|
7215
|
-
* Load topic scores from meta.json.
|
|
7216
|
-
*/
|
|
7217
7208
|
async loadTopics(): Promise<{ topics: TopicScore[]; updatedAt: string | null }> {
|
|
7218
7209
|
const metaPath = path.join(this.stateDir, "topics.json");
|
|
7219
7210
|
try {
|
|
@@ -7226,9 +7217,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7226
7217
|
}
|
|
7227
7218
|
}
|
|
7228
7219
|
|
|
7229
|
-
/**
|
|
7230
|
-
* Add links to an existing memory.
|
|
7231
|
-
*/
|
|
7232
7220
|
async addLinksToMemory(
|
|
7233
7221
|
memoryId: string,
|
|
7234
7222
|
links: MemoryLink[],
|
|
@@ -7241,7 +7229,6 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
7241
7229
|
const existingLinks = memory.frontmatter.links ?? [];
|
|
7242
7230
|
const mergedLinks = [...existingLinks];
|
|
7243
7231
|
|
|
7244
|
-
// Add new links, avoiding duplicates
|
|
7245
7232
|
for (const link of links) {
|
|
7246
7233
|
if (!mergedLinks.some((l) => l.targetId === link.targetId && l.linkType === link.linkType)) {
|
|
7247
7234
|
mergedLinks.push(link);
|