@remnic/core 9.3.729 → 9.3.730
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 +6 -5
- package/dist/access-boundary.js +13 -13
- package/dist/access-cli.js +34 -33
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +19 -19
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +18 -18
- package/dist/access-operations-batch.js +16 -16
- package/dist/access-operations.d.ts +11 -10
- package/dist/access-operations.js +17 -17
- package/dist/access-schema.d.ts +42 -42
- package/dist/{access-service-BXMJqkTA.d.ts → access-service-Byoe3HsT.d.ts} +18 -3
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +12 -12
- package/dist/access-surface-catalog.d.ts +6 -5
- 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 +2 -2
- package/dist/{auto-sync-F3KCSQFJ.js → auto-sync-G3V5AN4C.js} +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -4
- 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/capabilities.d.ts +55 -2
- package/dist/capabilities.js +5 -1
- package/dist/{catalog-bMczlO2G.d.ts → catalog-DM1nk7cM.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/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-4NOI577P.js → chunk-24ZLC5QN.js} +2 -2
- package/dist/{chunk-DMQWV6SQ.js → chunk-5RZOFIDL.js} +2 -2
- package/dist/{chunk-UM5AWJNQ.js → chunk-646B5ERJ.js} +62 -37
- package/dist/chunk-646B5ERJ.js.map +1 -0
- package/dist/{chunk-H5BWGCRZ.js → chunk-6TUWSUUD.js} +51 -47
- package/dist/{chunk-H5BWGCRZ.js.map → chunk-6TUWSUUD.js.map} +1 -1
- package/dist/{chunk-36SEDFAU.js → chunk-C5WN74HH.js} +2 -2
- package/dist/{chunk-MLKWJOAJ.js → chunk-CGPHMNL4.js} +2 -2
- package/dist/{chunk-R6OVFAX6.js → chunk-DJECGBBM.js} +7 -2
- package/dist/{chunk-R6OVFAX6.js.map → chunk-DJECGBBM.js.map} +1 -1
- package/dist/{chunk-3RCBIOOJ.js → chunk-ESF7TRYJ.js} +18 -2
- package/dist/{chunk-3RCBIOOJ.js.map → chunk-ESF7TRYJ.js.map} +1 -1
- package/dist/{chunk-EY6MDMHK.js → chunk-F6OEXJQQ.js} +2 -2
- package/dist/{chunk-M4V2JVSZ.js → chunk-FFUCCKPN.js} +2 -2
- package/dist/{chunk-O6YH5F47.js → chunk-FYWUNVFZ.js} +8 -8
- package/dist/{chunk-37436GML.js → chunk-GEXI357K.js} +2 -2
- package/dist/{chunk-YGIQORJ3.js → chunk-GGL34GCW.js} +3 -3
- package/dist/{chunk-4H4F3BFL.js → chunk-GXTYEG7Q.js} +3 -3
- package/dist/{chunk-4H4F3BFL.js.map → chunk-GXTYEG7Q.js.map} +1 -1
- package/dist/{chunk-WAS3NV46.js → chunk-H3MB3F62.js} +2 -2
- package/dist/{chunk-UTHQPSRC.js → chunk-HHNSLPX7.js} +11 -8
- package/dist/chunk-HHNSLPX7.js.map +1 -0
- package/dist/{chunk-76NHR6ZL.js → chunk-HJWQJUKQ.js} +4 -4
- package/dist/{chunk-Y4YD5TYW.js → chunk-IIBFCZA7.js} +3 -3
- package/dist/{chunk-UVPL565C.js → chunk-IQOEW5YV.js} +2 -2
- package/dist/{chunk-V4GNSDFH.js → chunk-KQYGNGON.js} +2 -2
- package/dist/{chunk-CBWI22JN.js → chunk-KSJCXXOU.js} +2 -2
- package/dist/{chunk-M4AYVEWS.js → chunk-LUCARK4W.js} +12 -11
- package/dist/chunk-LUCARK4W.js.map +1 -0
- package/dist/{chunk-I5REUWLH.js → chunk-NIZQH6AU.js} +3 -3
- package/dist/{chunk-4ESAR77N.js → chunk-NOU77OHX.js} +2 -2
- package/dist/{chunk-BQCHEV7O.js → chunk-O4ZIBPIW.js} +3 -3
- package/dist/{chunk-HOHVIVDR.js → chunk-OBJUKYDO.js} +2 -2
- package/dist/{chunk-YKBHOBJK.js → chunk-OEHCMALU.js} +56 -4
- package/dist/chunk-OEHCMALU.js.map +1 -0
- package/dist/{chunk-YOLZS7NY.js → chunk-OTQE66IX.js} +2 -2
- package/dist/{chunk-HEDZUSCS.js → chunk-OUITHS6V.js} +246 -368
- package/dist/chunk-OUITHS6V.js.map +1 -0
- package/dist/{chunk-4T3BMHKD.js → chunk-OZECYVGI.js} +20 -7
- package/dist/chunk-OZECYVGI.js.map +1 -0
- package/dist/{chunk-C2PG7IEH.js → chunk-P3VJCTVE.js} +2 -2
- package/dist/{chunk-EH5ED3C6.js → chunk-PMAQRUN4.js} +4 -4
- package/dist/chunk-PMAQRUN4.js.map +1 -0
- package/dist/{chunk-7RLCCNHU.js → chunk-Q6BAA57B.js} +4 -3
- package/dist/chunk-Q6BAA57B.js.map +1 -0
- package/dist/{chunk-HGNOCPZX.js → chunk-QZHF4BWN.js} +6 -6
- package/dist/chunk-SYX73XD7.js +283 -0
- package/dist/chunk-SYX73XD7.js.map +1 -0
- package/dist/{chunk-ASKXIAHD.js → chunk-WDJCZQB4.js} +2 -2
- package/dist/{chunk-AQLOP5F4.js → chunk-WNGXQMRI.js} +95 -6
- package/dist/chunk-WNGXQMRI.js.map +1 -0
- package/dist/{chunk-JYLFZWUX.js → chunk-XYUFGHUW.js} +2 -2
- package/dist/{chunk-3TSTGI6F.js → chunk-ZQUAVZNG.js} +2 -2
- package/dist/{chunk-4NR2PDZ5.js → chunk-ZVHRSR2R.js} +14 -4
- package/dist/chunk-ZVHRSR2R.js.map +1 -0
- package/dist/{cli-DmAiDd8A.d.ts → cli-Dt-tuiYH.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +35 -35
- package/dist/compounding/engine.d.ts +12 -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 +2 -2
- 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 +1 -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/embedding-fallback.js +2 -2
- 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 +6 -4
- package/dist/explicit-capture.js +1 -1
- 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/extraction.js +3 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-22LPFHD5.js → first-start-migration-OOR57AYR.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +296 -11
- package/dist/index.js +359 -47
- 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/local-model-endpoint.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 +6 -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-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 +9 -9
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +5 -5
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +3 -3
- 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/orchestration/compression-guideline-coordinator.d.ts +105 -0
- package/dist/orchestration/compression-guideline-coordinator.js +12 -0
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-BR0sGsME.d.ts → orchestrator-B4KjaOtY.d.ts} +24 -8
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +28 -26
- 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/provenance.js +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-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-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/{resolution-MN36NW5P.js → resolution-33PIX3TT.js} +2 -2
- package/dist/resolution-33PIX3TT.js.map +1 -0
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -3
- 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 +84 -84
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/embed-helper.js +2 -2
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +4 -4
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +8 -8
- 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-Cl15ksZy.d.ts → semantic-consolidation-CstTvHPo.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 +3 -3
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +22 -3
- 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 +46 -46
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ChQb9D6s.d.ts → types-VkxElCHR.d.ts} +8 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +12 -2
- package/src/access-service-coding-write.test.ts +2 -2
- package/src/access-service.ts +33 -14
- package/src/bitemporal-dedup-backfill.test.ts +10 -10
- package/src/bitemporal-frontmatter.test.ts +3 -3
- package/src/capabilities.test.ts +185 -0
- package/src/capabilities.ts +110 -0
- package/src/cli/creation-ledger-commands.ts +22 -21
- package/src/coding/architecture-surfaces.test.ts +25 -1
- package/src/coding/architecture-surfaces.ts +47 -19
- package/src/coding/decision-surfaces.ts +12 -3
- package/src/coding/review-context-structural.test.ts +303 -0
- package/src/coding/review-context.ts +79 -0
- package/src/coding/structural-context.test.ts +259 -0
- package/src/coding/structural-context.ts +315 -0
- package/src/coding/structural-subprocess-provider.test.ts +430 -0
- package/src/coding/structural-subprocess-provider.ts +355 -0
- package/src/compounding/engine.test.ts +90 -0
- package/src/compounding/engine.ts +25 -1
- package/src/contradiction/contradiction.test.ts +1 -1
- package/src/contradiction/resolution.ts +9 -1
- package/src/correction/correction-access-wiring.ts +21 -3
- package/src/correction/correction-contract.ts +7 -0
- package/src/correction/correction-planner.test.ts +231 -0
- package/src/correction/correction-planner.ts +73 -0
- package/src/correction/correction-service.ts +29 -0
- package/src/correction/correction-storage-integration.test.ts +2 -2
- package/src/dedup/semantic.test.ts +1 -1
- package/src/explicit-capture.ts +7 -4
- package/src/extraction-eventtime-wiring.test.ts +3 -3
- package/src/extraction-redaction-rules.test.ts +172 -0
- package/src/index.ts +29 -0
- package/src/maintenance/forget.test.ts +1 -1
- package/src/maintenance/memory-governance.test.ts +1 -1
- package/src/maintenance/pattern-reinforcement.test.ts +2 -2
- package/src/operator-toolkit.ts +4 -3
- package/src/orchestration/compression-guideline-coordinator.ts +449 -0
- package/src/orchestrator.ts +345 -464
- package/src/procedural/procedure-miner.test.ts +2 -2
- package/src/provenance-extraction.test.ts +382 -4
- package/src/provenance.ts +158 -22
- package/src/recall-handles-wiring.test.ts +1 -1
- package/src/search/factory.ts +4 -3
- package/src/semantic-rule-promotion.ts +1 -1
- package/src/source-attribution-roundtrip.test.ts +2 -2
- package/src/storage-contract/atomicity.test.ts +1 -1
- package/src/storage-contract/cache-coherence.test.ts +2 -2
- package/src/storage-contract/content-hash.test.ts +1 -1
- package/src/storage-contract/namespace-routing.test.ts +1 -1
- package/src/storage-contract/round-trip.test.ts +3 -3
- package/src/storage.ts +23 -2
- package/src/temporal-supersession.test.ts +12 -11
- package/src/temporal-validity.test.ts +11 -11
- package/src/wearables/cli.test.ts +30 -0
- package/src/wearables/cli.ts +3 -0
- package/src/wearables/memory-gen.test.ts +57 -1
- package/src/wearables/memory-gen.ts +31 -7
- package/src/wearables/pipeline.test.ts +40 -1
- package/src/wearables/pipeline.ts +4 -0
- package/src/wearables/service.test.ts +2 -2
- package/src/wearables/storage-io.test.ts +3 -3
- package/src/wearables/types.ts +7 -0
- package/dist/chunk-4NR2PDZ5.js.map +0 -1
- package/dist/chunk-4T3BMHKD.js.map +0 -1
- package/dist/chunk-7RLCCNHU.js.map +0 -1
- package/dist/chunk-AQLOP5F4.js.map +0 -1
- package/dist/chunk-EH5ED3C6.js.map +0 -1
- package/dist/chunk-HEDZUSCS.js.map +0 -1
- package/dist/chunk-M4AYVEWS.js.map +0 -1
- package/dist/chunk-UM5AWJNQ.js.map +0 -1
- package/dist/chunk-UTHQPSRC.js.map +0 -1
- package/dist/chunk-YKBHOBJK.js.map +0 -1
- /package/dist/{auto-sync-F3KCSQFJ.js.map → auto-sync-G3V5AN4C.js.map} +0 -0
- /package/dist/{chunk-4NOI577P.js.map → chunk-24ZLC5QN.js.map} +0 -0
- /package/dist/{chunk-DMQWV6SQ.js.map → chunk-5RZOFIDL.js.map} +0 -0
- /package/dist/{chunk-36SEDFAU.js.map → chunk-C5WN74HH.js.map} +0 -0
- /package/dist/{chunk-MLKWJOAJ.js.map → chunk-CGPHMNL4.js.map} +0 -0
- /package/dist/{chunk-EY6MDMHK.js.map → chunk-F6OEXJQQ.js.map} +0 -0
- /package/dist/{chunk-M4V2JVSZ.js.map → chunk-FFUCCKPN.js.map} +0 -0
- /package/dist/{chunk-O6YH5F47.js.map → chunk-FYWUNVFZ.js.map} +0 -0
- /package/dist/{chunk-37436GML.js.map → chunk-GEXI357K.js.map} +0 -0
- /package/dist/{chunk-YGIQORJ3.js.map → chunk-GGL34GCW.js.map} +0 -0
- /package/dist/{chunk-WAS3NV46.js.map → chunk-H3MB3F62.js.map} +0 -0
- /package/dist/{chunk-76NHR6ZL.js.map → chunk-HJWQJUKQ.js.map} +0 -0
- /package/dist/{chunk-Y4YD5TYW.js.map → chunk-IIBFCZA7.js.map} +0 -0
- /package/dist/{chunk-UVPL565C.js.map → chunk-IQOEW5YV.js.map} +0 -0
- /package/dist/{chunk-V4GNSDFH.js.map → chunk-KQYGNGON.js.map} +0 -0
- /package/dist/{chunk-CBWI22JN.js.map → chunk-KSJCXXOU.js.map} +0 -0
- /package/dist/{chunk-I5REUWLH.js.map → chunk-NIZQH6AU.js.map} +0 -0
- /package/dist/{chunk-4ESAR77N.js.map → chunk-NOU77OHX.js.map} +0 -0
- /package/dist/{chunk-BQCHEV7O.js.map → chunk-O4ZIBPIW.js.map} +0 -0
- /package/dist/{chunk-HOHVIVDR.js.map → chunk-OBJUKYDO.js.map} +0 -0
- /package/dist/{chunk-YOLZS7NY.js.map → chunk-OTQE66IX.js.map} +0 -0
- /package/dist/{chunk-C2PG7IEH.js.map → chunk-P3VJCTVE.js.map} +0 -0
- /package/dist/{chunk-HGNOCPZX.js.map → chunk-QZHF4BWN.js.map} +0 -0
- /package/dist/{chunk-ASKXIAHD.js.map → chunk-WDJCZQB4.js.map} +0 -0
- /package/dist/{chunk-JYLFZWUX.js.map → chunk-XYUFGHUW.js.map} +0 -0
- /package/dist/{chunk-3TSTGI6F.js.map → chunk-ZQUAVZNG.js.map} +0 -0
- /package/dist/{first-start-migration-22LPFHD5.js.map → first-start-migration-OOR57AYR.js.map} +0 -0
- /package/dist/{resolution-MN36NW5P.js.map → orchestration/compression-guideline-coordinator.js.map} +0 -0
|
@@ -491,3 +491,234 @@ test("#1678: never_store plan redacts the request text in the persisted pending-
|
|
|
491
491
|
assert.ok(reloaded!.request.text.includes("redacted"));
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// #1713 Item 2: stale applying plan recovery
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
test("#1713: markConsumed(applying) stamps applyingAt", async () => {
|
|
500
|
+
await withTempDir(async (dir) => {
|
|
501
|
+
const state: StubState = {
|
|
502
|
+
candidatesById: new Map([
|
|
503
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })],
|
|
504
|
+
]),
|
|
505
|
+
llmResult: {
|
|
506
|
+
classification: "outdated",
|
|
507
|
+
confidence: 0.9,
|
|
508
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
509
|
+
relevance: [],
|
|
510
|
+
warnings: [],
|
|
511
|
+
},
|
|
512
|
+
writeReplacementCalls: 0,
|
|
513
|
+
propagateCalls: 0,
|
|
514
|
+
retireCalls: 0,
|
|
515
|
+
};
|
|
516
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
517
|
+
const plan = await planner.plan({ text: "correct this" }, ["default"]);
|
|
518
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
519
|
+
const reloaded = await planner.loadPlan("default", plan.planId);
|
|
520
|
+
assert.equal(reloaded!.status, "applying");
|
|
521
|
+
assert.ok(reloaded!.applyingAt, "applyingAt must be stamped when entering applying state");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("#1713: markConsumed(applied) clears applyingAt", async () => {
|
|
526
|
+
await withTempDir(async (dir) => {
|
|
527
|
+
const state: StubState = {
|
|
528
|
+
candidatesById: new Map([
|
|
529
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })],
|
|
530
|
+
]),
|
|
531
|
+
llmResult: {
|
|
532
|
+
classification: "outdated",
|
|
533
|
+
confidence: 0.9,
|
|
534
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
535
|
+
relevance: [],
|
|
536
|
+
warnings: [],
|
|
537
|
+
},
|
|
538
|
+
writeReplacementCalls: 0,
|
|
539
|
+
propagateCalls: 0,
|
|
540
|
+
retireCalls: 0,
|
|
541
|
+
};
|
|
542
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
543
|
+
const plan = await planner.plan({ text: "correct this" }, ["default"]);
|
|
544
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
545
|
+
await planner.markConsumed("default", plan.planId, "applied");
|
|
546
|
+
const reloaded = await planner.loadPlan("default", plan.planId);
|
|
547
|
+
assert.equal(reloaded!.status, "applied");
|
|
548
|
+
assert.ok(!reloaded!.applyingAt, "applyingAt must be cleared on terminal status");
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("#1713: recoverStaleApplyingPlans discards stale applying plans", async () => {
|
|
553
|
+
await withTempDir(async (dir) => {
|
|
554
|
+
const state: StubState = {
|
|
555
|
+
candidatesById: new Map([
|
|
556
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "secret-token-1234" })],
|
|
557
|
+
]),
|
|
558
|
+
llmResult: {
|
|
559
|
+
classification: "never_store",
|
|
560
|
+
confidence: 0.9,
|
|
561
|
+
actions: [{ kind: "redaction_rule", pattern: "secret-token-\\d+" }],
|
|
562
|
+
relevance: [],
|
|
563
|
+
warnings: [],
|
|
564
|
+
},
|
|
565
|
+
writeReplacementCalls: 0,
|
|
566
|
+
propagateCalls: 0,
|
|
567
|
+
retireCalls: 0,
|
|
568
|
+
};
|
|
569
|
+
const planTime = new Date("2026-03-15T12:00:00Z");
|
|
570
|
+
const recoveryTime = new Date("2026-03-15T13:00:00Z");
|
|
571
|
+
const deps = makeDeps(dir, state);
|
|
572
|
+
deps.now = () => planTime;
|
|
573
|
+
const planner = new CorrectionPlanner(deps);
|
|
574
|
+
const plan = await planner.plan({ text: "never store this secret", targetIds: ["mem-x"] }, ["default"]);
|
|
575
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
576
|
+
|
|
577
|
+
let reloaded = await planner.loadPlan("default", plan.planId);
|
|
578
|
+
assert.equal(reloaded!.status, "applying");
|
|
579
|
+
|
|
580
|
+
const recovered = await planner.recoverStaleApplyingPlans("default", { now: recoveryTime });
|
|
581
|
+
assert.equal(recovered.length, 1);
|
|
582
|
+
assert.equal(recovered[0], plan.planId);
|
|
583
|
+
|
|
584
|
+
// Read the raw file (loadPlan/parsePlan rejects the scrubbed pattern by
|
|
585
|
+
// design — validation rejects placeholder patterns on re-load).
|
|
586
|
+
const { readFile: rf } = await import("node:fs/promises");
|
|
587
|
+
const rawPlan = JSON.parse(
|
|
588
|
+
await rf(dir + "/state/corrections/pending/" + plan.planId + ".json", "utf-8"),
|
|
589
|
+
);
|
|
590
|
+
assert.equal(rawPlan.status, "discarded");
|
|
591
|
+
assert.ok(!rawPlan.applyingAt, "applyingAt cleared on discard");
|
|
592
|
+
const redactionAction = rawPlan.actions.find(
|
|
593
|
+
(a: { kind: string }) => a.kind === "redaction_rule",
|
|
594
|
+
);
|
|
595
|
+
assert.ok(redactionAction, "redaction_rule action should still exist");
|
|
596
|
+
assert.match(String(redactionAction.pattern), /redacted/);
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("#1713: recoverStaleApplyingPlans leaves fresh applying plans alone", async () => {
|
|
601
|
+
await withTempDir(async (dir) => {
|
|
602
|
+
const state: StubState = {
|
|
603
|
+
candidatesById: new Map([
|
|
604
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })],
|
|
605
|
+
]),
|
|
606
|
+
llmResult: {
|
|
607
|
+
classification: "outdated",
|
|
608
|
+
confidence: 0.9,
|
|
609
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
610
|
+
relevance: [],
|
|
611
|
+
warnings: [],
|
|
612
|
+
},
|
|
613
|
+
writeReplacementCalls: 0,
|
|
614
|
+
propagateCalls: 0,
|
|
615
|
+
retireCalls: 0,
|
|
616
|
+
};
|
|
617
|
+
const fixedNow = new Date("2026-03-15T12:00:00Z");
|
|
618
|
+
const deps = makeDeps(dir, state);
|
|
619
|
+
const planner = new CorrectionPlanner(deps);
|
|
620
|
+
const plan = await planner.plan({ text: "correct this" }, ["default"]);
|
|
621
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
622
|
+
|
|
623
|
+
const recovered = await planner.recoverStaleApplyingPlans("default", {
|
|
624
|
+
now: new Date(fixedNow.getTime() + 2 * 60 * 1000),
|
|
625
|
+
});
|
|
626
|
+
assert.equal(recovered.length, 0);
|
|
627
|
+
|
|
628
|
+
const reloaded = await planner.loadPlan("default", plan.planId);
|
|
629
|
+
assert.equal(reloaded!.status, "applying");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("#1713: recoverStaleApplyingPlans returns empty for namespace with no plans", async () => {
|
|
634
|
+
await withTempDir(async (dir) => {
|
|
635
|
+
const state: StubState = {
|
|
636
|
+
candidatesById: new Map(),
|
|
637
|
+
llmResult: { classification: "outdated", confidence: 0.5, actions: [], relevance: [], warnings: [] },
|
|
638
|
+
writeReplacementCalls: 0,
|
|
639
|
+
propagateCalls: 0,
|
|
640
|
+
retireCalls: 0,
|
|
641
|
+
};
|
|
642
|
+
const planner = new CorrectionPlanner(makeDeps(dir, state));
|
|
643
|
+
const recovered = await planner.recoverStaleApplyingPlans("default");
|
|
644
|
+
assert.deepEqual(recovered, []);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("#1713: recoverStaleApplyingPlans recovers pre-fix plans via expiresAt fallback", async () => {
|
|
649
|
+
await withTempDir(async (dir) => {
|
|
650
|
+
const state: StubState = {
|
|
651
|
+
candidatesById: new Map([
|
|
652
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })],
|
|
653
|
+
]),
|
|
654
|
+
llmResult: {
|
|
655
|
+
classification: "outdated",
|
|
656
|
+
confidence: 0.9,
|
|
657
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
658
|
+
relevance: [],
|
|
659
|
+
warnings: [],
|
|
660
|
+
},
|
|
661
|
+
writeReplacementCalls: 0,
|
|
662
|
+
propagateCalls: 0,
|
|
663
|
+
retireCalls: 0,
|
|
664
|
+
};
|
|
665
|
+
const fixedNow = new Date("2026-03-15T12:00:00Z");
|
|
666
|
+
const deps = makeDeps(dir, state);
|
|
667
|
+
const planner = new CorrectionPlanner(deps);
|
|
668
|
+
const plan = await planner.plan({ text: "correct this" }, ["default"]);
|
|
669
|
+
|
|
670
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
671
|
+
// Simulate a pre-fix plan: manually strip applyingAt from the file
|
|
672
|
+
const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
|
|
673
|
+
const planFile = dir + "/state/corrections/pending/" + plan.planId + ".json";
|
|
674
|
+
const planJson = JSON.parse(await rf(planFile, "utf-8"));
|
|
675
|
+
delete planJson.applyingAt;
|
|
676
|
+
await wf(planFile, JSON.stringify(planJson) + "\n", "utf-8");
|
|
677
|
+
|
|
678
|
+
// Recovery 25h later (past the 24h expiry, no applyingAt → falls back to expiresAt)
|
|
679
|
+
const recovered = await planner.recoverStaleApplyingPlans("default", {
|
|
680
|
+
now: new Date(fixedNow.getTime() + 25 * 60 * 60 * 1000),
|
|
681
|
+
});
|
|
682
|
+
assert.equal(recovered.length, 1);
|
|
683
|
+
assert.equal(recovered[0], plan.planId);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("#1713: recoverStaleApplyingPlans recovers pre-fix plans immediately at expiry (no extra TTL wait)", async () => {
|
|
688
|
+
await withTempDir(async (dir) => {
|
|
689
|
+
const state: StubState = {
|
|
690
|
+
candidatesById: new Map([
|
|
691
|
+
["mem-x", makeCandidate({ memoryId: "mem-x", content: "old fact" })],
|
|
692
|
+
]),
|
|
693
|
+
llmResult: {
|
|
694
|
+
classification: "outdated",
|
|
695
|
+
confidence: 0.9,
|
|
696
|
+
actions: [{ kind: "supersede", loserId: "mem-x", replacement: { content: "new fact" } }],
|
|
697
|
+
relevance: [],
|
|
698
|
+
warnings: [],
|
|
699
|
+
},
|
|
700
|
+
writeReplacementCalls: 0,
|
|
701
|
+
propagateCalls: 0,
|
|
702
|
+
retireCalls: 0,
|
|
703
|
+
};
|
|
704
|
+
const fixedNow = new Date("2026-03-15T12:00:00Z");
|
|
705
|
+
const deps = makeDeps(dir, state);
|
|
706
|
+
const planner = new CorrectionPlanner(deps);
|
|
707
|
+
const plan = await planner.plan({ text: "correct this" }, ["default"]);
|
|
708
|
+
|
|
709
|
+
await planner.markConsumed("default", plan.planId, "applying");
|
|
710
|
+
// Simulate a pre-fix plan: manually strip applyingAt from the file
|
|
711
|
+
const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
|
|
712
|
+
const planFile = dir + "/state/corrections/pending/" + plan.planId + ".json";
|
|
713
|
+
const planJson = JSON.parse(await rf(planFile, "utf-8"));
|
|
714
|
+
delete planJson.applyingAt;
|
|
715
|
+
await wf(planFile, JSON.stringify(planJson) + "\n", "utf-8");
|
|
716
|
+
|
|
717
|
+
// Recovery at exactly expiresAt: plan is already expired -> recover now
|
|
718
|
+
// (review thread ff034716: no extra TTL wait for pre-fix plans)
|
|
719
|
+
const recovered = await planner.recoverStaleApplyingPlans("default", {
|
|
720
|
+
now: new Date(planJson.expiresAt),
|
|
721
|
+
});
|
|
722
|
+
assert.equal(recovered.length, 1, "pre-fix plan at expiry must be recovered immediately");
|
|
723
|
+
});
|
|
724
|
+
});
|
|
@@ -478,6 +478,15 @@ export class CorrectionPlanner {
|
|
|
478
478
|
const plan = parsePlan(raw);
|
|
479
479
|
if (!plan) return;
|
|
480
480
|
plan.status = status;
|
|
481
|
+
// #1713 Item 2: stamp applyingAt when entering the applying state so
|
|
482
|
+
// startup recovery can detect plans stuck mid-apply (process death).
|
|
483
|
+
// Cleared on terminal transitions; harmless residue on non-applying
|
|
484
|
+
// plans is ignored by the recovery scan.
|
|
485
|
+
if (status === "applying") {
|
|
486
|
+
plan.applyingAt = this.deps.now().toISOString();
|
|
487
|
+
} else {
|
|
488
|
+
delete plan.applyingAt;
|
|
489
|
+
}
|
|
481
490
|
// #1669 thread P1: scrub redaction_rule patterns from consumed plans so
|
|
482
491
|
// a never-store pattern does not persist on disk after apply/discard.
|
|
483
492
|
// The executor reloads via loadPlan only between "applying" (step 1) and
|
|
@@ -508,6 +517,70 @@ export class CorrectionPlanner {
|
|
|
508
517
|
}
|
|
509
518
|
});
|
|
510
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Stale-applying TTL: a plan in `applying` state longer than this is
|
|
523
|
+
* assumed to be from a crashed process and is recovered on startup
|
|
524
|
+
* (#1713 Item 2). 10 minutes is generous for an apply that normally
|
|
525
|
+
* completes in seconds, yet short enough to avoid leaving stale state.
|
|
526
|
+
*/
|
|
527
|
+
static readonly STALE_APPLYING_TTL_MS = 10 * 60 * 1000;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Recover stale `applying` plans past the stale-applying TTL (#1713 Item 2).
|
|
531
|
+
*
|
|
532
|
+
* A plan stuck in `applying` means the process died mid-apply. Re-applying
|
|
533
|
+
* wholesale would duplicate succeeded actions (replacements, tombstones,
|
|
534
|
+
* audits), so recovery discards + scrubs the plan instead. The operator
|
|
535
|
+
* inspects the outcome and files a NEW plan for any actions that did not
|
|
536
|
+
* complete. Plans without `applyingAt` (predating this fix) are recovered
|
|
537
|
+
* when they are past their `expiresAt` timestamp.
|
|
538
|
+
*
|
|
539
|
+
* @returns the ids of recovered (discarded) plans.
|
|
540
|
+
*/
|
|
541
|
+
async recoverStaleApplyingPlans(
|
|
542
|
+
namespace: string,
|
|
543
|
+
opts?: { ttlMs?: number; now?: Date },
|
|
544
|
+
): Promise<string[]> {
|
|
545
|
+
const ttl = opts?.ttlMs ?? CorrectionPlanner.STALE_APPLYING_TTL_MS;
|
|
546
|
+
const now = (opts?.now ?? this.deps.now()).getTime();
|
|
547
|
+
const dir = await this.pendingDir(namespace);
|
|
548
|
+
let files: string[];
|
|
549
|
+
try {
|
|
550
|
+
files = await readdir(dir);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
553
|
+
if (code === "ENOENT") return [];
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
const recovered: string[] = [];
|
|
557
|
+
for (const f of files) {
|
|
558
|
+
if (!f.endsWith(".json")) continue;
|
|
559
|
+
const planId = f.replace(/\.json$/, "");
|
|
560
|
+
let plan: CorrectionPlan | null;
|
|
561
|
+
try {
|
|
562
|
+
plan = await this.loadPlan(namespace, planId);
|
|
563
|
+
} catch {
|
|
564
|
+
continue; // malformed — skip (rule 34).
|
|
565
|
+
}
|
|
566
|
+
if (!plan || plan.status !== "applying") continue;
|
|
567
|
+
// Post-fix plans (with applyingAt): stale if the applying timestamp is
|
|
568
|
+
// past the TTL (process died mid-apply). Pre-fix plans (no applyingAt):
|
|
569
|
+
// stale as soon as they have expired — no additional TTL wait, since the
|
|
570
|
+
// plan is already past its intended lifetime (review thread ff034716).
|
|
571
|
+
const stale = plan.applyingAt
|
|
572
|
+
? now - new Date(plan.applyingAt).getTime() >= ttl
|
|
573
|
+
: now >= new Date(plan.expiresAt).getTime();
|
|
574
|
+
if (!stale) continue;
|
|
575
|
+
try {
|
|
576
|
+
await this.markConsumed(namespace, planId, "discarded");
|
|
577
|
+
recovered.push(planId);
|
|
578
|
+
} catch {
|
|
579
|
+
// Best-effort: a single failed discard must not abort the scan.
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return recovered;
|
|
583
|
+
}
|
|
511
584
|
}
|
|
512
585
|
|
|
513
586
|
// ---------------------------------------------------------------------------
|
|
@@ -170,6 +170,35 @@ export class CorrectionService {
|
|
|
170
170
|
await this.planner.deletePlan(namespace, planId);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Recover stale `applying` plans across the given namespaces (#1713 Item 2).
|
|
175
|
+
*
|
|
176
|
+
* Startup maintenance: scans each namespace for plans stuck in `applying`
|
|
177
|
+
* past the stale-applying TTL (process died mid-apply) and discards+scrubs
|
|
178
|
+
* them. Unlike plan/apply/listPending, this is NOT a per-caller request —
|
|
179
|
+
* it operates on all supplied namespaces directly because it is an
|
|
180
|
+
* internal maintenance operation invoked by the orchestrator at init.
|
|
181
|
+
*
|
|
182
|
+
* @returns the total count of recovered plans across all namespaces.
|
|
183
|
+
*/
|
|
184
|
+
async recoverStaleApplyingPlans(namespaces: readonly string[]): Promise<number> {
|
|
185
|
+
// Intentionally NOT gated on isEnabled(): stale-applying recovery is a
|
|
186
|
+
// maintenance/cleanup operation, not a user-facing correction. Plans left
|
|
187
|
+
// in 'applying' after a mid-apply crash hold unscrubbed redaction_rule
|
|
188
|
+
// patterns on disk; they must be discarded+scrubbed even if the correction
|
|
189
|
+
// feature is currently disabled (review thread 9e1f07f0).
|
|
190
|
+
let recovered = 0;
|
|
191
|
+
for (const namespace of namespaces) {
|
|
192
|
+
try {
|
|
193
|
+
const ids = await this.planner.recoverStaleApplyingPlans(namespace);
|
|
194
|
+
recovered += ids.length;
|
|
195
|
+
} catch {
|
|
196
|
+
// Best-effort: a single namespace failure must not abort the sweep.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return recovered;
|
|
200
|
+
}
|
|
201
|
+
|
|
173
202
|
private requireEnabled(): void {
|
|
174
203
|
if (!this.deps.isEnabled()) {
|
|
175
204
|
throw new CorrectionContractError(
|
|
@@ -86,7 +86,7 @@ test("#1672 item 3: applyEditMemory recomputes contentHash from the patched body
|
|
|
86
86
|
const { storage, cleanup } = await makeStorage();
|
|
87
87
|
try {
|
|
88
88
|
const wiring = { orchestrator: { getStorage: async () => storage } } as any;
|
|
89
|
-
const id = await storage.writeMemory("fact", "the database is MySQL", { source: "test" });
|
|
89
|
+
const { id: id } = await storage.writeMemory("fact", "the database is MySQL", { source: "test" });
|
|
90
90
|
const before = await storage.getMemoryById(id);
|
|
91
91
|
assert.ok(before?.frontmatter.contentHash, "writeMemory must set an initial contentHash");
|
|
92
92
|
const oldHash = before!.frontmatter.contentHash!;
|
|
@@ -110,7 +110,7 @@ test("#1672 item 3: applyEditMemory leaves non-fact contentHash untouched (only
|
|
|
110
110
|
const { storage, cleanup } = await makeStorage();
|
|
111
111
|
try {
|
|
112
112
|
const wiring = { orchestrator: { getStorage: async () => storage } } as any;
|
|
113
|
-
const id = await storage.writeMemory("preference", "likes dark mode", { source: "test" });
|
|
113
|
+
const { id: id } = await storage.writeMemory("preference", "likes dark mode", { source: "test" });
|
|
114
114
|
await applyEditMemory(wiring, "default", id, "likes light mode");
|
|
115
115
|
const after = await storage.getMemoryById(id);
|
|
116
116
|
assert.equal(after!.content, "likes light mode");
|
|
@@ -901,7 +901,7 @@ test("round 6: chunked parent writeMemory carries supersedes in frontmatter", as
|
|
|
901
901
|
const storage = new StorageManager(memDir);
|
|
902
902
|
const OLD_ID = "fact-old-abc-123";
|
|
903
903
|
|
|
904
|
-
const newId = await storage.writeMemory("fact", "the user prefers dark mode", {
|
|
904
|
+
const { id: newId } = await storage.writeMemory("fact", "the user prefers dark mode", {
|
|
905
905
|
confidence: 0.9,
|
|
906
906
|
tags: ["chunked", "preference"],
|
|
907
907
|
supersedes: OLD_ID,
|
package/src/explicit-capture.ts
CHANGED
|
@@ -418,7 +418,7 @@ export async function persistExplicitCapture(
|
|
|
418
418
|
orchestrator: Orchestrator,
|
|
419
419
|
candidate: ValidExplicitCapture,
|
|
420
420
|
source: ExplicitCaptureSource,
|
|
421
|
-
): Promise<{ id: string; duplicateOf?: string }> {
|
|
421
|
+
): Promise<{ id: string; duplicateOf?: string; tombstoneBlocked?: boolean }> {
|
|
422
422
|
const resolvedNamespace = candidate.namespacePreResolved
|
|
423
423
|
? asTrimmed(candidate.namespace)
|
|
424
424
|
: resolveExplicitCaptureNamespace(orchestrator, candidate.namespace);
|
|
@@ -428,7 +428,10 @@ export async function persistExplicitCapture(
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
const storage = await orchestrator.getStorage(resolvedNamespace);
|
|
431
|
-
|
|
431
|
+
// #1645 (review thread yG-): surface the tombstone block so callers
|
|
432
|
+
// (memory_store tool, access-service HTTP/MCP) can report the capture as
|
|
433
|
+
// queued for review instead of a successfully stored active memory.
|
|
434
|
+
const { id, tombstoneBlocked } = await storage.writeMemory(candidate.category, candidate.content, {
|
|
432
435
|
confidence: candidate.confidence,
|
|
433
436
|
tags: candidate.tags,
|
|
434
437
|
entityRef: candidate.entityRef,
|
|
@@ -450,7 +453,7 @@ export async function persistExplicitCapture(
|
|
|
450
453
|
};
|
|
451
454
|
await storage.appendMemoryLifecycleEvents([event]);
|
|
452
455
|
|
|
453
|
-
return { id };
|
|
456
|
+
return { id, tombstoneBlocked };
|
|
454
457
|
}
|
|
455
458
|
|
|
456
459
|
function buildExplicitCaptureReviewContent(input: ExplicitCaptureInput, reason: string): string {
|
|
@@ -527,7 +530,7 @@ export async function queueExplicitCaptureForReview(
|
|
|
527
530
|
: "fact";
|
|
528
531
|
const requestedTags = sanitizeReviewTags(input.tags);
|
|
529
532
|
const storage = await orchestrator.getStorage(queueNamespace);
|
|
530
|
-
const id = await storage.writeMemory(reviewCategory, content, {
|
|
533
|
+
const { id: id } = await storage.writeMemory(reviewCategory, content, {
|
|
531
534
|
confidence: 0.2,
|
|
532
535
|
tags: Array.from(new Set([...EXPLICIT_CAPTURE_REVIEW_TAGS, ...requestedTags])),
|
|
533
536
|
entityRef: sanitizeReviewMetadata(input.entityRef),
|
|
@@ -153,7 +153,7 @@ test("storage: invalid_at round-trips through writeMemoryFrontmatter → readAll
|
|
|
153
153
|
const storage = new StorageManager(dir);
|
|
154
154
|
await storage.ensureDirectories();
|
|
155
155
|
|
|
156
|
-
const id = await storage.writeMemory("fact", "Old stack was Node 18.", {
|
|
156
|
+
const { id: id } = await storage.writeMemory("fact", "Old stack was Node 18.", {
|
|
157
157
|
validAt: "2025-01-01T00:00:00.000Z",
|
|
158
158
|
observedAt: "2025-06-01T00:00:00.000Z",
|
|
159
159
|
eventTimeSource: "extracted",
|
|
@@ -188,7 +188,7 @@ test("storage: writeMemory accepts invalidAt directly (end-bound at extraction w
|
|
|
188
188
|
try {
|
|
189
189
|
const storage = new StorageManager(dir);
|
|
190
190
|
await storage.ensureDirectories();
|
|
191
|
-
const id = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
191
|
+
const { id: id } = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
192
192
|
validAt: "2024-01-01T00:00:00.000Z",
|
|
193
193
|
invalidAt: "2025-06-01T00:00:00.000Z",
|
|
194
194
|
observedAt: "2025-06-20T00:00:00.000Z",
|
|
@@ -222,7 +222,7 @@ test("storage: writeChunk propagates invalidAt/observedAt/eventTimeSource from t
|
|
|
222
222
|
try {
|
|
223
223
|
const storage = new StorageManager(dir);
|
|
224
224
|
await storage.ensureDirectories();
|
|
225
|
-
const parentId = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
225
|
+
const { id: parentId } = await storage.writeMemory("fact", "We used MongoDB until June 2025.", {
|
|
226
226
|
validAt: "2024-01-01T00:00:00.000Z",
|
|
227
227
|
invalidAt: "2025-06-01T00:00:00.000Z",
|
|
228
228
|
observedAt: "2025-06-20T00:00:00.000Z",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
compileRedactionPattern,
|
|
17
17
|
contentMatchesRedactionRules,
|
|
18
18
|
loadRedactionRules,
|
|
19
|
+
type CompiledRedactionRule,
|
|
19
20
|
} from "./extraction-redaction-rules.js";
|
|
20
21
|
import { validateRedactionPattern } from "./correction/correction-contract.js";
|
|
21
22
|
|
|
@@ -211,3 +212,174 @@ test("#1669 P2: compileRedactionPattern empty body never matches", () => {
|
|
|
211
212
|
assert.equal(rule.matcher("anything"), false);
|
|
212
213
|
assert.equal(rule.matcher(""), false);
|
|
213
214
|
});
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// #1713 Item 1: pre-judge redaction filter consults target namespace rules.
|
|
218
|
+
// The orchestrator's pre-judge filter builds a combined rule set from source +
|
|
219
|
+
// shared + routed target namespace dirs. This test verifies that loadRedactionRules
|
|
220
|
+
// correctly returns rules from each dir and that contentMatchesRedactionRules
|
|
221
|
+
// catches patterns from ANY of the combined dirs (not just the source).
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
test("#1713 Item 1: combined rules from source + target dirs catch cross-namespace patterns", async () => {
|
|
225
|
+
await withTempDir(async (sourceDir) => {
|
|
226
|
+
await withTempDir(async (targetDir) => {
|
|
227
|
+
// Source namespace has a rule for "source-secret"
|
|
228
|
+
await writeRule(sourceDir, "source-secret");
|
|
229
|
+
// Target namespace has a rule for "target-secret"
|
|
230
|
+
await writeRule(targetDir, "target-confidential");
|
|
231
|
+
|
|
232
|
+
// Simulate what the orchestrator's redactionRulesFor(...dirs) does:
|
|
233
|
+
// load from each dir and combine.
|
|
234
|
+
const sourceRules = await loadRedactionRules(sourceDir);
|
|
235
|
+
const targetRules = await loadRedactionRules(targetDir);
|
|
236
|
+
const combined = [...sourceRules, ...targetRules];
|
|
237
|
+
|
|
238
|
+
assert.equal(combined.length, 2);
|
|
239
|
+
// Source-namespace rule catches source content
|
|
240
|
+
assert.ok(
|
|
241
|
+
contentMatchesRedactionRules("leaked source-secret here", combined),
|
|
242
|
+
"source-namespace rule must catch matching content",
|
|
243
|
+
);
|
|
244
|
+
// Target-namespace rule catches target content (the #1713 fix)
|
|
245
|
+
assert.ok(
|
|
246
|
+
contentMatchesRedactionRules("leaked target-confidential here", combined),
|
|
247
|
+
"target-namespace rule must catch matching content when dirs are combined",
|
|
248
|
+
);
|
|
249
|
+
// Clean content passes
|
|
250
|
+
assert.ok(
|
|
251
|
+
!contentMatchesRedactionRules("clean content", combined),
|
|
252
|
+
"non-matching content must not be redacted",
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("#1713 Item 1: source-only rules miss target-namespace patterns (the bug this fixes)", async () => {
|
|
259
|
+
await withTempDir(async (sourceDir) => {
|
|
260
|
+
await withTempDir(async (targetDir) => {
|
|
261
|
+
// Only the TARGET namespace has the redaction rule
|
|
262
|
+
await writeRule(targetDir, "cross-ns-secret");
|
|
263
|
+
|
|
264
|
+
// The OLD behavior (pre-fix): only source rules loaded at pre-judge point
|
|
265
|
+
const sourceOnlyRules = await loadRedactionRules(sourceDir);
|
|
266
|
+
assert.equal(sourceOnlyRules.length, 0);
|
|
267
|
+
assert.ok(
|
|
268
|
+
!contentMatchesRedactionRules("cross-ns-secret in content", sourceOnlyRules),
|
|
269
|
+
"source-only rules miss the target-namespace pattern (the pre-fix gap)",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// The NEW behavior: source + target rules combined
|
|
273
|
+
const combined = [
|
|
274
|
+
...(await loadRedactionRules(sourceDir)),
|
|
275
|
+
...(await loadRedactionRules(targetDir)),
|
|
276
|
+
];
|
|
277
|
+
assert.equal(combined.length, 1);
|
|
278
|
+
assert.ok(
|
|
279
|
+
contentMatchesRedactionRules("cross-ns-secret in content", combined),
|
|
280
|
+
"combined rules catch the target-namespace pattern (the fix)",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("#1713 (codex PRRT_kwDORJXyws6PBj5X): scope-classification routed shared target rules consulted at pre-judge", async () => {
|
|
287
|
+
// Scope classification routes a scope=global fact to the shared namespace
|
|
288
|
+
// independent of routing rules. The pre-judge redaction filter must consult
|
|
289
|
+
// the SHARED namespace's rules for such a fact, or a never-store pattern
|
|
290
|
+
// registered only under shared is missed until the write gate and the content
|
|
291
|
+
// reaches the extraction judge/training path. This test mirrors the
|
|
292
|
+
// orchestrator's per-fact scope-routing decision + rule combining.
|
|
293
|
+
await withTempDir(async (sourceDir) => {
|
|
294
|
+
await withTempDir(async (sharedDir) => {
|
|
295
|
+
// Only the SHARED namespace carries the never-store rule.
|
|
296
|
+
await writeRule(sharedDir, "shared-only-secret");
|
|
297
|
+
|
|
298
|
+
// A scope=global fact the orchestrator will route to shared.
|
|
299
|
+
const fact = { scope: "global" as string, content: "leaked shared-only-secret here" };
|
|
300
|
+
const sharedNamespace: string = "shared";
|
|
301
|
+
const sourceNamespace: string = "default"; // source !== shared
|
|
302
|
+
|
|
303
|
+
// Mirror of the orchestrator's scope-routing condition (persistExtraction
|
|
304
|
+
// pre-judge per-fact block): a fact is scope-routed to shared when no
|
|
305
|
+
// routing rule set an explicit namespace, scope classification is on, the
|
|
306
|
+
// fact is global, shared writes are allowed, and the source is not already
|
|
307
|
+
// shared.
|
|
308
|
+
const preRoutedNamespaceByFact: string | undefined = undefined;
|
|
309
|
+
const scopeClassificationEnabled = true;
|
|
310
|
+
const namespacesEnabled = true;
|
|
311
|
+
const profileAllowsSharedWrites = true;
|
|
312
|
+
let factNs: string | undefined = preRoutedNamespaceByFact;
|
|
313
|
+
if (
|
|
314
|
+
!factNs &&
|
|
315
|
+
scopeClassificationEnabled &&
|
|
316
|
+
namespacesEnabled &&
|
|
317
|
+
fact.scope === "global" &&
|
|
318
|
+
profileAllowsSharedWrites &&
|
|
319
|
+
sourceNamespace !== sharedNamespace
|
|
320
|
+
) {
|
|
321
|
+
factNs = sharedNamespace;
|
|
322
|
+
}
|
|
323
|
+
assert.equal(factNs, sharedNamespace, "global fact must be scope-routed to shared");
|
|
324
|
+
|
|
325
|
+
// Base (source-only) rules + routed target (shared) rules combined — what
|
|
326
|
+
// the orchestrator builds for this fact.
|
|
327
|
+
const baseRules: CompiledRedactionRule[] = await loadRedactionRules(sourceDir);
|
|
328
|
+
const targetRules: CompiledRedactionRule[] =
|
|
329
|
+
factNs === sharedNamespace ? await loadRedactionRules(sharedDir) : [];
|
|
330
|
+
const factRedactionRules: CompiledRedactionRule[] = [...baseRules, ...targetRules];
|
|
331
|
+
|
|
332
|
+
assert.equal(baseRules.length, 0, "source namespace has no rules");
|
|
333
|
+
assert.equal(targetRules.length, 1, "shared namespace carries the never-store rule");
|
|
334
|
+
|
|
335
|
+
// Pre-fix (source-only) behavior: the shared-only pattern is missed at
|
|
336
|
+
// pre-judge — the gap the codex thread flags.
|
|
337
|
+
assert.ok(
|
|
338
|
+
!contentMatchesRedactionRules(fact.content, baseRules),
|
|
339
|
+
"source-only rules miss the shared-namespace never-store pattern (the gap)",
|
|
340
|
+
);
|
|
341
|
+
// Post-fix: combined source+shared rules catch it before the judge.
|
|
342
|
+
assert.ok(
|
|
343
|
+
contentMatchesRedactionRules(fact.content, factRedactionRules),
|
|
344
|
+
"combined source+shared rules catch the shared never-store at pre-judge (the fix)",
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("#1713 (codex): non-global fact is NOT scope-routed to shared", async () => {
|
|
351
|
+
// A scope-local fact must not consult shared rules at pre-judge — only
|
|
352
|
+
// routing-rule targets and scope=global facts do. Guards against over-redacting
|
|
353
|
+
// unrelated facts with shared-only never-store patterns (codex sibling threads).
|
|
354
|
+
await withTempDir(async (sourceDir) => {
|
|
355
|
+
await withTempDir(async (sharedDir) => {
|
|
356
|
+
await writeRule(sharedDir, "shared-only-secret");
|
|
357
|
+
|
|
358
|
+
const fact = { scope: "local" as string, content: "leaked shared-only-secret here" };
|
|
359
|
+
const sharedNamespace: string = "shared";
|
|
360
|
+
const sourceNamespace: string = "default";
|
|
361
|
+
|
|
362
|
+
const preRoutedNamespaceByFact: string | undefined = undefined;
|
|
363
|
+
const scopeClassificationEnabled = true;
|
|
364
|
+
const namespacesEnabled = true;
|
|
365
|
+
const profileAllowsSharedWrites = true;
|
|
366
|
+
let factNs: string | undefined = preRoutedNamespaceByFact;
|
|
367
|
+
if (
|
|
368
|
+
!factNs &&
|
|
369
|
+
scopeClassificationEnabled &&
|
|
370
|
+
namespacesEnabled &&
|
|
371
|
+
fact.scope === "global" &&
|
|
372
|
+
profileAllowsSharedWrites &&
|
|
373
|
+
sourceNamespace !== sharedNamespace
|
|
374
|
+
) {
|
|
375
|
+
factNs = sharedNamespace;
|
|
376
|
+
}
|
|
377
|
+
assert.equal(factNs, undefined, "non-global fact must not be scope-routed to shared");
|
|
378
|
+
|
|
379
|
+
const baseRules: CompiledRedactionRule[] = await loadRedactionRules(sourceDir);
|
|
380
|
+
const targetRules: CompiledRedactionRule[] = [];
|
|
381
|
+
const factRedactionRules: CompiledRedactionRule[] = [...baseRules, ...targetRules];
|
|
382
|
+
assert.equal(factRedactionRules.length, 0, "no rules consulted for a scope-local fact");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|