@remnic/core 9.3.699 → 9.3.701
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-boundary.d.ts +5 -5
- package/dist/access-boundary.js +14 -10
- package/dist/access-cli.js +30 -28
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +18 -14
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +17 -13
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +16 -12
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DftqtNUy.d.ts → access-service-CGVWK6lZ.d.ts} +2 -2
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +13 -9
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/action-confidence.js +2 -2
- 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 +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -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/capabilities.d.ts +1 -1
- package/dist/{catalog-D7YDNNF7.d.ts → catalog-DBIghceA.d.ts} +27 -53
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +8 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-E6GOVHHJ.js → chunk-27LQPUMZ.js} +3 -3
- package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
- package/dist/chunk-3FAMU5TX.js.map +1 -0
- package/dist/chunk-3JJWNZTT.js +446 -0
- package/dist/chunk-3JJWNZTT.js.map +1 -0
- package/dist/{chunk-JYVD2XZ4.js → chunk-4HIAWLA2.js} +58 -25
- package/dist/chunk-4HIAWLA2.js.map +1 -0
- package/dist/{chunk-A62RAIBN.js → chunk-6W2D6FGG.js} +2 -2
- package/dist/{chunk-COEZR6F5.js → chunk-7TAQEPLE.js} +2 -2
- package/dist/{chunk-TIVZ4MCG.js → chunk-D75JXBV4.js} +3 -3
- package/dist/{chunk-YY2HGKWR.js → chunk-DEDQXIDL.js} +2 -2
- package/dist/{chunk-XFG3PVZE.js → chunk-FUCJAZ25.js} +8 -8
- package/dist/{chunk-EZWQZYLK.js → chunk-HF4N43Q7.js} +2 -2
- package/dist/{chunk-HZ5DRA4Q.js → chunk-HXHKLVAS.js} +20 -20
- package/dist/{chunk-AC5LO7IU.js → chunk-I4WBNM3M.js} +7 -1
- package/dist/chunk-I4WBNM3M.js.map +1 -0
- package/dist/{chunk-QVTSLRKT.js → chunk-IJEZMWKA.js} +3 -3
- package/dist/{chunk-ZXUOAFUG.js → chunk-IKNQAGBV.js} +1 -1
- package/dist/chunk-IKNQAGBV.js.map +1 -0
- package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
- package/dist/{chunk-2ULWWAQH.js → chunk-ISLJ5WIM.js} +2 -2
- package/dist/{chunk-2KJG6ZZK.js → chunk-IYOPIG3E.js} +2 -2
- package/dist/{chunk-M4DQWUKX.js → chunk-JKOKX3PS.js} +62 -163
- package/dist/chunk-JKOKX3PS.js.map +1 -0
- package/dist/{chunk-SJQ4HY3E.js → chunk-JO3E5VGS.js} +2 -2
- package/dist/{chunk-3LSSFCB4.js → chunk-K4DWSPMW.js} +11 -1
- package/dist/chunk-K4DWSPMW.js.map +1 -0
- package/dist/{chunk-IZ5E6ZTK.js → chunk-KF4TXW7Z.js} +6 -6
- package/dist/{chunk-CQ4PGFMC.js → chunk-KS7WQ4BZ.js} +4 -4
- package/dist/chunk-LTJAMRGI.js +286 -0
- package/dist/chunk-LTJAMRGI.js.map +1 -0
- package/dist/{chunk-PJIHCOGV.js → chunk-MNU5G4TK.js} +2 -2
- package/dist/{chunk-2K64VH66.js → chunk-ODTWHSY2.js} +69 -45
- package/dist/chunk-ODTWHSY2.js.map +1 -0
- package/dist/{chunk-LIWU4QI6.js → chunk-OLOYQZFB.js} +5 -5
- package/dist/{chunk-MRFVMSTR.js → chunk-OMNTQPZO.js} +20 -1
- package/dist/{chunk-MRFVMSTR.js.map → chunk-OMNTQPZO.js.map} +1 -1
- package/dist/{chunk-JI6HWBYL.js → chunk-Q4O3ET6F.js} +2 -2
- package/dist/{chunk-N55RJT4N.js → chunk-QP37KL5H.js} +2 -2
- package/dist/{chunk-LPIWEKZ3.js → chunk-SDPDU2PM.js} +3 -3
- package/dist/{chunk-RIC5U67B.js → chunk-SFMRLXIV.js} +232 -4
- package/dist/chunk-SFMRLXIV.js.map +1 -0
- package/dist/{chunk-RTN2BLZM.js → chunk-T5QAZIBO.js} +2 -2
- package/dist/{chunk-ANZLT74L.js → chunk-TFVVONWD.js} +2 -2
- package/dist/{chunk-SIDSEXUG.js → chunk-WFEZUGU5.js} +2 -2
- package/dist/{chunk-FSEQXHEZ.js → chunk-XTIRCSIH.js} +2 -2
- package/dist/{chunk-XL5RSHZP.js → chunk-YO4MBK3I.js} +2 -2
- package/dist/{chunk-AH2JUU6X.js → chunk-Z2YFLXHU.js} +2 -2
- package/dist/{chunk-MRX6S22R.js → chunk-ZT7B64BE.js} +2 -2
- package/dist/{chunk-UDDSC6PO.js → chunk-ZYNMX6IU.js} +54 -6
- package/dist/chunk-ZYNMX6IU.js.map +1 -0
- package/dist/{cli-DUMkkdLl.d.ts → cli-D3XeenwN.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +32 -28
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -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 +7 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/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 +7 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/extraction-faithfulness.d.ts +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/{forget-PLR6J5DN.js → forget-6SOIPUMQ.js} +32 -1
- package/dist/forget-6SOIPUMQ.js.map +1 -0
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +65 -43
- 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 +7 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -3
- package/dist/maintenance/rebuild-memory-projection.js +8 -4
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- 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-provenance.js +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 +2 -2
- package/dist/namespaces/migrate.js +8 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +32 -3
- package/dist/namespaces/storage.js +7 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +19 -2
- package/dist/operator-toolkit.js +17 -11
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +9 -5
- package/dist/{orchestrator-Dv8JQu3-.d.ts → orchestrator-BzMCZlKn.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +22 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +4 -4
- 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 +5 -5
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +4 -4
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +3 -3
- 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 +28 -28
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-CgREi9E0.d.ts → semantic-consolidation-BtUfv-AL.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +8 -4
- package/dist/semantic-rule-promotion.js +7 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +7 -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 +306 -2
- package/dist/storage.js +6 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -2
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/summary-snapshot.js +2 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-supersession.js +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-CeZYSC-E.d.ts → types-PuiPZ9iE.d.ts} +34 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/utils/serialize-mutations.js +5 -280
- package/dist/utils/serialize-mutations.js.map +1 -1
- package/dist/verified-recall.js +7 -3
- package/package.json +2 -2
- package/src/config.ts +10 -0
- package/src/lifecycle/tombstones.test.ts +355 -0
- package/src/lifecycle/tombstones.ts +807 -0
- package/src/maintenance/forget.ts +41 -0
- package/src/maintenance/pattern-reinforcement.ts +47 -0
- package/src/memory-provenance.ts +12 -0
- package/src/namespaces/catalog.test.ts +222 -14
- package/src/namespaces/catalog.ts +54 -187
- package/src/namespaces/storage.ts +120 -80
- package/src/operator-toolkit.ts +77 -0
- package/src/orchestrator.ts +16 -0
- package/src/review/index.ts +48 -3
- package/src/storage.ts +329 -2
- package/src/summary-snapshot.test.ts +63 -1
- package/src/summary-snapshot.ts +61 -80
- package/src/temporal-supersession.ts +38 -0
- package/src/types.ts +34 -0
- package/src/utils/serialize-mutations.ts +10 -6
- package/dist/chunk-2K64VH66.js.map +0 -1
- package/dist/chunk-3LSSFCB4.js.map +0 -1
- package/dist/chunk-AC5LO7IU.js.map +0 -1
- package/dist/chunk-JYVD2XZ4.js.map +0 -1
- package/dist/chunk-M4DQWUKX.js.map +0 -1
- package/dist/chunk-RIC5U67B.js.map +0 -1
- package/dist/chunk-UDDSC6PO.js.map +0 -1
- package/dist/chunk-XJNBEDFE.js.map +0 -1
- package/dist/chunk-ZXUOAFUG.js.map +0 -1
- package/dist/forget-PLR6J5DN.js.map +0 -1
- /package/dist/{chunk-E6GOVHHJ.js.map → chunk-27LQPUMZ.js.map} +0 -0
- /package/dist/{chunk-A62RAIBN.js.map → chunk-6W2D6FGG.js.map} +0 -0
- /package/dist/{chunk-COEZR6F5.js.map → chunk-7TAQEPLE.js.map} +0 -0
- /package/dist/{chunk-TIVZ4MCG.js.map → chunk-D75JXBV4.js.map} +0 -0
- /package/dist/{chunk-YY2HGKWR.js.map → chunk-DEDQXIDL.js.map} +0 -0
- /package/dist/{chunk-XFG3PVZE.js.map → chunk-FUCJAZ25.js.map} +0 -0
- /package/dist/{chunk-EZWQZYLK.js.map → chunk-HF4N43Q7.js.map} +0 -0
- /package/dist/{chunk-HZ5DRA4Q.js.map → chunk-HXHKLVAS.js.map} +0 -0
- /package/dist/{chunk-QVTSLRKT.js.map → chunk-IJEZMWKA.js.map} +0 -0
- /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
- /package/dist/{chunk-2ULWWAQH.js.map → chunk-ISLJ5WIM.js.map} +0 -0
- /package/dist/{chunk-2KJG6ZZK.js.map → chunk-IYOPIG3E.js.map} +0 -0
- /package/dist/{chunk-SJQ4HY3E.js.map → chunk-JO3E5VGS.js.map} +0 -0
- /package/dist/{chunk-IZ5E6ZTK.js.map → chunk-KF4TXW7Z.js.map} +0 -0
- /package/dist/{chunk-CQ4PGFMC.js.map → chunk-KS7WQ4BZ.js.map} +0 -0
- /package/dist/{chunk-PJIHCOGV.js.map → chunk-MNU5G4TK.js.map} +0 -0
- /package/dist/{chunk-LIWU4QI6.js.map → chunk-OLOYQZFB.js.map} +0 -0
- /package/dist/{chunk-JI6HWBYL.js.map → chunk-Q4O3ET6F.js.map} +0 -0
- /package/dist/{chunk-N55RJT4N.js.map → chunk-QP37KL5H.js.map} +0 -0
- /package/dist/{chunk-LPIWEKZ3.js.map → chunk-SDPDU2PM.js.map} +0 -0
- /package/dist/{chunk-RTN2BLZM.js.map → chunk-T5QAZIBO.js.map} +0 -0
- /package/dist/{chunk-ANZLT74L.js.map → chunk-TFVVONWD.js.map} +0 -0
- /package/dist/{chunk-SIDSEXUG.js.map → chunk-WFEZUGU5.js.map} +0 -0
- /package/dist/{chunk-FSEQXHEZ.js.map → chunk-XTIRCSIH.js.map} +0 -0
- /package/dist/{chunk-XL5RSHZP.js.map → chunk-YO4MBK3I.js.map} +0 -0
- /package/dist/{chunk-AH2JUU6X.js.map → chunk-Z2YFLXHU.js.map} +0 -0
- /package/dist/{chunk-MRX6S22R.js.map → chunk-ZT7B64BE.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -2728,6 +2728,22 @@ export class Orchestrator {
|
|
|
2728
2728
|
maxVersionsPerPage: config.versioningMaxPerPage,
|
|
2729
2729
|
sidecarDir: config.versioningSidecarDir,
|
|
2730
2730
|
});
|
|
2731
|
+
// Wire the tombstone non-resurrection invariant (issue #1579) on the
|
|
2732
|
+
// primary default-namespace storage. Router-created namespace storages
|
|
2733
|
+
// get the same config via NamespaceStorageRouter.bindTombstonesConfig
|
|
2734
|
+
// (installed below) so every write path funnels through the chokepoint.
|
|
2735
|
+
const { tombstonesEnabled, tombstonesSemanticMatch, tombstonesSemanticThreshold } = config;
|
|
2736
|
+
this.storage.setTombstonesConfig({
|
|
2737
|
+
enabled: tombstonesEnabled,
|
|
2738
|
+
semanticMatch: tombstonesSemanticMatch,
|
|
2739
|
+
semanticThreshold: tombstonesSemanticThreshold,
|
|
2740
|
+
namespace: config.defaultNamespace,
|
|
2741
|
+
});
|
|
2742
|
+
this.storageRouter.bindTombstonesConfig(this.storage, config.defaultNamespace, {
|
|
2743
|
+
enabled: tombstonesEnabled,
|
|
2744
|
+
semanticMatch: tombstonesSemanticMatch,
|
|
2745
|
+
semanticThreshold: tombstonesSemanticThreshold,
|
|
2746
|
+
});
|
|
2731
2747
|
// Wire at-rest encryption (issue #690 PR 3/4).
|
|
2732
2748
|
// If secureStoreEnabled, check whether the keyring already holds a key
|
|
2733
2749
|
// for this memory dir (e.g. operator unlocked before daemon restart).
|
package/src/review/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ReviewItem {
|
|
|
29
29
|
/** Created date */
|
|
30
30
|
created: string;
|
|
31
31
|
/** Reason it's in review */
|
|
32
|
-
reviewReason: "low_confidence" | "suggestion" | "contradiction" | "duplicate";
|
|
32
|
+
reviewReason: "low_confidence" | "suggestion" | "contradiction" | "duplicate" | "tombstone_blocked";
|
|
33
33
|
/** Additional context */
|
|
34
34
|
context?: string;
|
|
35
35
|
}
|
|
@@ -45,6 +45,8 @@ export interface ReviewResult {
|
|
|
45
45
|
updatedPath?: string;
|
|
46
46
|
/** Status message */
|
|
47
47
|
message: string;
|
|
48
|
+
/** Tombstone id cleared by an approve action (for the caller to revoke). */
|
|
49
|
+
clearedTombstoneId?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface ReviewListResult {
|
|
@@ -70,6 +72,15 @@ export interface ReviewOptions {
|
|
|
70
72
|
export interface ReviewActionOptions {
|
|
71
73
|
/** Match the threshold used when listing review items (default: 0.7) */
|
|
72
74
|
confidenceThreshold?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Revocation hook (issue #1579). When approving a memory whose frontmatter
|
|
77
|
+
* carries `blockedBy: <tombstoneId>`, the hook fires so the caller (CLI /
|
|
78
|
+
* orchestrator) can append a `kind: "revocation"` tombstone entry —
|
|
79
|
+
* re-allowing the content. Fire-and-forget: a revocation failure MUST NOT
|
|
80
|
+
* fail the approval (gotcha #13). The hook receives the tombstone id and
|
|
81
|
+
* the memory id.
|
|
82
|
+
*/
|
|
83
|
+
onApproveBlockedMemory?: (tombstoneId: string, memoryId: string) => void | Promise<void>;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
interface ReviewFileMatch {
|
|
@@ -211,7 +222,7 @@ export function listReviewItems(options: ReviewOptions): ReviewListResult {
|
|
|
211
222
|
if (!fm?.id) return false;
|
|
212
223
|
|
|
213
224
|
const confidence = parseConfidence(fm.confidence, 1);
|
|
214
|
-
if (confidence >= confidenceThreshold) return false;
|
|
225
|
+
if (confidence >= confidenceThreshold && !(typeof fm.blockedBy === "string" && fm.blockedBy.length > 0)) return false;
|
|
215
226
|
if (parseBoolean(fm.reviewDismissed)) return false;
|
|
216
227
|
|
|
217
228
|
// Skip if already in items
|
|
@@ -226,7 +237,7 @@ export function listReviewItems(options: ReviewOptions): ReviewListResult {
|
|
|
226
237
|
source: (fm.source as string) ?? "unknown",
|
|
227
238
|
filePath,
|
|
228
239
|
created: (fm.created as string) ?? new Date().toISOString(),
|
|
229
|
-
reviewReason: "low_confidence",
|
|
240
|
+
reviewReason: (typeof fm.blockedBy === "string" && fm.blockedBy.length > 0) ? "tombstone_blocked" : "low_confidence",
|
|
230
241
|
});
|
|
231
242
|
return isLimitReached();
|
|
232
243
|
});
|
|
@@ -276,10 +287,32 @@ function approveItem(
|
|
|
276
287
|
const fm = parseFrontmatter(content);
|
|
277
288
|
if (!fm) return { itemId, action: "approve", message: "Could not parse frontmatter" };
|
|
278
289
|
|
|
290
|
+
// Issue #1579 — when approving a tombstone-blocked memory, fire the
|
|
291
|
+
// revocation hook so the content is re-allowed (append-only log: a
|
|
292
|
+
// `kind: "revocation"` entry supersedes the tombstone at lookup). Clear
|
|
293
|
+
// blockedBy/tombstoneBlockTier on the promoted memory so it is fully
|
|
294
|
+
// active. Fire-and-forget: a hook failure never fails the approval.
|
|
295
|
+
const blockedBy = typeof fm.blockedBy === "string" ? fm.blockedBy : null;
|
|
296
|
+
if (blockedBy && options.onApproveBlockedMemory) {
|
|
297
|
+
try {
|
|
298
|
+
const maybe = options.onApproveBlockedMemory(blockedBy, itemId);
|
|
299
|
+
if (maybe && typeof (maybe as Promise<void>).then === "function") {
|
|
300
|
+
void (maybe as Promise<void>).catch(() => undefined);
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
/* fire-and-forget — never fail the approval (gotcha #13) */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
279
307
|
const updatedContent = updateFrontmatterFields(content, {
|
|
280
308
|
confidence: "0.9",
|
|
281
309
|
confidenceTier: "high",
|
|
282
310
|
reviewDismissed: null,
|
|
311
|
+
// Clear the tombstone-block markers so the promoted memory is active.
|
|
312
|
+
// Clear the tombstone-block markers AND restore active status so the
|
|
313
|
+
// promoted memory is fully active in recall + provenance (issue #1579
|
|
314
|
+
// review: leaving status: pending_review kept it invisible to recall).
|
|
315
|
+
...(blockedBy ? { blockedBy: null, tombstoneBlockTier: null, status: "active" } : {}),
|
|
283
316
|
});
|
|
284
317
|
|
|
285
318
|
if (found.location === "category") {
|
|
@@ -289,6 +322,7 @@ function approveItem(
|
|
|
289
322
|
action: "approve",
|
|
290
323
|
updatedPath: found.filePath,
|
|
291
324
|
message: "Approved low-confidence memory in place with confidence 0.9",
|
|
325
|
+
...(blockedBy ? { clearedTombstoneId: blockedBy } : {}),
|
|
292
326
|
};
|
|
293
327
|
}
|
|
294
328
|
|
|
@@ -310,6 +344,7 @@ function approveItem(
|
|
|
310
344
|
action: "approve",
|
|
311
345
|
updatedPath: promotedPath,
|
|
312
346
|
message: `Promoted to ${category} with confidence 0.9`,
|
|
347
|
+
...(blockedBy ? { clearedTombstoneId: blockedBy } : {}),
|
|
313
348
|
};
|
|
314
349
|
}
|
|
315
350
|
|
|
@@ -418,6 +453,16 @@ function isLowConfidenceReviewCandidate(
|
|
|
418
453
|
options: ReviewActionOptions,
|
|
419
454
|
): boolean {
|
|
420
455
|
const threshold = options.confidenceThreshold ?? DEFAULT_CONFIDENCE_THRESHOLD;
|
|
456
|
+
// Issue #1579: tombstone-blocked facts are ALWAYS review candidates,
|
|
457
|
+
// regardless of confidence. The block sets status: pending_review +
|
|
458
|
+
// blockedBy; these must be visible in the review queue so an operator
|
|
459
|
+
// can approve (revoke the tombstone) or dismiss them. Without this, a
|
|
460
|
+
// high-confidence blocked fact is visible on disk but absent from the
|
|
461
|
+
// review UI — a silent drop, which violates the non-resurrection design
|
|
462
|
+
// (rule 34: visible, not silent).
|
|
463
|
+
if (typeof fm.blockedBy === "string" && fm.blockedBy.length > 0) {
|
|
464
|
+
return !parseBoolean(fm.reviewDismissed);
|
|
465
|
+
}
|
|
421
466
|
return (
|
|
422
467
|
parseConfidence(fm.confidence, 1) < threshold &&
|
|
423
468
|
!parseBoolean(fm.reviewDismissed)
|
package/src/storage.ts
CHANGED
|
@@ -38,6 +38,17 @@ import {
|
|
|
38
38
|
stripCitationForTemplate,
|
|
39
39
|
DEFAULT_CITATION_FORMAT,
|
|
40
40
|
} from "./source-attribution.js";
|
|
41
|
+
import {
|
|
42
|
+
TombstoneStore,
|
|
43
|
+
collectRetiredMemoriesForRebuild,
|
|
44
|
+
buildRetiredFactTombstoneInputs,
|
|
45
|
+
type TombstoneStoreOptions,
|
|
46
|
+
type TombstoneFileIo,
|
|
47
|
+
type TombstoneReason,
|
|
48
|
+
type TombstoneCreatedBy,
|
|
49
|
+
type TombstoneStats,
|
|
50
|
+
} from "./lifecycle/tombstones.js";
|
|
51
|
+
import { supersessionKeysForFact } from "./temporal-supersession.js";
|
|
41
52
|
import type {
|
|
42
53
|
AccessTrackingEntry,
|
|
43
54
|
BufferState,
|
|
@@ -315,6 +326,11 @@ function serializeFrontmatter(fm: MemoryFrontmatter): string {
|
|
|
315
326
|
}
|
|
316
327
|
// Status management
|
|
317
328
|
if (fm.status && fm.status !== "active") lines.push(`status: ${fm.status}`);
|
|
329
|
+
// Tombstone block visibility (issue #1579). Emit only when present so legacy
|
|
330
|
+
// memories round-trip unchanged; the review queue reads these to drive the
|
|
331
|
+
// automatic revocation on approval.
|
|
332
|
+
if (fm.blockedBy) lines.push(`blockedBy: ${fm.blockedBy}`);
|
|
333
|
+
if (fm.tombstoneBlockTier) lines.push(`tombstoneBlockTier: ${fm.tombstoneBlockTier}`);
|
|
318
334
|
if (fm.supersededBy) lines.push(`supersededBy: ${fm.supersededBy}`);
|
|
319
335
|
if (fm.supersededAt) lines.push(`supersededAt: ${fm.supersededAt}`);
|
|
320
336
|
if (fm.archivedAt) lines.push(`archivedAt: ${fm.archivedAt}`);
|
|
@@ -820,6 +836,15 @@ function parseFrontmatter(
|
|
|
820
836
|
lineage: lineage && lineage.length > 0 ? lineage : undefined,
|
|
821
837
|
// Status management
|
|
822
838
|
status: (fm.status as MemoryStatus) || "active",
|
|
839
|
+
// Tombstone block visibility (issue #1579) — read-through.
|
|
840
|
+
blockedBy: fm.blockedBy || undefined,
|
|
841
|
+
tombstoneBlockTier:
|
|
842
|
+
fm.tombstoneBlockTier === "exact" ||
|
|
843
|
+
fm.tombstoneBlockTier === "normalized" ||
|
|
844
|
+
fm.tombstoneBlockTier === "keyed" ||
|
|
845
|
+
fm.tombstoneBlockTier === "semantic"
|
|
846
|
+
? fm.tombstoneBlockTier
|
|
847
|
+
: undefined,
|
|
823
848
|
supersededBy: fm.supersededBy || undefined,
|
|
824
849
|
supersededAt: fm.supersededAt || undefined,
|
|
825
850
|
archivedAt: fm.archivedAt || undefined,
|
|
@@ -2294,6 +2319,19 @@ export class StorageManager {
|
|
|
2294
2319
|
try { this.onCatalogWrite?.(); } catch { /* gotcha #13 */ }
|
|
2295
2320
|
}
|
|
2296
2321
|
|
|
2322
|
+
// ── Tombstone store (issue #1579) ───────────────────────────────────────
|
|
2323
|
+
// Instance-scoped (rule 11) + namespace-scoped (rule 42) tombstone index.
|
|
2324
|
+
// Lazily loaded from <stateDir>/tombstones.jsonl; invalidated together with
|
|
2325
|
+
// every other cache layer via invalidateAllMemoriesCache (rule 25).
|
|
2326
|
+
private tombstoneStore: TombstoneStore | null = null;
|
|
2327
|
+
private tombstoneStoreLoadPromise: Promise<TombstoneStore> | null = null;
|
|
2328
|
+
private tombstonesConfig: {
|
|
2329
|
+
enabled: boolean;
|
|
2330
|
+
semanticMatch: boolean;
|
|
2331
|
+
semanticThreshold: number;
|
|
2332
|
+
namespace: string;
|
|
2333
|
+
} = { enabled: false, semanticMatch: false, semanticThreshold: 0.9, namespace: "default" };
|
|
2334
|
+
|
|
2297
2335
|
/** Page-versioning configuration. Set by the orchestrator after construction. */
|
|
2298
2336
|
private _versioningConfig: VersioningConfig | null = null;
|
|
2299
2337
|
|
|
@@ -2673,7 +2711,10 @@ export class StorageManager {
|
|
|
2673
2711
|
* Promote a pending_review wearable memory to active in place,
|
|
2674
2712
|
* merging updated trust evidence into structuredAttributes. Returns
|
|
2675
2713
|
* false when the memory is missing or no longer pending_review (a
|
|
2676
|
-
* concurrent review decision wins)
|
|
2714
|
+
* concurrent review decision wins), or when the row is tombstone-blocked
|
|
2715
|
+
* (`blockedBy` — issue #1579 threads OcuDx/Ocu1l): writeMemoryFrontmatter
|
|
2716
|
+
* bypasses the writeMemory chokepoint, so a blocked row must first be
|
|
2717
|
+
* cleared via revokeTombstone before promotion can proceed.
|
|
2677
2718
|
*/
|
|
2678
2719
|
async promoteWearableMemory(
|
|
2679
2720
|
id: string,
|
|
@@ -2684,6 +2725,8 @@ export class StorageManager {
|
|
|
2684
2725
|
const memory = memories.find((entry) => entry.frontmatter.id === id);
|
|
2685
2726
|
if (!memory) return false;
|
|
2686
2727
|
if (memory.frontmatter.status !== "pending_review") return false;
|
|
2728
|
+
// Tombstone-blocked rows need revokeTombstone first (issue #1579 OcuDx/Ocu1l).
|
|
2729
|
+
if (memory.frontmatter.blockedBy) return false;
|
|
2687
2730
|
return this.writeMemoryFrontmatter(memory, {
|
|
2688
2731
|
status: "active",
|
|
2689
2732
|
// Keep frontmatter confidence in step with the re-scored trust —
|
|
@@ -3047,6 +3090,178 @@ export class StorageManager {
|
|
|
3047
3090
|
return path.join(this.stateDir, "fact-hashes.ready");
|
|
3048
3091
|
}
|
|
3049
3092
|
|
|
3093
|
+
// ── Tombstone store access (issue #1579) ─────────────────────────────────
|
|
3094
|
+
/**
|
|
3095
|
+
* The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
|
|
3096
|
+
* so it is co-located with the fact-hash index and encrypted at rest when
|
|
3097
|
+
* the secure-store is enabled (the StorageManager's secure read/write
|
|
3098
|
+
* helpers are injected into the store).
|
|
3099
|
+
*/
|
|
3100
|
+
private get tombstonesPath(): string {
|
|
3101
|
+
return path.join(this.stateDir, "tombstones.jsonl");
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
/**
|
|
3105
|
+
* Configure the tombstone invariant for this storage instance. Installed
|
|
3106
|
+
* by the orchestrator after construction (same pattern as
|
|
3107
|
+
* `setVersioningConfig` / `citationTemplate`). When `enabled` is false
|
|
3108
|
+
* (default until the orchestrator wires config), the chokepoint check is a
|
|
3109
|
+
* no-op — pre-feature behavior for rollback safety (rule 30).
|
|
3110
|
+
*/
|
|
3111
|
+
setTombstonesConfig(config: {
|
|
3112
|
+
enabled: boolean;
|
|
3113
|
+
semanticMatch: boolean;
|
|
3114
|
+
semanticThreshold: number;
|
|
3115
|
+
namespace: string;
|
|
3116
|
+
}): void {
|
|
3117
|
+
this.tombstonesConfig = { ...config };
|
|
3118
|
+
// Reset the store so the next access rebuilds with the new options.
|
|
3119
|
+
if (this.tombstoneStore) {
|
|
3120
|
+
this.tombstoneStore.invalidate();
|
|
3121
|
+
this.tombstoneStore = null;
|
|
3122
|
+
this.tombstoneStoreLoadPromise = null;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
private buildTombstoneStore(): TombstoneStore {
|
|
3127
|
+
const options: TombstoneStoreOptions = {
|
|
3128
|
+
enabled: this.tombstonesConfig.enabled,
|
|
3129
|
+
semanticMatch: this.tombstonesConfig.semanticMatch,
|
|
3130
|
+
semanticThreshold: this.tombstonesConfig.semanticThreshold,
|
|
3131
|
+
// Wire the SAME helpers the dedup index uses (rule 23 / checklist §13):
|
|
3132
|
+
// hashContent = ContentHashIndex.computeHash, normalizeText =
|
|
3133
|
+
// ContentHashIndex.normalizeContent. Importing them here (not copying)
|
|
3134
|
+
// guarantees the tombstone tiers can never drift from dedup.
|
|
3135
|
+
hashContent: ContentHashIndex.computeHash,
|
|
3136
|
+
normalizeText: ContentHashIndex.normalizeContent,
|
|
3137
|
+
};
|
|
3138
|
+
const io: TombstoneFileIo = {
|
|
3139
|
+
read: (filePath) => this.readStorageSecureFile(filePath),
|
|
3140
|
+
append: (filePath, content) => this.appendStorageSecureFile(filePath, content),
|
|
3141
|
+
write: (filePath, content) => this.writeStorageSecureFile(filePath, content),
|
|
3142
|
+
// stat lets the store own its cross-process staleness probe (#1579).
|
|
3143
|
+
stat: (filePath) => statSync(filePath),
|
|
3144
|
+
};
|
|
3145
|
+
return new TombstoneStore(
|
|
3146
|
+
this.tombstonesPath,
|
|
3147
|
+
this.tombstonesConfig.namespace,
|
|
3148
|
+
options,
|
|
3149
|
+
io,
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Lazily load the tombstone store. Mirrors the fact-hash-index lazy-load
|
|
3155
|
+
* pattern: a single in-flight load promise dedups concurrent first-access.
|
|
3156
|
+
* The cross-process staleness probe lives inside the store now (#1579): on
|
|
3157
|
+
* each cached access it stats its own file and invalidates if a peer process
|
|
3158
|
+
* appended, so the next lookup reloads from disk. If the probe just
|
|
3159
|
+
* invalidated the store, fall through to the reload branch — returning null
|
|
3160
|
+
* here was the original regression (the chokepoint threw and failed OPEN).
|
|
3161
|
+
*/
|
|
3162
|
+
getTombstoneStore(): Promise<TombstoneStore> {
|
|
3163
|
+
if (this.tombstoneStore) {
|
|
3164
|
+
// The store owns the staleness probe + reload (#1579): on a cached hit it
|
|
3165
|
+
// stats its own file and, if a peer process appended, invalidates + reloads
|
|
3166
|
+
// in place. We await it so the chokepoint's lookup always sees a fresh index.
|
|
3167
|
+
const cached = this.tombstoneStore;
|
|
3168
|
+
return cached.ensureFreshAgainstDisk().then(() => cached);
|
|
3169
|
+
}
|
|
3170
|
+
if (!this.tombstoneStoreLoadPromise) {
|
|
3171
|
+
const store = this.buildTombstoneStore();
|
|
3172
|
+
this.tombstoneStoreLoadPromise = store
|
|
3173
|
+
.load()
|
|
3174
|
+
.then(() => {
|
|
3175
|
+
this.tombstoneStore = store;
|
|
3176
|
+
return store;
|
|
3177
|
+
})
|
|
3178
|
+
.catch((err) => {
|
|
3179
|
+
this.tombstoneStoreLoadPromise = null;
|
|
3180
|
+
throw err;
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
return this.tombstoneStoreLoadPromise;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* Append a tombstone for a retired memory (issue #1579 emitters).
|
|
3188
|
+
* Best-effort: a tombstone append failure MUST NOT fail the supersession /
|
|
3189
|
+
* correction that triggered it (gotcha #13 / rule 34). The memory is
|
|
3190
|
+
* already retired on disk; the tombstone is the non-resurrection guard.
|
|
3191
|
+
*/
|
|
3192
|
+
async appendTombstone(input: {
|
|
3193
|
+
reason: TombstoneReason;
|
|
3194
|
+
createdBy: TombstoneCreatedBy;
|
|
3195
|
+
sourceMemoryId: string;
|
|
3196
|
+
rawContent: string;
|
|
3197
|
+
entityRef?: string;
|
|
3198
|
+
supersessionKey?: string;
|
|
3199
|
+
createdAt?: string;
|
|
3200
|
+
/** Canonical contentHash from the retired memory's frontmatter (#1579). */
|
|
3201
|
+
contentHash?: string;
|
|
3202
|
+
}): Promise<string | null> {
|
|
3203
|
+
if (!this.tombstonesConfig.enabled) return null;
|
|
3204
|
+
try {
|
|
3205
|
+
const store = await this.getTombstoneStore();
|
|
3206
|
+
// Chokepoint citation strip (#1579 review): strip citation annotations
|
|
3207
|
+
// from the raw body so BOTH the exact-tier hash (when the caller omits
|
|
3208
|
+
// contentHash and the store computes it from rawContent) AND the
|
|
3209
|
+
// normalized-text tier match re-extraction, which hashes/normalizes the
|
|
3210
|
+
// citation-stripped contentHashSource. Idempotent on already-stripped
|
|
3211
|
+
// text, so callers that pre-strip (e.g. recordSupersession) are unaffected.
|
|
3212
|
+
const strippedRawContent = stripCitationForTemplate(input.rawContent, this.citationTemplate);
|
|
3213
|
+
// The store records its own mtime after the append (markWritten) so the
|
|
3214
|
+
// staleness probe does not treat this write as a peer append (#1579).
|
|
3215
|
+
return await store.appendTombstone({ ...input, rawContent: strippedRawContent });
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
log.warn(`tombstone append failed (memory=${input.sourceMemoryId}): ${err}`);
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
/**
|
|
3223
|
+
* Revoke a tombstone (re-allow the content). Used by the review-queue
|
|
3224
|
+
* approval path when an operator approves a blocked memory.
|
|
3225
|
+
*/
|
|
3226
|
+
async revokeTombstone(tombstoneId: string, createdBy: TombstoneCreatedBy): Promise<string | null> {
|
|
3227
|
+
if (!this.tombstonesConfig.enabled) return null;
|
|
3228
|
+
try {
|
|
3229
|
+
const store = await this.getTombstoneStore();
|
|
3230
|
+
return await store.revoke(tombstoneId, createdBy);
|
|
3231
|
+
} catch (err) {
|
|
3232
|
+
log.warn(`tombstone revoke failed (id=${tombstoneId}): ${err}`);
|
|
3233
|
+
return null;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
/** Aggregate tombstone stats for `remnic doctor`. */
|
|
3238
|
+
async getTombstoneStats(): Promise<TombstoneStats | null> {
|
|
3239
|
+
if (!this.tombstonesConfig.enabled) return null;
|
|
3240
|
+
try {
|
|
3241
|
+
const store = await this.getTombstoneStore();
|
|
3242
|
+
return store.stats();
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
log.warn(`tombstone stats failed: ${err}`);
|
|
3245
|
+
return null;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* Rebuild the tombstone log from retired memories on disk. Exposed for
|
|
3251
|
+
* `remnic doctor --rebuild-tombstones` and tests.
|
|
3252
|
+
*/
|
|
3253
|
+
async rebuildTombstonesFromFiles(): Promise<number> {
|
|
3254
|
+
if (!this.tombstonesConfig.enabled) return 0;
|
|
3255
|
+
const store = await this.getTombstoneStore();
|
|
3256
|
+
const all = [...(await this.readAllMemories()), ...(await this.readAllColdMemories())];
|
|
3257
|
+
// Pure projection (lifecycle/tombstones.ts) keeps this method thin (#1520).
|
|
3258
|
+
const retired = collectRetiredMemoriesForRebuild(all, {
|
|
3259
|
+
stripCitation: (text) => stripCitationForTemplate(text, this.citationTemplate),
|
|
3260
|
+
supersessionKeysForFact,
|
|
3261
|
+
});
|
|
3262
|
+
return await store.rebuild(retired);
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3050
3265
|
private async getFactHashIndex(): Promise<ContentHashIndex> {
|
|
3051
3266
|
if (this.factHashIndex) {
|
|
3052
3267
|
return this.factHashIndex;
|
|
@@ -3486,12 +3701,72 @@ export class StorageManager {
|
|
|
3486
3701
|
// consolidation paths can remove the correct hash from ContentHashIndex
|
|
3487
3702
|
// regardless of what citation format (if any) has been appended to the
|
|
3488
3703
|
// stored body. Mirrors the logic in the fact-hash-index update below.
|
|
3704
|
+
let factHashSourceForTombstone: string | null = null;
|
|
3489
3705
|
if (category === "fact") {
|
|
3490
3706
|
const hashSource =
|
|
3491
3707
|
options.contentHashSource !== undefined && options.contentHashSource.length > 0
|
|
3492
3708
|
? sanitizeMemoryContent(options.contentHashSource).text
|
|
3493
3709
|
: sanitized.text;
|
|
3494
3710
|
fm.contentHash = ContentHashIndex.computeHash(hashSource);
|
|
3711
|
+
factHashSourceForTombstone = hashSource;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
// ── Non-resurrection chokepoint (issue #1579) ────────────────────────
|
|
3715
|
+
// Before a new fact becomes active, consult the tombstone index. If a
|
|
3716
|
+
// retired fact matches (exact / normalized / keyed / semantic), persist
|
|
3717
|
+
// the candidate as pending_review + blockedBy instead — VISIBLE, never a
|
|
3718
|
+
// silent drop (rule 34) — and skip the active dedup/index registration
|
|
3719
|
+
// (rule 44). This is the SINGLE storage persist path: every write
|
|
3720
|
+
// (extraction, import, consolidation, dreams, pattern-reinforcement)
|
|
3721
|
+
// funnels through writeMemory, so the five resurrection paths are blocked
|
|
3722
|
+
// here without per-path code (rule 43).
|
|
3723
|
+
let tombstoneBlocked = false;
|
|
3724
|
+
if (
|
|
3725
|
+
category === "fact" &&
|
|
3726
|
+
this.tombstonesConfig.enabled &&
|
|
3727
|
+
factHashSourceForTombstone !== null &&
|
|
3728
|
+
// Block facts that would become ACTIVE or are already pending_review
|
|
3729
|
+
// (issue #1579 thread ObteQ: wearable/native imports write fact
|
|
3730
|
+
// candidates with status: pending_review; without checking them here,
|
|
3731
|
+
// promoteWearableMemory could later flip a retired fact active without
|
|
3732
|
+
// a tombstone check). A caller that explicitly requests a terminal
|
|
3733
|
+
// non-active status (rejected/retracted/superseded) is respected.
|
|
3734
|
+
(fm.status === undefined || fm.status === "active" || fm.status === "pending_review")
|
|
3735
|
+
) {
|
|
3736
|
+
try {
|
|
3737
|
+
const tombstoneStore = await this.getTombstoneStore();
|
|
3738
|
+
// Derive the keyed-tier supersession key from structured attributes
|
|
3739
|
+
// using the SAME helper write-time supersession uses (one helper).
|
|
3740
|
+
const keyedSupersession =
|
|
3741
|
+
options.entityRef && options.structuredAttributes
|
|
3742
|
+
? supersessionKeysForFact({
|
|
3743
|
+
entityRef: options.entityRef,
|
|
3744
|
+
structuredAttributes: options.structuredAttributes,
|
|
3745
|
+
})
|
|
3746
|
+
: [];
|
|
3747
|
+
// Issue #1579 thread Ociag/Oci-W: pass EVERY key — emitters register one tombstone per key, so the block can be on any later key.
|
|
3748
|
+
const match = tombstoneStore.lookup({
|
|
3749
|
+
contentHash: fm.contentHash,
|
|
3750
|
+
normalizedText: ContentHashIndex.normalizeContent(factHashSourceForTombstone),
|
|
3751
|
+
...(options.entityRef ? { entityRef: options.entityRef } : {}),
|
|
3752
|
+
...(keyedSupersession.length > 0 ? { supersessionKeys: keyedSupersession } : {}),
|
|
3753
|
+
namespace: this.tombstonesConfig.namespace,
|
|
3754
|
+
});
|
|
3755
|
+
if (match) {
|
|
3756
|
+
tombstoneBlocked = true;
|
|
3757
|
+
fm.status = "pending_review";
|
|
3758
|
+
fm.blockedBy = match.tombstoneId;
|
|
3759
|
+
fm.tombstoneBlockTier = match.matchedTier;
|
|
3760
|
+
log.info(
|
|
3761
|
+
`tombstone: blocked resurrection of fact ${id} (tier=${match.matchedTier}, tombstone=${match.tombstoneId}, reason=${match.reason})`,
|
|
3762
|
+
);
|
|
3763
|
+
}
|
|
3764
|
+
} catch (err) {
|
|
3765
|
+
// Fail-open (rule 34 spirit): a tombstone lookup error must not block
|
|
3766
|
+
// the write. The fact persists as active; the next lookup after the
|
|
3767
|
+
// store recovers will catch a subsequent re-extraction.
|
|
3768
|
+
log.warn(`tombstone lookup failed for fact ${id} (fail-open): ${err}`);
|
|
3769
|
+
}
|
|
3495
3770
|
}
|
|
3496
3771
|
|
|
3497
3772
|
const fileContent = `${serializeFrontmatter(fm)}\n\n${sanitized.text}\n`;
|
|
@@ -3512,7 +3787,11 @@ export class StorageManager {
|
|
|
3512
3787
|
...((options.lineage ?? []).filter(Boolean)),
|
|
3513
3788
|
],
|
|
3514
3789
|
});
|
|
3515
|
-
if (category === "fact") {
|
|
3790
|
+
if (category === "fact" && !tombstoneBlocked) {
|
|
3791
|
+
// Rule 44 (#1579): a tombstone-blocked fact MUST NOT be registered as an
|
|
3792
|
+
// active dedup/index entry — otherwise the block is invisible to dedup
|
|
3793
|
+
// and the content is silently banned on the next extraction. Only active
|
|
3794
|
+
// (un-blocked) facts enter the hash index.
|
|
3516
3795
|
try {
|
|
3517
3796
|
const factHashIndex = await this.getFactHashIndex();
|
|
3518
3797
|
// When the caller provides a separate contentHashSource (e.g. the raw
|
|
@@ -3566,6 +3845,23 @@ export class StorageManager {
|
|
|
3566
3845
|
await factHashIndex.save();
|
|
3567
3846
|
}
|
|
3568
3847
|
|
|
3848
|
+
/**
|
|
3849
|
+
* Re-register a fact's contentHash in the dedup index after a tombstone
|
|
3850
|
+
* block is lifted on approval (issue #1579 thread ObnTy). `writeMemory`
|
|
3851
|
+
* skips hash-index registration for tombstone-blocked facts (rule 44); when
|
|
3852
|
+
* the review queue later promotes such a fact back to `status: active`, the
|
|
3853
|
+
* hash must enter the index or the next extraction of the same content
|
|
3854
|
+
* creates a second active fact. Reads the memory by id so the caller (the
|
|
3855
|
+
* review CLI) does not need to re-parse the file. No-op for non-facts or
|
|
3856
|
+
* facts that are not active.
|
|
3857
|
+
*/
|
|
3858
|
+
async restoreFactHashAfterApproval(memoryId: string): Promise<void> {
|
|
3859
|
+
const all = await this.readAllMemories();
|
|
3860
|
+
const memory = all.find((m) => m.frontmatter.id === memoryId);
|
|
3861
|
+
if (!memory) return;
|
|
3862
|
+
await this.addActiveFactContentHash(memory);
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3569
3865
|
private async syncFactHashIndexAfterRewrite(before: MemoryFile, after: MemoryFile): Promise<void> {
|
|
3570
3866
|
if (before.frontmatter.category !== "fact" && after.frontmatter.category !== "fact") return;
|
|
3571
3867
|
|
|
@@ -4068,6 +4364,15 @@ export class StorageManager {
|
|
|
4068
4364
|
// recall cache was never invalidated on mutations, so recall served
|
|
4069
4365
|
// pre-edit bundles for the remainder of its fresh/stale TTL window.
|
|
4070
4366
|
invalidateAllForDir(this.baseDir);
|
|
4367
|
+
// Issue #1579 — tombstone store is a cache layer too (rule 25: clear ALL
|
|
4368
|
+
// cache layers). A tombstone append from another path (supersession /
|
|
4369
|
+
// correction) must be visible to the next writeMemory lookup on this
|
|
4370
|
+
// instance without waiting for a process restart.
|
|
4371
|
+
if (this.tombstoneStore) {
|
|
4372
|
+
this.tombstoneStore.invalidate();
|
|
4373
|
+
this.tombstoneStore = null;
|
|
4374
|
+
this.tombstoneStoreLoadPromise = null;
|
|
4375
|
+
}
|
|
4071
4376
|
}
|
|
4072
4377
|
|
|
4073
4378
|
/**
|
|
@@ -7133,6 +7438,28 @@ export class StorageManager {
|
|
|
7133
7438
|
this.bumpMemoryStatusVersion();
|
|
7134
7439
|
log.debug(`superseded memory ${oldMemoryId} by ${newMemoryId}: ${reason}`);
|
|
7135
7440
|
|
|
7441
|
+
// Issue #1579 — emit a tombstone so the superseded fact cannot
|
|
7442
|
+
// resurrect. Contradiction resolution verbs (keep-a/keep-b/merge) all
|
|
7443
|
+
// funnel through supersedeMemory, so emitting here covers every verb
|
|
7444
|
+
// exactly once (rule 22). Only facts participate. Thread Oci-Y: emit
|
|
7445
|
+
// one tombstone PER derived supersession key (buildRetiredFactTombstoneInputs)
|
|
7446
|
+
// so a paraphrased re-write is caught on the keyed tier — a single
|
|
7447
|
+
// entityRef-only record missed and the fact resurrected until rebuild.
|
|
7448
|
+
if (oldMemory.frontmatter.category === "fact") {
|
|
7449
|
+
for (const input of buildRetiredFactTombstoneInputs(
|
|
7450
|
+
{
|
|
7451
|
+
id: oldMemoryId,
|
|
7452
|
+
content: stripCitationForTemplate(oldMemory.content, this.citationTemplate),
|
|
7453
|
+
contentHash: oldMemory.frontmatter.contentHash,
|
|
7454
|
+
entityRef: oldMemory.frontmatter.entityRef,
|
|
7455
|
+
structuredAttributes: oldMemory.frontmatter.structuredAttributes,
|
|
7456
|
+
},
|
|
7457
|
+
{ reason: "contradiction_resolution", createdBy: "contradiction_resolution", createdAt: now, supersessionKeysForFact },
|
|
7458
|
+
)) {
|
|
7459
|
+
await this.appendTombstone(input);
|
|
7460
|
+
}
|
|
7461
|
+
}
|
|
7462
|
+
|
|
7136
7463
|
// Also write a correction entry for the audit trail
|
|
7137
7464
|
await this.writeMemory("correction", `Superseded: ${oldMemory.content}\n\nReason: ${reason}`, {
|
|
7138
7465
|
confidence: 1.0,
|
|
@@ -2,7 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { mkdtemp, mkdir, open, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { mkdtemp, mkdir, open, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
import { HourlySummarizer } from "./summarizer.js";
|
|
7
7
|
import {
|
|
8
8
|
readSummarySnapshot,
|
|
@@ -756,3 +756,65 @@ test("hourly transcript lookup reads encoded transcript channel directories", as
|
|
|
756
756
|
["encoded transcript"],
|
|
757
757
|
);
|
|
758
758
|
});
|
|
759
|
+
|
|
760
|
+
// ── Issue #1524 adoption prove-fail: summary-snapshot upserts must run through
|
|
761
|
+
// the shared serializeMutations chain (rejection-recovering). The defect class
|
|
762
|
+
// is a naive bare-.then(fn) chain that silently drops subsequent tasks after a
|
|
763
|
+
// rejection: if upsert A rejects (e.g. transient I/O error) and the chain does
|
|
764
|
+
// not recover, upsert B queued behind it never runs — its hour goes missing
|
|
765
|
+
// from the snapshot. We seed the failure by rejecting the first upsert (a
|
|
766
|
+
// hostile lock directory that throws on mkdir), then upsert B must STILL land
|
|
767
|
+
// its hour. Pre-fix (a poison chain) B would be skipped.
|
|
768
|
+
test("upsertSummarySnapshot recovers after a prior upsert rejects (issue #1524 poison-chain prove-fail)", async () => {
|
|
769
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-summary-poison-chain-"));
|
|
770
|
+
const sessionKey = "session-poison";
|
|
771
|
+
try {
|
|
772
|
+
// The lock directory for sessionKey is <root>/state/summaries/<encoded>.lock.
|
|
773
|
+
// Pre-create a FILE at the summaries root so the upsert's mkdir(dirname)
|
|
774
|
+
// inside writeSummarySnapshot throws ENOTDIR — a deterministic rejection
|
|
775
|
+
// that does not depend on filesystem timing. Both upserts target the SAME
|
|
776
|
+
// sessionKey so they serialize through the same chain.
|
|
777
|
+
const summariesRoot = path.join(memoryDir, "state", "summaries");
|
|
778
|
+
await mkdir(path.dirname(summariesRoot), { recursive: true });
|
|
779
|
+
await writeFile(summariesRoot, "not-a-dir", "utf-8");
|
|
780
|
+
|
|
781
|
+
const summaryA = {
|
|
782
|
+
hour: "2026-03-26T08:00:00.000Z",
|
|
783
|
+
sessionKey,
|
|
784
|
+
bullets: ["A"],
|
|
785
|
+
turnCount: 1,
|
|
786
|
+
generatedAt: "2026-03-26T08:15:00.000Z",
|
|
787
|
+
};
|
|
788
|
+
const summaryB = {
|
|
789
|
+
hour: "2026-03-26T09:00:00.000Z",
|
|
790
|
+
sessionKey,
|
|
791
|
+
bullets: ["B"],
|
|
792
|
+
turnCount: 1,
|
|
793
|
+
generatedAt: "2026-03-26T09:15:00.000Z",
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Upsert A rejects (hostile summaries root). We MUST catch — the contract
|
|
797
|
+
// is that the chain recovers, not that the failing op silently succeeds.
|
|
798
|
+
await upsertSummarySnapshot(memoryDir, summaryA).then(
|
|
799
|
+
() => assert.fail("upsert A should have rejected on the hostile summaries root"),
|
|
800
|
+
() => undefined,
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
// Restore a usable summaries root so upsert B can actually write.
|
|
804
|
+
await unlink(summariesRoot);
|
|
805
|
+
await mkdir(summariesRoot, { recursive: true });
|
|
806
|
+
|
|
807
|
+
// Upsert B MUST still run despite A's rejection. With a poison chain B
|
|
808
|
+
// would be skipped (no snapshot, no B-hour bullet).
|
|
809
|
+
await upsertSummarySnapshot(memoryDir, summaryB);
|
|
810
|
+
|
|
811
|
+
const snapshot = await readSummarySnapshot(memoryDir, sessionKey);
|
|
812
|
+
assert.ok(snapshot, "upsert B landed after upsert A rejected (chain recovered)");
|
|
813
|
+
assert.ok(
|
|
814
|
+
snapshot.some((s) => s.bullets.includes("B")),
|
|
815
|
+
"upsert B's hour survived — the chain was not poisoned by A's rejection",
|
|
816
|
+
);
|
|
817
|
+
} finally {
|
|
818
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
819
|
+
}
|
|
820
|
+
});
|