@remnic/core 9.3.653 → 9.3.655
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-cli.js +24 -24
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +17 -17
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +16 -16
- package/dist/access-schema.d.ts +12 -12
- package/dist/{access-service-CdJFd3_b.d.ts → access-service-BEJvriUt.d.ts} +11 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +15 -15
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-GI45G4BK.js → chunk-2RCGZ67B.js} +4 -4
- package/dist/{chunk-BEMWL2FZ.js → chunk-54LOUIBE.js} +2 -2
- package/dist/{chunk-E3J6O6N7.js → chunk-55ZMNKMQ.js} +20 -9
- package/dist/{chunk-E3J6O6N7.js.map → chunk-55ZMNKMQ.js.map} +1 -1
- package/dist/{chunk-7WEB3FLJ.js → chunk-5PLUC5OB.js} +2 -2
- package/dist/{chunk-SPMZZUEJ.js → chunk-5QD3QD76.js} +2684 -401
- package/dist/chunk-5QD3QD76.js.map +1 -0
- package/dist/{chunk-WLGE6KEO.js → chunk-67G4T7KI.js} +3 -3
- package/dist/{chunk-JX2RINDR.js → chunk-6G5JEN55.js} +2 -2
- package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
- package/dist/chunk-6IMKOIZ6.js.map +1 -0
- package/dist/{chunk-KJDKZVF3.js → chunk-A3Y37UWI.js} +3 -3
- package/dist/{chunk-CFOCZPIQ.js → chunk-BGKXTVNG.js} +2 -2
- package/dist/{chunk-QQHIQ7JD.js → chunk-COVZLGMR.js} +87 -18
- package/dist/chunk-COVZLGMR.js.map +1 -0
- package/dist/{chunk-JVRPJ7D4.js → chunk-EKQMQQ3U.js} +48 -12
- package/dist/chunk-EKQMQQ3U.js.map +1 -0
- package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
- package/dist/chunk-GKKAXVAJ.js.map +1 -0
- package/dist/{chunk-JBHXMCYN.js → chunk-GRYAECRV.js} +2 -2
- package/dist/{chunk-EHQLDFSH.js → chunk-IQ53ZSXV.js} +2 -2
- package/dist/{chunk-C63WC454.js → chunk-KOI765XP.js} +125 -1
- package/dist/chunk-KOI765XP.js.map +1 -0
- package/dist/{chunk-IVYSVAC6.js → chunk-KZZ4YAEC.js} +2 -2
- package/dist/{chunk-2DGQLOOM.js → chunk-M3VYPE2H.js} +1 -1
- package/dist/{chunk-2DGQLOOM.js.map → chunk-M3VYPE2H.js.map} +1 -1
- package/dist/{chunk-JF7SFXTG.js → chunk-NCSJKK23.js} +2 -2
- package/dist/{chunk-XMN6MMTU.js → chunk-NRBGRZW4.js} +2 -2
- package/dist/{chunk-NOBL7OUP.js → chunk-OKW6F5S5.js} +12 -5
- package/dist/{chunk-NOBL7OUP.js.map → chunk-OKW6F5S5.js.map} +1 -1
- package/dist/{chunk-BNFRL6QW.js → chunk-PTMJ2FH2.js} +2 -2
- package/dist/{chunk-KWM33SPU.js → chunk-PVE7KSQP.js} +2 -2
- package/dist/{chunk-EW52H5EM.js → chunk-QDVQ4AN2.js} +12 -5
- package/dist/chunk-QDVQ4AN2.js.map +1 -0
- package/dist/{chunk-PYWNNF2I.js → chunk-QRSKPI62.js} +99 -66
- package/dist/chunk-QRSKPI62.js.map +1 -0
- package/dist/{chunk-YM3LR4LS.js → chunk-SSSXWIBP.js} +5 -5
- package/dist/{chunk-C43KEWEV.js → chunk-TDZSSJV4.js} +1 -1
- package/dist/chunk-TDZSSJV4.js.map +1 -0
- package/dist/{chunk-Y7NWBBHV.js → chunk-TEO46GMM.js} +2 -2
- package/dist/{chunk-AJE7FJVE.js → chunk-UCEABZZN.js} +2 -2
- package/dist/{chunk-IENGGY2C.js → chunk-UCEDY5M7.js} +2 -2
- package/dist/{chunk-PRQXUSQV.js → chunk-UYNFWZWG.js} +2 -2
- package/dist/{chunk-V4UDXYGG.js → chunk-WDTUYOLS.js} +2 -2
- package/dist/{chunk-RZOBQ23O.js → chunk-XOFXKASO.js} +2 -2
- package/dist/chunk-XRKQOQLY.js +212 -0
- package/dist/chunk-XRKQOQLY.js.map +1 -0
- package/dist/{chunk-WTI35CVJ.js → chunk-YYN3LIYA.js} +5 -5
- package/dist/{cli-DDo7Qgs-.d.ts → cli-BGahB_d3.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +29 -29
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +19 -1
- package/dist/contradiction/index.js +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +37 -35
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +15 -4
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +52 -3
- package/dist/namespaces/storage.js +9 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +14 -14
- package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-BgzZlWxH.d.ts} +500 -3
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +20 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +5 -1
- package/dist/qmd.js +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.js} +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +6 -6
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +17 -1
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-Z8d_uMq8.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-D8yUmSik.d.ts → types-2OPlQWJG.d.ts} +23 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.ts +7 -0
- package/src/access-mcp.ts +7 -0
- package/src/access-service.ts +12 -0
- package/src/cli.ts +104 -0
- package/src/config.test.ts +109 -0
- package/src/config.ts +164 -0
- package/src/contradiction/contradiction.test.ts +284 -0
- package/src/contradiction/resolution.ts +151 -4
- package/src/explicit-capture.ts +31 -10
- package/src/index.ts +10 -0
- package/src/maintenance/namespace-planner.test.ts +1120 -0
- package/src/maintenance/namespace-planner.ts +893 -0
- package/src/namespaces/catalog.test.ts +3356 -0
- package/src/namespaces/catalog.ts +2123 -0
- package/src/namespaces/search.test.ts +130 -2
- package/src/namespaces/search.ts +71 -10
- package/src/namespaces/storage.ts +210 -30
- package/src/orchestrator-flush.test.ts +720 -0
- package/src/orchestrator.ts +881 -239
- package/src/qmd-client.test.ts +59 -0
- package/src/qmd.ts +124 -84
- package/src/search/port.ts +16 -0
- package/src/types.ts +23 -0
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-EW52H5EM.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-JVRPJ7D4.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-PYWNNF2I.js.map +0 -1
- package/dist/chunk-QQHIQ7JD.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-SPMZZUEJ.js.map +0 -1
- /package/dist/{chunk-GI45G4BK.js.map → chunk-2RCGZ67B.js.map} +0 -0
- /package/dist/{chunk-BEMWL2FZ.js.map → chunk-54LOUIBE.js.map} +0 -0
- /package/dist/{chunk-7WEB3FLJ.js.map → chunk-5PLUC5OB.js.map} +0 -0
- /package/dist/{chunk-WLGE6KEO.js.map → chunk-67G4T7KI.js.map} +0 -0
- /package/dist/{chunk-JX2RINDR.js.map → chunk-6G5JEN55.js.map} +0 -0
- /package/dist/{chunk-KJDKZVF3.js.map → chunk-A3Y37UWI.js.map} +0 -0
- /package/dist/{chunk-CFOCZPIQ.js.map → chunk-BGKXTVNG.js.map} +0 -0
- /package/dist/{chunk-JBHXMCYN.js.map → chunk-GRYAECRV.js.map} +0 -0
- /package/dist/{chunk-EHQLDFSH.js.map → chunk-IQ53ZSXV.js.map} +0 -0
- /package/dist/{chunk-IVYSVAC6.js.map → chunk-KZZ4YAEC.js.map} +0 -0
- /package/dist/{chunk-JF7SFXTG.js.map → chunk-NCSJKK23.js.map} +0 -0
- /package/dist/{chunk-XMN6MMTU.js.map → chunk-NRBGRZW4.js.map} +0 -0
- /package/dist/{chunk-BNFRL6QW.js.map → chunk-PTMJ2FH2.js.map} +0 -0
- /package/dist/{chunk-KWM33SPU.js.map → chunk-PVE7KSQP.js.map} +0 -0
- /package/dist/{chunk-YM3LR4LS.js.map → chunk-SSSXWIBP.js.map} +0 -0
- /package/dist/{chunk-Y7NWBBHV.js.map → chunk-TEO46GMM.js.map} +0 -0
- /package/dist/{chunk-AJE7FJVE.js.map → chunk-UCEABZZN.js.map} +0 -0
- /package/dist/{chunk-IENGGY2C.js.map → chunk-UCEDY5M7.js.map} +0 -0
- /package/dist/{chunk-PRQXUSQV.js.map → chunk-UYNFWZWG.js.map} +0 -0
- /package/dist/{chunk-V4UDXYGG.js.map → chunk-WDTUYOLS.js.map} +0 -0
- /package/dist/{chunk-RZOBQ23O.js.map → chunk-XOFXKASO.js.map} +0 -0
- /package/dist/{chunk-WTI35CVJ.js.map → chunk-YYN3LIYA.js.map} +0 -0
- /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
|
@@ -91,6 +91,7 @@ function makeResolutionStorage(options: {
|
|
|
91
91
|
failRollbackFor?: string;
|
|
92
92
|
partialSupersedeBeforeFailureFor?: string;
|
|
93
93
|
onSupersede?: (oldId: string, newId: string) => void;
|
|
94
|
+
dir?: string;
|
|
94
95
|
} = {}) {
|
|
95
96
|
const memories = new Map<string, MemoryFile>([
|
|
96
97
|
["mem-a-001", makeMemory("mem-a-001")],
|
|
@@ -111,6 +112,7 @@ function makeResolutionStorage(options: {
|
|
|
111
112
|
supersedeCalls,
|
|
112
113
|
frontmatterWrites,
|
|
113
114
|
removedFactHashIds,
|
|
115
|
+
dir: options.dir ?? "/tmp/contradiction-namespace-storage",
|
|
114
116
|
async getMemoryById(id: string) {
|
|
115
117
|
const memory = memories.get(id);
|
|
116
118
|
return memory ? cloneMemory(memory) : null;
|
|
@@ -1819,6 +1821,213 @@ test("executeResolution rolls back memory changes when pair resolution persisten
|
|
|
1819
1821
|
}
|
|
1820
1822
|
});
|
|
1821
1823
|
|
|
1824
|
+
// ── Catalog-write touch ordering (NH1dX, rule #25) ──────────────────────────────
|
|
1825
|
+
// The merge catalog touch (onMergedMemoryWritten) must fire ONLY after the
|
|
1826
|
+
// resolution durably commits past the rollback point. If resolvePair fails and
|
|
1827
|
+
// the merge rolls back, NO catalog write may be recorded for a write that did
|
|
1828
|
+
// not survive.
|
|
1829
|
+
|
|
1830
|
+
test("executeResolution merge records NO catalog write touch when resolution persistence fails and rolls back", async () => {
|
|
1831
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1832
|
+
try {
|
|
1833
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1834
|
+
const pairFile = path.join(dir, ".review", "contradictions", `${written.pairId}.json`);
|
|
1835
|
+
// Force resolvePair to fail by removing the pair file once supersession runs,
|
|
1836
|
+
// mirroring the existing rollback test. This drives the rollback path.
|
|
1837
|
+
const storage = makeResolutionStorage({
|
|
1838
|
+
dir: "/tmp/work-namespace-storage",
|
|
1839
|
+
onSupersede: () => {
|
|
1840
|
+
fs.rmSync(pairFile, { force: true });
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1844
|
+
|
|
1845
|
+
const result = await executeResolution(dir, storage, written.pairId, "merge", {
|
|
1846
|
+
mergedContent: "merged canonical fact",
|
|
1847
|
+
storageForNamespace: () => storage,
|
|
1848
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1849
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1850
|
+
},
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
assert.match(result.message, /Resolution persistence failed; rolled back memory changes/);
|
|
1854
|
+
assert.deepEqual(result.affectedIds, []);
|
|
1855
|
+
// The merged memory was created then cleaned up by the rollback, and the
|
|
1856
|
+
// resolution never persisted — so the catalog touch must NOT have fired.
|
|
1857
|
+
assert.deepEqual(
|
|
1858
|
+
catalogTouches,
|
|
1859
|
+
[],
|
|
1860
|
+
"no catalog write touch may be recorded for a rolled-back merge",
|
|
1861
|
+
);
|
|
1862
|
+
assert.equal(readPair(dir, written.pairId), null);
|
|
1863
|
+
} finally {
|
|
1864
|
+
await cleanup();
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
test("executeResolution merge records exactly one catalog write touch for the pair namespace on success", async () => {
|
|
1869
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1870
|
+
try {
|
|
1871
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1872
|
+
const storage = makeResolutionStorage({ dir: "/tmp/work-namespace-storage" });
|
|
1873
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1874
|
+
|
|
1875
|
+
const result = await executeResolution(dir, storage, written.pairId, "merge", {
|
|
1876
|
+
mergedContent: "merged canonical fact",
|
|
1877
|
+
storageForNamespace: () => storage,
|
|
1878
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1879
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1880
|
+
},
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
assert.match(result.message, /Both memories superseded by merged/);
|
|
1884
|
+
assert.deepEqual(result.affectedIds, ["mem-a-001", "mem-b-002"]);
|
|
1885
|
+
assert.equal(readPair(dir, written.pairId)?.resolution, "merge");
|
|
1886
|
+
// Exactly one touch, carrying the pair namespace and the routed storage dir.
|
|
1887
|
+
assert.deepEqual(catalogTouches, [
|
|
1888
|
+
{ namespace: "work", storageDir: "/tmp/work-namespace-storage" },
|
|
1889
|
+
]);
|
|
1890
|
+
} finally {
|
|
1891
|
+
await cleanup();
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
// NH3X3: supersede-only resolutions (keep-a / keep-b) also mutate the namespace,
|
|
1896
|
+
// so they must record a catalog touch — post-commit, never on rollback.
|
|
1897
|
+
|
|
1898
|
+
test("executeResolution keep-a records exactly one catalog write touch on success", async () => {
|
|
1899
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1900
|
+
try {
|
|
1901
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1902
|
+
const storage = makeResolutionStorage({ dir: "/tmp/work-namespace-storage" });
|
|
1903
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1904
|
+
|
|
1905
|
+
const result = await executeResolution(dir, storage, written.pairId, "keep-a", {
|
|
1906
|
+
storageForNamespace: () => storage,
|
|
1907
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1908
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1909
|
+
},
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
assert.deepEqual(result.affectedIds, ["mem-b-002"]);
|
|
1913
|
+
assert.equal(readPair(dir, written.pairId)?.resolution, "keep-a");
|
|
1914
|
+
assert.deepEqual(catalogTouches, [
|
|
1915
|
+
{ namespace: "work", storageDir: "/tmp/work-namespace-storage" },
|
|
1916
|
+
]);
|
|
1917
|
+
} finally {
|
|
1918
|
+
await cleanup();
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
test("executeResolution keep-b records exactly one catalog write touch on success", async () => {
|
|
1923
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1924
|
+
try {
|
|
1925
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1926
|
+
const storage = makeResolutionStorage({ dir: "/tmp/work-namespace-storage" });
|
|
1927
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1928
|
+
|
|
1929
|
+
const result = await executeResolution(dir, storage, written.pairId, "keep-b", {
|
|
1930
|
+
storageForNamespace: () => storage,
|
|
1931
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1932
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1933
|
+
},
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
assert.deepEqual(result.affectedIds, ["mem-a-001"]);
|
|
1937
|
+
assert.equal(readPair(dir, written.pairId)?.resolution, "keep-b");
|
|
1938
|
+
assert.deepEqual(catalogTouches, [
|
|
1939
|
+
{ namespace: "work", storageDir: "/tmp/work-namespace-storage" },
|
|
1940
|
+
]);
|
|
1941
|
+
} finally {
|
|
1942
|
+
await cleanup();
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
test("executeResolution keep-a records NO catalog write touch when resolution persistence fails and rolls back", async () => {
|
|
1947
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1948
|
+
try {
|
|
1949
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1950
|
+
const pairFile = path.join(dir, ".review", "contradictions", `${written.pairId}.json`);
|
|
1951
|
+
const storage = makeResolutionStorage({
|
|
1952
|
+
dir: "/tmp/work-namespace-storage",
|
|
1953
|
+
onSupersede: () => {
|
|
1954
|
+
fs.rmSync(pairFile, { force: true });
|
|
1955
|
+
},
|
|
1956
|
+
});
|
|
1957
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1958
|
+
|
|
1959
|
+
const result = await executeResolution(dir, storage, written.pairId, "keep-a", {
|
|
1960
|
+
storageForNamespace: () => storage,
|
|
1961
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1962
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1963
|
+
},
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
assert.match(result.message, /Resolution persistence failed; rolled back memory changes/);
|
|
1967
|
+
assert.deepEqual(result.affectedIds, []);
|
|
1968
|
+
assert.deepEqual(catalogTouches, [], "a rolled-back keep-a must not record a catalog write touch");
|
|
1969
|
+
assert.equal(readPair(dir, written.pairId), null);
|
|
1970
|
+
} finally {
|
|
1971
|
+
await cleanup();
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
test("executeResolution keep-a records a catalog write touch when resolution persistence fails and rollback is incomplete", async () => {
|
|
1976
|
+
const { dir, cleanup } = await makeTempDir();
|
|
1977
|
+
try {
|
|
1978
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
1979
|
+
const pairFile = path.join(dir, ".review", "contradictions", `${written.pairId}.json`);
|
|
1980
|
+
const storage = makeResolutionStorage({
|
|
1981
|
+
dir: "/tmp/work-namespace-storage",
|
|
1982
|
+
failRollbackFor: "mem-b-002",
|
|
1983
|
+
onSupersede: () => {
|
|
1984
|
+
fs.rmSync(pairFile, { force: true });
|
|
1985
|
+
},
|
|
1986
|
+
});
|
|
1987
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1988
|
+
|
|
1989
|
+
const result = await executeResolution(dir, storage, written.pairId, "keep-a", {
|
|
1990
|
+
storageForNamespace: () => storage,
|
|
1991
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
1992
|
+
catalogTouches.push({ namespace, storageDir });
|
|
1993
|
+
},
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
assert.match(result.message, /Resolution persistence failed; rollback incomplete/);
|
|
1997
|
+
assert.equal(storage.memories.get("mem-b-002")?.frontmatter.status, "superseded");
|
|
1998
|
+
assert.equal(storage.memories.get("mem-b-002")?.frontmatter.supersededBy, "mem-a-001");
|
|
1999
|
+
assert.deepEqual(catalogTouches, [
|
|
2000
|
+
{ namespace: "work", storageDir: "/tmp/work-namespace-storage" },
|
|
2001
|
+
]);
|
|
2002
|
+
assert.equal(readPair(dir, written.pairId), null);
|
|
2003
|
+
} finally {
|
|
2004
|
+
await cleanup();
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// Non-mutating verbs never touch the catalog (no namespace memory changed).
|
|
2009
|
+
test("executeResolution both-valid records no catalog write touch", async () => {
|
|
2010
|
+
const { dir, cleanup } = await makeTempDir();
|
|
2011
|
+
try {
|
|
2012
|
+
const written = writePair(dir, makePair({ namespace: "work" }));
|
|
2013
|
+
const storage = makeResolutionStorage({ dir: "/tmp/work-namespace-storage" });
|
|
2014
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
2015
|
+
|
|
2016
|
+
const result = await executeResolution(dir, storage, written.pairId, "both-valid", {
|
|
2017
|
+
storageForNamespace: () => storage,
|
|
2018
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
2019
|
+
catalogTouches.push({ namespace, storageDir });
|
|
2020
|
+
},
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
assert.deepEqual(result.affectedIds, []);
|
|
2024
|
+
assert.equal(readPair(dir, written.pairId)?.resolution, "both-valid");
|
|
2025
|
+
assert.deepEqual(catalogTouches, [], "both-valid mutates no namespace memory — no catalog touch");
|
|
2026
|
+
} finally {
|
|
2027
|
+
await cleanup();
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
|
|
1822
2031
|
test("executeResolution merge rolls back the first supersession when the second fails", async () => {
|
|
1823
2032
|
const { dir, cleanup } = await makeTempDir();
|
|
1824
2033
|
try {
|
|
@@ -1948,9 +2157,13 @@ test("executeResolution merge keeps created replacement when rollback fails", as
|
|
|
1948
2157
|
failSupersedeFor: "mem-b-002",
|
|
1949
2158
|
failRollbackFor: "mem-a-001",
|
|
1950
2159
|
});
|
|
2160
|
+
const catalogTouches: Array<{ namespace: string | undefined; storageDir: string }> = [];
|
|
1951
2161
|
|
|
1952
2162
|
const result = await executeResolution(dir, storage, written.pairId, "merge", {
|
|
1953
2163
|
mergedContent: "merged canonical fact",
|
|
2164
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
2165
|
+
catalogTouches.push({ namespace, storageDir });
|
|
2166
|
+
},
|
|
1954
2167
|
});
|
|
1955
2168
|
|
|
1956
2169
|
const mergedId = storage.supersedeCalls[0]?.newId;
|
|
@@ -1960,6 +2173,9 @@ test("executeResolution merge keeps created replacement when rollback fails", as
|
|
|
1960
2173
|
assert.equal(storage.memories.get("mem-a-001")?.frontmatter.supersededBy, mergedId);
|
|
1961
2174
|
assert.equal(storage.memories.get(mergedId)?.content, "merged canonical fact");
|
|
1962
2175
|
assert.deepEqual(storage.removedFactHashIds, []);
|
|
2176
|
+
assert.deepEqual(catalogTouches, [
|
|
2177
|
+
{ namespace: undefined, storageDir: "/tmp/contradiction-namespace-storage" },
|
|
2178
|
+
]);
|
|
1963
2179
|
assert.equal(readPair(dir, written.pairId)?.resolution, undefined);
|
|
1964
2180
|
} finally {
|
|
1965
2181
|
await cleanup();
|
|
@@ -2489,3 +2705,71 @@ test("readPair returns null for non-object JSON", async () => {
|
|
|
2489
2705
|
await cleanup();
|
|
2490
2706
|
}
|
|
2491
2707
|
});
|
|
2708
|
+
|
|
2709
|
+
// ── issue #1499 sweep: a contradiction merge that CREATES a new merged memory
|
|
2710
|
+
// writes durable data to the pair's (possibly dynamic) namespace storage,
|
|
2711
|
+
// bypassing the extraction write path. executeResolution must invoke
|
|
2712
|
+
// onMergedMemoryWritten(namespace, storageDir) so the caller records the catalog
|
|
2713
|
+
// write — otherwise a dynamic namespace whose only durable mutation is a merge
|
|
2714
|
+
// stays invisible to QMD maintenance / writtenSince.
|
|
2715
|
+
test("executeResolution merge fires onMergedMemoryWritten with the pair namespace when a new memory is created", async () => {
|
|
2716
|
+
const { dir, cleanup } = await makeTempDir();
|
|
2717
|
+
try {
|
|
2718
|
+
const written = writePair(dir, makePair({ namespace: "project-origin-dynamic" }));
|
|
2719
|
+
const storage = makeResolutionStorage();
|
|
2720
|
+
(storage as { dir?: string }).dir = "/memory/namespaces/project-origin-dynamic-token";
|
|
2721
|
+
|
|
2722
|
+
const touches: Array<{ namespace?: string; storageDir: string }> = [];
|
|
2723
|
+
|
|
2724
|
+
const result = await executeResolution(dir, storage, written.pairId, "merge", {
|
|
2725
|
+
mergedContent: "merged canonical fact for dynamic namespace",
|
|
2726
|
+
storageForNamespace: () => storage,
|
|
2727
|
+
onMergedMemoryWritten: (namespace, storageDir) => {
|
|
2728
|
+
touches.push({ namespace, storageDir });
|
|
2729
|
+
},
|
|
2730
|
+
});
|
|
2731
|
+
|
|
2732
|
+
assert.deepEqual(result.affectedIds, ["mem-a-001", "mem-b-002"]);
|
|
2733
|
+
assert.equal(touches.length, 1, "exactly one catalog write touch for a created merge memory");
|
|
2734
|
+
assert.equal(
|
|
2735
|
+
touches[0]!.namespace,
|
|
2736
|
+
"project-origin-dynamic",
|
|
2737
|
+
"the catalog touch carries the pair's (dynamic) namespace",
|
|
2738
|
+
);
|
|
2739
|
+
assert.equal(
|
|
2740
|
+
touches[0]!.storageDir,
|
|
2741
|
+
"/memory/namespaces/project-origin-dynamic-token",
|
|
2742
|
+
"the catalog touch carries the resolved namespace storage dir",
|
|
2743
|
+
);
|
|
2744
|
+
} finally {
|
|
2745
|
+
await cleanup();
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
// Reusing an EXISTING merged memory id still supersedes BOTH sources — a durable
|
|
2750
|
+
// namespace mutation — so the catalog touch MUST fire so `lastWriteAt` refreshes
|
|
2751
|
+
// (NH3X3). It fires exactly once, post-commit.
|
|
2752
|
+
test("executeResolution merge fires onMergedMemoryWritten exactly once when reusing an existing merged id", async () => {
|
|
2753
|
+
const { dir, cleanup } = await makeTempDir();
|
|
2754
|
+
try {
|
|
2755
|
+
const written = writePair(dir, makePair({ namespace: "project-origin-dynamic" }));
|
|
2756
|
+
const storage = makeResolutionStorage();
|
|
2757
|
+
(storage as { dir?: string }).dir = "/memory/namespaces/project-origin-dynamic-token";
|
|
2758
|
+
|
|
2759
|
+
let touchCount = 0;
|
|
2760
|
+
|
|
2761
|
+
const result = await executeResolution(dir, storage, written.pairId, "merge", {
|
|
2762
|
+
mergedMemoryId: "mem-merged-003", // pre-existing merged memory, sources still superseded
|
|
2763
|
+
storageForNamespace: () => storage,
|
|
2764
|
+
onMergedMemoryWritten: () => {
|
|
2765
|
+
touchCount += 1;
|
|
2766
|
+
},
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
assert.deepEqual(result.affectedIds, ["mem-a-001", "mem-b-002"]);
|
|
2770
|
+
assert.equal(touchCount, 1, "reusing an existing merged memory still supersedes both sources — record one touch");
|
|
2771
|
+
assert.equal(readPair(dir, written.pairId)?.resolution, "merge");
|
|
2772
|
+
} finally {
|
|
2773
|
+
await cleanup();
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* reimplement supersession logic here (rule 22: deduplicate resolution).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { log } from "../logger.js";
|
|
8
9
|
import type { StorageManager } from "../storage.js";
|
|
9
10
|
import type { MemoryCategory, MemoryFile } from "../types.js";
|
|
10
11
|
import type { ResolutionVerb } from "./contradiction-review.js";
|
|
11
|
-
import {
|
|
12
|
-
import { log } from "../logger.js";
|
|
12
|
+
import { readPair, resolvePair } from "./contradiction-review.js";
|
|
13
13
|
|
|
14
14
|
export interface ResolutionResult {
|
|
15
15
|
pairId: string;
|
|
@@ -29,6 +29,24 @@ export interface ExecuteResolutionOptions {
|
|
|
29
29
|
mergedCategory?: MemoryCategory;
|
|
30
30
|
/** Resolve storage for the pair namespace, or the default namespace for legacy unscoped pairs. */
|
|
31
31
|
storageForNamespace?: (namespace: string | undefined) => StorageManager | Promise<StorageManager>;
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort hook invoked after a contradiction resolution leaves a durable
|
|
34
|
+
* mutation in the namespace's memory files (issue #1499 sweep, NH1dX / NH3X3).
|
|
35
|
+
* Every mutating verb — `merge` (creates a new memory and supersedes both
|
|
36
|
+
* sources), `keep-a`, and `keep-b` (supersede the losing source + rewrite
|
|
37
|
+
* frontmatter) — writes directly to the pair's (possibly DYNAMIC) namespace
|
|
38
|
+
* storage, bypassing the extraction write path that records catalog writes. So
|
|
39
|
+
* without this the namespace's `lastWriteAt` stays stale and QMD maintenance /
|
|
40
|
+
* `writtenSince` can skip a namespace whose only post-write mutation is
|
|
41
|
+
* resolving a contradiction. It fires after the resolution commits, or after a
|
|
42
|
+
* failed resolution/rollback path when durable memory changes are still left on
|
|
43
|
+
* disk. If a failure rolls back cleanly, this is never called, so the catalog
|
|
44
|
+
* never records a write that did not survive (rule #25). Non-mutating verbs
|
|
45
|
+
* (`both-valid`, `needs-more-context`) never trigger it. Callers wire this to
|
|
46
|
+
* `Orchestrator.recordCatalogWrite(namespace, storageDir)`. Must be
|
|
47
|
+
* failure-tolerant: it is fire-and-forget and must never affect resolution.
|
|
48
|
+
*/
|
|
49
|
+
onMergedMemoryWritten?: (namespace: string | undefined, storageDir: string) => void;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
const VALID_VERBS: ResolutionVerb[] = ["keep-a", "keep-b", "merge", "both-valid", "needs-more-context"];
|
|
@@ -81,6 +99,48 @@ export async function executeResolution(
|
|
|
81
99
|
let message = "";
|
|
82
100
|
let supersedeFailed = false;
|
|
83
101
|
let rollbackAfterResolveFailure: (() => Promise<boolean>) | null = null;
|
|
102
|
+
// Deferred catalog-write touch for any resolution that leaves durable namespace
|
|
103
|
+
// memory mutations (issue #1499 sweep, NH1dX / NH3X3). Rule #25: never record a
|
|
104
|
+
// catalog touch for a write that is fully rolled back. Successful mutating
|
|
105
|
+
// resolutions invoke it after `resolvePair` persists. Failed paths invoke it
|
|
106
|
+
// only when rollback inspection shows the namespace still differs from the
|
|
107
|
+
// pre-mutation snapshot.
|
|
108
|
+
let recordCatalogWriteTouch: (() => void) | null = null;
|
|
109
|
+
// Returns the deferred touch fn for a mutating verb (or null when the caller
|
|
110
|
+
// wired no catalog hook), so each branch assigns `recordCatalogWriteTouch`
|
|
111
|
+
// directly in the function body — keeping TS control-flow narrowing intact at
|
|
112
|
+
// the post-commit invocation below.
|
|
113
|
+
const buildCatalogTouch = (): (() => void) | null => {
|
|
114
|
+
if (!options.onMergedMemoryWritten) return null;
|
|
115
|
+
const onMergedMemoryWritten = options.onMergedMemoryWritten;
|
|
116
|
+
const namespace = pair.namespace;
|
|
117
|
+
const storageDir = resolutionStorage.dir;
|
|
118
|
+
return () => onMergedMemoryWritten(namespace, storageDir);
|
|
119
|
+
};
|
|
120
|
+
const catalogWriteTouch = buildCatalogTouch();
|
|
121
|
+
const recordCatalogWriteTouchSafely = (context: string, touch = catalogWriteTouch): void => {
|
|
122
|
+
if (!touch) return;
|
|
123
|
+
try {
|
|
124
|
+
touch();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
log.warn(
|
|
127
|
+
"[contradiction-resolution] catalog write touch failed for pair=%s context=%s: %s",
|
|
128
|
+
pairId,
|
|
129
|
+
context,
|
|
130
|
+
err instanceof Error ? err.message : err,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const touchCatalogIfRollbackLeftChange = async (
|
|
135
|
+
context: string,
|
|
136
|
+
snapshots: MemoryFile[],
|
|
137
|
+
replacement?: Extract<MergeReplacement, { ok: true }>,
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
if (!catalogWriteTouch) return;
|
|
140
|
+
if (await rollbackLeftDurableMutation(resolutionStorage, snapshots, replacement)) {
|
|
141
|
+
recordCatalogWriteTouchSafely(context);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
84
144
|
|
|
85
145
|
switch (verb) {
|
|
86
146
|
case "keep-a": {
|
|
@@ -98,6 +158,9 @@ export async function executeResolution(
|
|
|
98
158
|
affectedIds.push(idB);
|
|
99
159
|
rollbackAfterResolveFailure = async () =>
|
|
100
160
|
restoreMemorySnapshot(resolutionStorage, sourceB!, "contradiction-resolution:keep-a-rollback");
|
|
161
|
+
// keep-a superseded idB in this namespace — record a catalog touch once
|
|
162
|
+
// the resolution durably commits (NH3X3).
|
|
163
|
+
recordCatalogWriteTouch = catalogWriteTouch;
|
|
101
164
|
message = `Kept ${idA}, superseded ${idB}`;
|
|
102
165
|
}
|
|
103
166
|
else {
|
|
@@ -105,6 +168,9 @@ export async function executeResolution(
|
|
|
105
168
|
const rolledBack = sourceB
|
|
106
169
|
? await restoreMemorySnapshot(resolutionStorage, sourceB, "contradiction-resolution:keep-a-rollback")
|
|
107
170
|
: false;
|
|
171
|
+
if (sourceB && !rolledBack) {
|
|
172
|
+
await touchCatalogIfRollbackLeftChange("keep-a-rollback-incomplete", [sourceB]);
|
|
173
|
+
}
|
|
108
174
|
message = rolledBack
|
|
109
175
|
? `Supersede failed for ${idB}; restored ${idB} and did not resolve`
|
|
110
176
|
: `Supersede failed for ${idB}; rollback incomplete for ${idB} and pair is not resolved`;
|
|
@@ -126,6 +192,9 @@ export async function executeResolution(
|
|
|
126
192
|
affectedIds.push(idA);
|
|
127
193
|
rollbackAfterResolveFailure = async () =>
|
|
128
194
|
restoreMemorySnapshot(resolutionStorage, sourceA!, "contradiction-resolution:keep-b-rollback");
|
|
195
|
+
// keep-b superseded idA in this namespace — record a catalog touch once
|
|
196
|
+
// the resolution durably commits (NH3X3).
|
|
197
|
+
recordCatalogWriteTouch = catalogWriteTouch;
|
|
129
198
|
message = `Kept ${idB}, superseded ${idA}`;
|
|
130
199
|
}
|
|
131
200
|
else {
|
|
@@ -133,6 +202,9 @@ export async function executeResolution(
|
|
|
133
202
|
const rolledBack = sourceA
|
|
134
203
|
? await restoreMemorySnapshot(resolutionStorage, sourceA, "contradiction-resolution:keep-b-rollback")
|
|
135
204
|
: false;
|
|
205
|
+
if (sourceA && !rolledBack) {
|
|
206
|
+
await touchCatalogIfRollbackLeftChange("keep-b-rollback-incomplete", [sourceA]);
|
|
207
|
+
}
|
|
136
208
|
message = rolledBack
|
|
137
209
|
? `Supersede failed for ${idA}; restored ${idA} and did not resolve`
|
|
138
210
|
: `Supersede failed for ${idA}; rollback incomplete for ${idA} and pair is not resolved`;
|
|
@@ -157,6 +229,9 @@ export async function executeResolution(
|
|
|
157
229
|
if (rolledBackA) {
|
|
158
230
|
await cleanupCreatedReplacement(resolutionStorage, replacement);
|
|
159
231
|
}
|
|
232
|
+
else {
|
|
233
|
+
await touchCatalogIfRollbackLeftChange("merge-first-rollback-incomplete", [replacement.sourceA], replacement);
|
|
234
|
+
}
|
|
160
235
|
break;
|
|
161
236
|
}
|
|
162
237
|
|
|
@@ -174,6 +249,13 @@ export async function executeResolution(
|
|
|
174
249
|
if (rolledBackA && rolledBackB) {
|
|
175
250
|
await cleanupCreatedReplacement(resolutionStorage, replacement);
|
|
176
251
|
}
|
|
252
|
+
else {
|
|
253
|
+
await touchCatalogIfRollbackLeftChange(
|
|
254
|
+
"merge-second-rollback-incomplete",
|
|
255
|
+
[replacement.sourceA, replacement.sourceB],
|
|
256
|
+
replacement,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
177
259
|
break;
|
|
178
260
|
}
|
|
179
261
|
|
|
@@ -186,6 +268,17 @@ export async function executeResolution(
|
|
|
186
268
|
}
|
|
187
269
|
return rolledBackA && rolledBackB;
|
|
188
270
|
};
|
|
271
|
+
// Catalog write touch (issue #1499 sweep): the merge supersedes BOTH sources
|
|
272
|
+
// (and, when created, writes a fresh merged memory) in the pair's (possibly
|
|
273
|
+
// dynamic) namespace storage — but the resolution is not durable yet
|
|
274
|
+
// (resolvePair persists below, and a failure rolls the merge back). Defer
|
|
275
|
+
// the touch so it fires ONLY after the resolution commits past the rollback
|
|
276
|
+
// point (NH1dX, rule #25). Arm it for EVERY successful merge — even reusing
|
|
277
|
+
// an existing merged-id still supersedes both sources, a namespace mutation
|
|
278
|
+
// that must refresh `lastWriteAt` (NH3X3). Otherwise a dynamic namespace
|
|
279
|
+
// whose only durable mutation is a contradiction merge stays invisible to
|
|
280
|
+
// QMD maintenance / `writtenSince`. Best-effort on the caller side.
|
|
281
|
+
recordCatalogWriteTouch = catalogWriteTouch;
|
|
189
282
|
message = `Both memories superseded by merged ${replacement.mergedId}`;
|
|
190
283
|
break;
|
|
191
284
|
}
|
|
@@ -217,10 +310,20 @@ export async function executeResolution(
|
|
|
217
310
|
affectedIds.length = 0;
|
|
218
311
|
message = rolledBack
|
|
219
312
|
? `Resolution persistence failed; rolled back memory changes and did not resolve ${pairId}`
|
|
220
|
-
:
|
|
313
|
+
: "Resolution persistence failed; rollback incomplete and pair is not resolved";
|
|
314
|
+
if (!rolledBack && recordCatalogWriteTouch) {
|
|
315
|
+
recordCatalogWriteTouchSafely("resolve-persistence-rollback-incomplete", recordCatalogWriteTouch);
|
|
316
|
+
}
|
|
221
317
|
} else {
|
|
222
|
-
message =
|
|
318
|
+
message = "Resolution persistence failed; pair is not resolved";
|
|
223
319
|
}
|
|
320
|
+
} else if (recordCatalogWriteTouch) {
|
|
321
|
+
// The resolution durably committed (memory mutated AND the resolution
|
|
322
|
+
// persisted past the rollback point). Only now is it safe to record the
|
|
323
|
+
// catalog write for the namespace mutation (NH1dX / NH3X3, rule #25).
|
|
324
|
+
// Best-effort: the caller's callback swallows errors; guard here so a
|
|
325
|
+
// throwing callback never derails a successful resolution.
|
|
326
|
+
recordCatalogWriteTouchSafely("resolved", recordCatalogWriteTouch);
|
|
224
327
|
}
|
|
225
328
|
}
|
|
226
329
|
log.info("[contradiction-resolution] pair=%s verb=%s affected=%d", pairId, verb, affectedIds.length);
|
|
@@ -376,6 +479,50 @@ async function restoreMemorySnapshot(
|
|
|
376
479
|
}
|
|
377
480
|
}
|
|
378
481
|
|
|
482
|
+
async function rollbackLeftDurableMutation(
|
|
483
|
+
storage: StorageManager,
|
|
484
|
+
snapshots: MemoryFile[],
|
|
485
|
+
replacement?: Extract<MergeReplacement, { ok: true }>,
|
|
486
|
+
): Promise<boolean> {
|
|
487
|
+
for (const snapshot of snapshots) {
|
|
488
|
+
try {
|
|
489
|
+
const current = await storage.getMemoryById(snapshot.frontmatter.id);
|
|
490
|
+
if (!current) return true;
|
|
491
|
+
if (supersessionStateChanged(current, snapshot)) return true;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
log.warn(
|
|
494
|
+
"[contradiction-resolution] rollback inspection failed for %s: %s",
|
|
495
|
+
snapshot.frontmatter.id,
|
|
496
|
+
err instanceof Error ? err.message : err,
|
|
497
|
+
);
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (replacement?.created) {
|
|
503
|
+
try {
|
|
504
|
+
return (await storage.getMemoryById(replacement.mergedId)) !== null;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
log.warn(
|
|
507
|
+
"[contradiction-resolution] rollback replacement inspection failed for %s: %s",
|
|
508
|
+
replacement.mergedId,
|
|
509
|
+
err instanceof Error ? err.message : err,
|
|
510
|
+
);
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function supersessionStateChanged(current: MemoryFile, snapshot: MemoryFile): boolean {
|
|
519
|
+
return (
|
|
520
|
+
current.frontmatter.status !== snapshot.frontmatter.status ||
|
|
521
|
+
current.frontmatter.supersededBy !== snapshot.frontmatter.supersededBy ||
|
|
522
|
+
current.frontmatter.supersededAt !== snapshot.frontmatter.supersededAt
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
379
526
|
async function cleanupCreatedReplacement(storage: StorageManager, replacement: Extract<MergeReplacement, { ok: true }>): Promise<void> {
|
|
380
527
|
if (!replacement.created) return;
|
|
381
528
|
await cleanupMemoryId(storage, replacement.mergedId);
|
package/src/explicit-capture.ts
CHANGED
|
@@ -435,6 +435,15 @@ export async function persistExplicitCapture(
|
|
|
435
435
|
expiresAt: candidate.expiresAt,
|
|
436
436
|
source: source === "inline" ? "explicit-inline" : "explicit",
|
|
437
437
|
});
|
|
438
|
+
// Record the catalog write touch (issue #1499, round 5 codex P2). Explicit
|
|
439
|
+
// captures bypass the extraction write path, so without this their namespace
|
|
440
|
+
// never updates `lastWriteAt`. An undefined namespace means the DEFAULT root
|
|
441
|
+
// (round 6, codex P2), which recordCatalogWrite resolves. The method is an
|
|
442
|
+
// optional best-effort hook — guard so Orchestrator-like callers without it
|
|
443
|
+
// don't break (rule #33). Best-effort and failure-tolerant.
|
|
444
|
+
if (typeof orchestrator.recordCatalogWrite === "function") {
|
|
445
|
+
orchestrator.recordCatalogWrite(resolvedNamespace, storage.dir);
|
|
446
|
+
}
|
|
438
447
|
|
|
439
448
|
const created = new Date().toISOString();
|
|
440
449
|
const event: MemoryLifecycleEvent = {
|
|
@@ -531,16 +540,28 @@ export async function queueExplicitCaptureForReview(
|
|
|
531
540
|
entityRef: sanitizeReviewMetadata(input.entityRef),
|
|
532
541
|
source: source === "inline" ? "explicit-inline-review" : "explicit-review",
|
|
533
542
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
543
|
+
try {
|
|
544
|
+
const created = await storage.getMemoryById(id);
|
|
545
|
+
if (created) {
|
|
546
|
+
await storage.writeMemoryFrontmatter(created, {
|
|
547
|
+
status: "pending_review",
|
|
548
|
+
updated: new Date().toISOString(),
|
|
549
|
+
}, {
|
|
550
|
+
actor: explicitCaptureActor(source),
|
|
551
|
+
reasonCode: reason,
|
|
552
|
+
ruleVersion: "explicit-capture.v1",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
// Record the catalog write touch (issue #1499, round 5/6 codex P2; NIhUg).
|
|
557
|
+
// A queued review capture writes memory to the namespace's root (the DEFAULT
|
|
558
|
+
// root when undefined), so its `lastWriteAt` must reflect the write once
|
|
559
|
+
// `writeMemory` returns an id. If the later pending-review frontmatter update
|
|
560
|
+
// fails, the memory file is still durable and must not disappear from
|
|
561
|
+
// writtenSince/maintenance scheduling. Guarded optional hook (rule #33).
|
|
562
|
+
if (typeof orchestrator.recordCatalogWrite === "function") {
|
|
563
|
+
orchestrator.recordCatalogWrite(queueNamespace, storage.dir);
|
|
564
|
+
}
|
|
544
565
|
}
|
|
545
566
|
const event: MemoryLifecycleEvent = {
|
|
546
567
|
eventId: `mle-${randomUUID()}`,
|
package/src/index.ts
CHANGED
|
@@ -323,6 +323,16 @@ export { MeilisearchBackend } from "./search/meilisearch-backend.js";
|
|
|
323
323
|
|
|
324
324
|
export { buildEntityRecallSection } from "./entity-retrieval.js";
|
|
325
325
|
export { resolvePrincipal } from "./namespaces/principal.js";
|
|
326
|
+
export {
|
|
327
|
+
NamespaceCatalog,
|
|
328
|
+
type NamespaceRecord,
|
|
329
|
+
type NamespaceKind,
|
|
330
|
+
type NamespaceDiscoverySource,
|
|
331
|
+
type NamespaceCatalogFilter,
|
|
332
|
+
type NamespaceTouchMetadata,
|
|
333
|
+
type NamespaceCatalogRebuildResult,
|
|
334
|
+
type NamespaceCatalogSkippedRoot,
|
|
335
|
+
} from "./namespaces/catalog.js";
|
|
326
336
|
|
|
327
337
|
// ---------------------------------------------------------------------------
|
|
328
338
|
// Session identity / transcript pathing (issue #1496)
|