@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/namespaces/storage.ts"],"sourcesContent":["import path from \"node:path\";\nimport { access, lstat, readdir } from \"node:fs/promises\";\nimport { isSafeRouteNamespace } from \"../routing/engine.js\";\nimport { StorageManager } from \"../storage.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { ALL_CATEGORY_DIRS } from \"../utils/category-dir.js\";\nimport { namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\nimport type { NamespaceCatalog } from \"./catalog.js\";\nimport { MutationSerializer } from \"../utils/serialize-mutations.js\";\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function hasStoredEntries(p: string): Promise<boolean> {\n try {\n const entry = await lstat(p);\n if (entry.isSymbolicLink()) return true;\n if (!entry.isDirectory()) return true;\n const children = await readdir(p, { withFileTypes: true });\n for (const child of children) {\n const childPath = path.join(p, child.name);\n if (child.isSymbolicLink() || child.isFile()) return true;\n if (child.isDirectory() && (await hasStoredEntries(childPath))) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\n// Build a per-namespace directory under `<memoryDir>/namespaces` and assert the\n// resolved path stays inside that base. Namespace identifiers can originate from\n// operator config (config.defaultNamespace) and request-derived routing, so this\n// containment check prevents directory traversal (CodeQL js/path-injection).\n// For safe segments this returns exactly `path.join(base, segment)`, so there is\n// no behavioral change for valid namespaces.\nfunction resolveNamespaceDir(memoryDir: string, segment: string): string {\n // Mirror isSafeRouteNamespace's separator/parent-ref rejection (without its\n // 64-char cap, so identity tokens still pass). Rejecting separators and \"..\"\n // up front keeps the value a single contained child of <memoryDir>/namespaces.\n if (\n segment.length === 0 ||\n segment.includes(\"/\") ||\n segment.includes(\"\\\\\") ||\n segment.includes(\"..\") ||\n path.isAbsolute(segment)\n ) {\n throw new Error(`unsafe namespace path segment: ${segment}`);\n }\n return path.join(memoryDir, \"namespaces\", segment);\n}\n\nconst LEGACY_NAMESPACE_CONTENT_CHILDREN = [\n ...ALL_CATEGORY_DIRS,\n \"entities\",\n \"artifacts\",\n \"identity\",\n \"config\",\n \"summaries\",\n \"profile.md\",\n] as const;\n\nconst LEGACY_NAMESPACE_RUNTIME_CHILDREN = [\"state\"] as const;\n\nasync function hasAnyLegacyData(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await hasStoredEntries(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\nasync function hasAnyNamespaceStorageMarker(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await exists(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\n/**\n * Storage routing for namespaces.\n *\n * Compatibility note:\n * - When namespaces are enabled, existing raw namespace roots are preserved.\n * New namespace roots use tokenized names under `memoryDir/namespaces/<token>`.\n * - The default namespace continues to use the legacy `memoryDir` root unless the caller\n * has created `memoryDir/namespaces/<defaultNamespace>` (in which case we use that).\n *\n * This avoids surprising \"lost memories\" when an install flips namespaces on without\n * migrating existing data.\n */\n/**\n * Optional hooks for the storage router. `onResolve` fires whenever a namespace's\n * storage is resolved/created, so a downstream consumer (e.g. the namespace\n * catalog, issue #1499) can register the namespace. The hook MUST NOT throw into\n * the router; the router invokes it defensively and a hook failure never affects\n * storage resolution.\n *\n * The hook MAY return (or resolve to) a boolean indicating whether the\n * registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves\n * to `false` (a dropped/no-op registration), the router does NOT mark the\n * (namespace, storageDir) pair as notified, so the next resolve RETRIES it\n * instead of suppressing it forever. A `void`/`undefined` result is treated as\n * success (legacy hooks).\n */\nexport interface NamespaceStorageRouterHooks {\n onResolve?: (\n namespace: string,\n storageDir: string,\n ) => void | boolean | Promise<void | boolean>;\n}\n\n/**\n * Resolve the runtime storage root for the configured DEFAULT namespace.\n *\n * Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)\n * and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two\n * can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through\n * the same logic). The contract is: while legacy memory data still lives\n * directly under `memoryDir`, the default root stays `memoryDir`; only once the\n * legacy root is empty and a `namespaces/<default|token>` dir holds data does\n * the default migrate into that tokenized/legacy-named dir.\n */\nexport async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {\n if (!config.namespacesEnabled) {\n return config.memoryDir;\n }\n\n // Build the legacy default root from the NORMALIZED (trimmed) name so a\n // whitespace-padded `defaultNamespace` still finds the live `namespaces/default`\n // root (NIabe). `storageFor()` classifies the trimmed value as the default, and\n // the on-disk legacy dir is created under the trimmed name; using the raw spaced\n // name here would look for `namespaces/<spaced>` and miss the real root, falling\n // back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes\n // internally, so the tokenized path is unaffected.\n const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);\n const tokenizedNsDir = resolveNamespaceDir(\n config.memoryDir,\n namespaceIdentityToken(config.defaultNamespace),\n );\n const tokenizedHasData =\n (await exists(tokenizedNsDir)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));\n const nsDir = tokenizedHasData\n ? tokenizedNsDir\n : (await exists(legacyNsDir))\n ? legacyNsDir\n : tokenizedNsDir;\n return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))\n ? nsDir\n : config.memoryDir;\n}\n\n/**\n * Resolve the runtime storage root for ANY namespace exactly as the live router\n * would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable\n * catalog records the SAME on-disk root the router routes to — a recall/read\n * touch must not guess `namespaces/<token>` when the router actually serves a\n * legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round\n * 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;\n * every other namespace prefers the tokenized root when it has a storage marker,\n * else a legacy raw-name dir when present, else the tokenized root.\n */\nexport async function resolveNamespaceStorageRoot(\n config: PluginConfig,\n namespace: string,\n): Promise<string> {\n if (!config.namespacesEnabled) return config.memoryDir;\n // Compare on NORMALIZED identity so a whitespace-padded configured default name\n // still routes to the default root rather than a tokenized non-default dir\n // (NH-FH). The catalog keys records by the same normalized identity.\n if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {\n return resolveDefaultNamespaceRoot(config);\n }\n const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);\n const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));\n if (\n (await exists(tokenizedRoot)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))\n ) {\n return tokenizedRoot;\n }\n return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;\n}\n\nexport class NamespaceStorageRouter {\n private readonly cache = new Map<string, StorageManager>();\n private defaultNsRootResolved: string | null = null;\n // Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction\n // call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +\n // append) on every cache hit grows `namespaces.jsonl` without bound between\n // rebuilds. We fire the hook only when the (namespace, storageDir) pair is new\n // or its dir changed, so a steady-state cache hit is a no-op for the catalog.\n private readonly notifiedResolved = new Map<string, string>();\n // Instance-scoped serializer for the resolve hook (issue #1524 adoption).\n // Replaces the bespoke `inFlightResolved` marker-then-clear pattern: the\n // serializer's per-key chain strictly orders concurrent notifications for the\n // SAME namespace, and the task re-checks `notifiedResolved` once it runs, so a\n // burst of cache hits before the first hook settles collapses to a single\n // hook invocation (the once-per-namespace intent). Recovery is preserved —\n // one rejected hook never poisons subsequent notifications for that key.\n private readonly resolveSerializer = new MutationSerializer();\n // (namespace, storageDir) pairs whose resolve hook is currently pending. Set SYNCHRONOUSLY in\n // notifyResolved (before queueing) so a burst of concurrent storageFor()\n // cache hits collapses onto the one in-flight hook instead of each enqueuing\n // its own task. Cleared when the queued hook settles. Without this, a dropped\n // hook (returns false on a rebuild-lock timeout) leaves notifiedResolved\n // unset, so every queued sibling task would re-run the hook — N serial lock\n // waits (cursor Medium 06f58a7c, codex P2).\n private readonly inFlightResolveHooks = new Set<string>();\n // Tracks every in-flight resolve-hook promise so callers can deterministically\n // await the fire-and-forget registrations that `storageFor()` kicks off (see\n // `whenResolveHooksSettled`). Entries are removed as each hook settles, so the\n // set holds at most one promise per concurrently-resolving namespace.\n private readonly pendingResolveHooks = new Set<Promise<unknown>>();\n // Pending post-write catalog touch promises (#1522). Like pendingResolveHooks,\n // lets tests await fire-and-forget write touches deterministically.\n private readonly pendingWriteTouches = new Set<Promise<unknown>>();\n\n // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`\n // normalizes its input, so default-namespace branches must compare against the\n // normalized config default too — otherwise a whitespace-padded configured\n // default name routes the default namespace to a tokenized non-default root.\n private readonly defaultNamespaceIdentity: string;\n\n constructor(\n private readonly config: PluginConfig,\n private readonly hooks: NamespaceStorageRouterHooks = {},\n /** Catalog reference for post-write/read touches (issue #1522 chokepoint). */\n private readonly catalog?: NamespaceCatalog,\n ) {\n this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n }\n\n private async defaultNamespaceRoot(): Promise<string> {\n this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);\n return this.defaultNsRootResolved;\n }\n\n private async namespaceRoot(namespace: string): Promise<string> {\n // NOTE: only used after defaultNamespaceRoot() resolution.\n if (!this.config.namespacesEnabled) return this.config.memoryDir;\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n return this.defaultNsRootResolved ?? this.config.memoryDir;\n }\n return resolveNamespaceStorageRoot(this.config, namespace);\n }\n\n async storageFor(namespace: string): Promise<StorageManager> {\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n throw new Error(`unsafe namespace: ${ns}`);\n }\n // Even when the default namespace is exempt from the check above, every\n // on-disk path is built through resolveNamespaceDir(), which rejects\n // traversal segments — so an unsafe configured default still cannot escape\n // <memoryDir>/namespaces (CodeQL js/path-injection).\n\n let root: string;\n if (ns === this.defaultNamespaceIdentity) {\n root = await this.defaultNamespaceRoot();\n const cached = this.cache.get(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n } else {\n const cached = this.cache.get(ns);\n root = await this.namespaceRoot(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n }\n\n const sm = new StorageManager(root, this.config.entitySchemas);\n // Propagate the inline-attribution template so that router-created storages\n // (used by extraction and shared-promotion paths) strip citations consistently,\n // matching the behaviour of the primary this.storage instance in the orchestrator.\n sm.citationTemplate = this.config.inlineSourceAttributionFormat;\n // #1522: install the post-write catalog touch at the chokepoint — every\n // successful write on this StorageManager records the namespace touch.\n this.bindCatalogWriteHook(sm, ns);\n // #1579: apply the tombstone non-resurrection config so every namespace\n // storage enforces the invariant at its own writeMemory chokepoint.\n this.applyTombstonesConfig(sm, ns);\n this.cache.set(ns, sm);\n this.notifyResolved(ns, root);\n return sm;\n }\n\n /**\n * Fire the resolve hook defensively. A hook failure (e.g. a catalog write\n * error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.\n *\n * Issue #1524 adoption: hook invocations for the SAME namespace are now\n * strictly ordered through the shared `MutationSerializer` rather than the\n * bespoke `inFlightResolved` marker-then-clear pattern. The serializer\n * guarantees one in-flight hook per namespace; a concurrent burst of\n * `storageFor()` cache hits collapses to a single hook invocation because\n * each queued task re-checks `notifiedResolved` before invoking the hook. A\n * dropped (`false`) or rejected hook leaves `notifiedResolved` unset so the\n * next `storageFor()` retries — the serializer recovers from the rejection,\n * so a failed hook never poisons later notifications.\n */\n private notifyResolved(namespace: string, storageDir: string): void {\n const hook = this.hooks.onResolve;\n if (!hook) return;\n // Permanent dedup: skip once we've SUCCESSFULLY notified this exact\n // (namespace, storageDir). A changed dir (rare: migration/realignment)\n // still re-fires once. The mark is set ONLY AFTER the hook succeeds, so a\n // dropped registration (e.g. rebuild-lock timeout) is RETRIED on the next\n // cache hit instead of being suppressed forever (round 6, cursor Medium —\n // ND3EJ).\n if (this.notifiedResolved.get(namespace) === storageDir) return;\n // In-flight dedup (cursor Medium 06f58a7c, codex P2): if a hook for THIS\n // namespace is already pending, collapse this call onto it instead of\n // enqueueing another task. The serializer strictly orders queued tasks, but\n // ordering alone is not enough — when the first hook returns `false`\n // (dropped touch, e.g. rebuild-lock timeout), `notifiedResolved` stays\n // unset, so each queued sibling task would pass its re-check and re-run the\n // hook. With the real catalog hook each retry can spend the full lock wait,\n // so N cache hits during a rebuild leave N serial background lock attempts.\n // Collapsing here means at most ONE hook runs per in-flight registration;\n // its result (set `notifiedResolved` on success, leave unset on drop)\n // decides whether a LATER `storageFor()` retries — collapsing loses\n // nothing because the drop is already retried on the next cache hit.\n // Keyed by the composite (namespace, storageDir) so a CHANGED dir\n // (migration/realignment) for the same namespace still gets its own hook —\n // it is NOT collapsed onto the old dir's pending registration (cursor\n // Medium, codex P2).\n const inFlightKey = namespace + \"\\u0000\" + storageDir;\n if (this.inFlightResolveHooks.has(inFlightKey)) return;\n this.inFlightResolveHooks.add(inFlightKey);\n // Queue through the serializer. Concurrent calls for the same namespace\n // strictly order here; the 2nd call's task runs only after the 1st's hook\n // settles, by which point `notifiedResolved` is either set (no-op) or still\n // unset (retry). The returned promise is tracked for\n // `whenResolveHooksSettled()`. Rejections from the task body are caught so\n // they never reach the caller (best-effort hook contract); the serializer's\n // recovered tail still lets subsequent notifications run.\n const task = async (): Promise<void> => {\n // Re-check after queueing: a prior task in this same chain may have just\n // marked the pair as notified.\n if (this.notifiedResolved.get(namespace) === storageDir) return;\n try {\n // Hook may be sync or async; Promise.resolve normalizes both. A result\n // of `false` means the registration was dropped/no-op (e.g. rebuild-lock\n // timeout) — leave notifiedResolved UNSET so the next storageFor retries\n // (round 6, codex P2 — NEFoX). `void`/`undefined` is success for legacy\n // hooks. A rejection leaves notifiedResolved unset for the same reason.\n const persisted = await Promise.resolve(hook(namespace, storageDir));\n if (persisted !== false) {\n this.notifiedResolved.set(namespace, storageDir);\n }\n } catch {\n // Best-effort: a hook failure MUST NOT crash storage resolution. Leave\n // notifiedResolved unset so a later storageFor retries the registration.\n }\n };\n const queued = this.resolveSerializer.serialize(namespace, task);\n this.pendingResolveHooks.add(queued);\n // Clear the in-flight marker when the hook settles so a subsequent\n // storageFor() can retry after a drop, or short-circuit via notifiedResolved\n // after a success.\n const cleanup = (): void => {\n this.inFlightResolveHooks.delete(inFlightKey);\n this.pendingResolveHooks.delete(queued);\n };\n void queued.then(cleanup, cleanup);\n }\n\n /**\n * Install the post-write catalog touch hook on an externally-constructed\n * StorageManager (issue #1522). Used by the orchestrator for the legacy\n * default-namespace storage (`this.storage`) that bypasses the router.\n */\n bindCatalogWriteHook(sm: StorageManager, namespace: string): void {\n sm.onCatalogWrite = () => this.touchCatalogWrite(namespace, sm.dir);\n }\n\n /**\n * Install the tombstone config on an externally-constructed StorageManager\n * (issue #1579). Mirrors `bindCatalogWriteHook` — used for the legacy\n * default-namespace storage that bypasses the router. Router-created\n * storages are wired inline in `storageFor()` via `applyTombstonesConfig`.\n */\n bindTombstonesConfig(\n sm: StorageManager,\n namespace: string,\n config: { enabled: boolean; semanticMatch: boolean; semanticThreshold: number },\n ): void {\n this.tombstonesGlobalConfig = { ...config };\n sm.setTombstonesConfig({ ...config, namespace });\n }\n\n private tombstonesGlobalConfig: {\n enabled: boolean;\n semanticMatch: boolean;\n semanticThreshold: number;\n } = { enabled: false, semanticMatch: false, semanticThreshold: 0.9 };\n\n /**\n * Apply the tombstone config to a router-created StorageManager. Called\n * inline in `storageFor()` so every namespace storage enforces the\n * non-resurrection invariant, namespace-scoped (rule 42).\n */\n private applyTombstonesConfig(sm: StorageManager, namespace: string): void {\n sm.setTombstonesConfig({ ...this.tombstonesGlobalConfig, namespace });\n }\n\n /**\n * Post-write catalog touch (issue #1522 chokepoint). Called by every\n * StorageManager's post-write hook AFTER a successful write. Best-effort\n * and failure-tolerant — a catalog error MUST NOT affect the primary write\n * (gotcha #13, rule #40). Fire-and-forget by design.\n */\n private touchCatalogWrite(namespace: string, storageDir: string): void {\n if (!this.catalog) return;\n const touch = this.catalog\n .markWrite(namespace, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Record a namespace read in the catalog (issue #1522 chokepoint move).\n * Best-effort and failure-tolerant. Used by recall paths so the read touch\n * lives in the storage layer, not at the caller.\n */\n recordRead(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n void this.catalog\n .markRead(ns, { discoveredBy: \"read\", storageDir })\n .catch(() => undefined);\n }\n /**\n * Record a namespace write touch in the catalog (issue #1522). Best-effort\n * and failure-tolerant. Used by consolidation/cleanup passes that may mutate\n * the store via delete-only paths (e.g. entity-file merges, TTL cleanup) so\n * the namespace's lastWriteAt stays fresh even when no explicit write went\n * through the storage chokepoint. This is NOT a substitute for the chokepoint\n * — it's a belt-and-suspenders for paths the chokepoint doesn't cover.\n */\n recordWrite(namespace: string, storageDir?: string): void {\n if (!this.catalog) return;\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n const touch = this.catalog\n .markWrite(ns, { discoveredBy: \"write\", storageDir })\n .catch(() => undefined);\n this.pendingWriteTouches.add(touch);\n void touch.then(\n () => { this.pendingWriteTouches.delete(touch); },\n () => { this.pendingWriteTouches.delete(touch); },\n );\n }\n\n /**\n * Resolve once every in-flight post-write catalog touch has settled (#1522).\n * Mirrors `whenResolveHooksSettled()`: the StorageManager's post-write hook\n * fires the catalog touch fire-and-forget, so tests asserting lastWriteAt\n * moved should await this instead of racing a timer.\n */\n async whenWriteTouchesSettled(): Promise<void> {\n while (this.pendingWriteTouches.size > 0) {\n await Promise.allSettled([...this.pendingWriteTouches]);\n }\n }\n\n /**\n * Resolve once every in-flight `onResolve` registration has settled.\n *\n * `storageFor()` fires the resolve hook fire-and-forget, so its catalog side\n * effect (e.g. `registerResolved(...)`) is not observable the moment\n * `storageFor()` returns. Callers that must act on that side effect — notably\n * tests asserting the catalog was updated — should await this instead of\n * racing a timer. Resolves immediately when no hook is registered or nothing\n * is in flight. The loop re-checks because a settling hook could, in\n * principle, trigger a follow-on resolution.\n */\n async whenResolveHooksSettled(): Promise<void> {\n while (this.pendingResolveHooks.size > 0) {\n await Promise.allSettled([...this.pendingResolveHooks]);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,19 +11,19 @@ import {
|
|
|
11
11
|
} from "./chunk-D24OXEPB.js";
|
|
12
12
|
import {
|
|
13
13
|
validateRequest
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-Q4O3ET6F.js";
|
|
15
15
|
import {
|
|
16
16
|
getOperation
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-YO4MBK3I.js";
|
|
18
18
|
import {
|
|
19
19
|
EngramAccessInputError
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FUCJAZ25.js";
|
|
21
21
|
import {
|
|
22
22
|
projectTagProjectId
|
|
23
23
|
} from "./chunk-HRUULBBV.js";
|
|
24
24
|
import {
|
|
25
25
|
validateBriefingFormat
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-HF4N43Q7.js";
|
|
27
27
|
import {
|
|
28
28
|
resolvePrincipal
|
|
29
29
|
} from "./chunk-UZYLX7M6.js";
|
|
@@ -3302,4 +3302,4 @@ ${body}`;
|
|
|
3302
3302
|
export {
|
|
3303
3303
|
EngramMcpServer
|
|
3304
3304
|
};
|
|
3305
|
-
//# sourceMappingURL=chunk-
|
|
3305
|
+
//# sourceMappingURL=chunk-OLOYQZFB.js.map
|
|
@@ -209,6 +209,25 @@ async function applyTemporalSupersession(args) {
|
|
|
209
209
|
supersededAt,
|
|
210
210
|
invalidAt: invalidAtPatch ?? fresh.frontmatter.invalid_at
|
|
211
211
|
});
|
|
212
|
+
const keysToTombstone = decision.matchedKeys.length > 0 ? decision.matchedKeys : [void 0];
|
|
213
|
+
for (const key of keysToTombstone) {
|
|
214
|
+
try {
|
|
215
|
+
await args.storage.appendTombstone({
|
|
216
|
+
reason: "supersession",
|
|
217
|
+
createdBy: "supersession",
|
|
218
|
+
sourceMemoryId: fresh.frontmatter.id,
|
|
219
|
+
...fresh.frontmatter.contentHash ? { contentHash: fresh.frontmatter.contentHash } : {},
|
|
220
|
+
rawContent: fresh.content,
|
|
221
|
+
...fresh.frontmatter.entityRef ? { entityRef: fresh.frontmatter.entityRef } : {},
|
|
222
|
+
...key ? { supersessionKey: key } : {},
|
|
223
|
+
createdAt: supersededAt
|
|
224
|
+
});
|
|
225
|
+
} catch (tombErr) {
|
|
226
|
+
log.warn(
|
|
227
|
+
`temporal-supersession: tombstone emit failed for ${fresh.frontmatter.id}${key ? ` (key=${key})` : ""} : ${tombErr}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
212
231
|
}
|
|
213
232
|
} catch (err) {
|
|
214
233
|
log.warn(
|
|
@@ -238,4 +257,4 @@ export {
|
|
|
238
257
|
applyTemporalSupersession,
|
|
239
258
|
shouldFilterSupersededFromRecall
|
|
240
259
|
};
|
|
241
|
-
//# sourceMappingURL=chunk-
|
|
260
|
+
//# sourceMappingURL=chunk-OMNTQPZO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/temporal-supersession.ts"],"sourcesContent":["/**\n * Temporal Supersession (issue #375)\n *\n * When a new fact lands with `structuredAttributes` keyed on a known\n * `entityRef`, any prior fact whose supersession key collides with the new\n * fact's key is marked `status: \"superseded\"` and linked via\n * `supersededBy` / `supersededAt`. Recall filters those superseded memories\n * by default so agents see only the \"current\" value per entity attribute.\n *\n * The algorithm is intentionally O(N) over the memory corpus per write, but\n * skips cheaply when the new fact has no structuredAttributes. It reuses the\n * cached `readAllMemories()` path so cost is amortized with the rest of the\n * write pipeline.\n */\nimport type { MemoryFile, MemoryFrontmatter } from \"./types.js\";\nimport type { StorageManager } from \"./storage.js\";\nimport { log } from \"./logger.js\";\nimport { effectiveValidAt } from \"./temporal-validity.js\";\n\n/**\n * Shared normalization for supersession key components.\n *\n * Trims surrounding whitespace, lowercases, then collapses any run of\n * whitespace OR hyphens to a single hyphen, and strips any leading/trailing\n * hyphens that result. Both `computeSupersessionKey` and\n * `lookupAttributeByNormalizedKey` must use this so that keys produced at\n * write time and keys used at lookup time are identical regardless of how\n * the LLM encoded whitespace, hyphens, or casing (Finding B fix).\n *\n * Symmetry guarantee: `\"foo bar\"`, `\"foo-bar\"`, `\"foo - bar\"`, and\n * `\"foo bar\"` all canonicalize to `\"foo-bar\"`.\n *\n * Exported so external tests can verify the canonical form.\n */\nexport function normalizeSupersessionKey(raw: string): string {\n return raw\n .trim()\n .toLowerCase()\n .replace(/[\\s\\-]+/g, \"-\")\n // The previous line already collapsed runs to single hyphens, so ^-|-$ is\n // equivalent to ^-+|-+$ here and drops the anchored quantifier flagged by\n // CodeQL js/polynomial-redos.\n .replace(/^-|-$/g, \"\");\n}\n\n/**\n * Stable supersession key for an (entityRef, attributeName) pair.\n *\n * The algorithm is:\n * - normalize the entityRef (trim, lower-case, collapse whitespace)\n * - normalize the attributeName the same way\n * - join with `::`\n *\n * Exported so tests and tools can recompute it without depending on storage.\n */\nexport function computeSupersessionKey(\n entityRef: string | undefined,\n attributeName: string,\n): string | null {\n if (!entityRef || typeof entityRef !== \"string\") return null;\n if (!attributeName || typeof attributeName !== \"string\") return null;\n const entity = normalizeSupersessionKey(entityRef);\n const attr = normalizeSupersessionKey(attributeName);\n if (entity.length === 0 || attr.length === 0) return null;\n return `${entity}::${attr}`;\n}\n\n/**\n * Compute the full set of supersession keys for a fact with structured\n * attributes. Returns an empty array if no keys can be derived.\n */\nexport function supersessionKeysForFact(spec: {\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n}): string[] {\n if (!spec.entityRef) return [];\n if (!spec.structuredAttributes) return [];\n const keys: string[] = [];\n for (const attrName of Object.keys(spec.structuredAttributes)) {\n const key = computeSupersessionKey(spec.entityRef, attrName);\n if (key) keys.push(key);\n }\n return keys;\n}\n\n/**\n * Look up a structured-attribute value by a raw key, normalizing both sides\n * with `normalizeSupersessionKey` before comparing. This ensures that keys\n * written by the LLM with mixed case, surrounding whitespace, or internal\n * whitespace (e.g. `\"City\"`, `\" city \"`, `\"job title\"`, `\"job-title\"`)\n * are all matched against normalized keys produced by `computeSupersessionKey`\n * (Finding B fix — uses the same helper so both sides are identical).\n *\n * The storage format is NOT changed — we only normalize at lookup time.\n */\nexport function lookupAttributeByNormalizedKey(\n attributes: Record<string, unknown>,\n rawKey: string,\n): unknown {\n const normalizedTarget = normalizeSupersessionKey(rawKey);\n for (const [k, v] of Object.entries(attributes)) {\n if (normalizeSupersessionKey(k) === normalizedTarget) return v;\n }\n return undefined;\n}\n\n/**\n * Decide whether an existing memory should be superseded by a newly-written\n * memory that carries the supplied supersession key set.\n *\n * Only memories that:\n * - are currently `active`\n * - share an `entityRef` with the new fact\n * - share at least one supersession key with the new fact\n * - are older than the new fact\n * - have a conflicting value (different string) for the overlapping key\n * are eligible. This keeps supersession local to the attribute that actually\n * changed — if fact A sets `{city: Austin, tool: vim}` and fact B sets\n * `{city: NYC}`, only the city attribute is superseded, not the tool.\n */\nexport function shouldSupersedeExisting(args: {\n candidate: MemoryFrontmatter;\n newEntityRef: string;\n newAttributes: Record<string, string>;\n newCreatedAt: string;\n newMemoryId: string;\n}): { matchedKeys: string[] } | null {\n const { candidate, newEntityRef, newAttributes, newCreatedAt, newMemoryId } = args;\n\n if (candidate.id === newMemoryId) return null;\n if (candidate.status && candidate.status !== \"active\") return null;\n if (!candidate.entityRef) return null;\n if (!candidate.structuredAttributes) return null;\n\n // Reuse the shared `normalizeSupersessionKey` helper so this comparison\n // cannot drift from the canonical form used to build supersession keys\n // elsewhere in this file.\n const candidateEntityNorm = normalizeSupersessionKey(candidate.entityRef);\n const newEntityNorm = normalizeSupersessionKey(newEntityRef);\n if (candidateEntityNorm !== newEntityNorm) return null;\n\n // Must be older than the new fact's effective validity start — equal\n // timestamps are ignored to avoid races within the same millisecond. When\n // replay/import supplies source time, valid_at must drive ordering instead\n // of wall-clock persistence time.\n const candidateCreated = Date.parse(effectiveValidAt(candidate));\n const newCreated = Date.parse(newCreatedAt);\n if (!Number.isFinite(candidateCreated) || !Number.isFinite(newCreated)) return null;\n if (candidateCreated >= newCreated) return null;\n\n const matchedKeys: string[] = [];\n for (const [attrName, newValue] of Object.entries(newAttributes)) {\n // Use normalized key lookup so mixed-case or whitespace-padded keys\n // stored by the LLM are matched correctly (Finding 2 fix).\n const candidateValue = lookupAttributeByNormalizedKey(\n candidate.structuredAttributes,\n attrName,\n );\n if (candidateValue === undefined) continue;\n // Only supersede on conflicting values — identical values are a no-op.\n if (normalizeValue(String(candidateValue)) === normalizeValue(newValue)) continue;\n const key = computeSupersessionKey(newEntityRef, attrName);\n if (key) matchedKeys.push(key);\n }\n\n return matchedKeys.length > 0 ? { matchedKeys } : null;\n}\n\nfunction normalizeValue(v: string): string {\n return v.trim().toLowerCase();\n}\n\nasync function expireChildChunksForSupersededParent(args: {\n storage: StorageManager;\n allCandidates: MemoryFile[];\n parentId: string;\n newMemoryId: string;\n supersededAt: string;\n invalidAt?: string;\n}): Promise<void> {\n const processedChunkIds = new Set<string>();\n const chunks = args.allCandidates.filter(\n (candidate) => candidate.frontmatter.parentId === args.parentId,\n );\n\n for (const chunk of chunks) {\n const chunkKey = chunk.frontmatter.id ?? chunk.path;\n if (processedChunkIds.has(chunkKey)) continue;\n\n try {\n const freshChunk = await args.storage.readMemoryByPath(chunk.path);\n if (!freshChunk) continue;\n processedChunkIds.add(chunkKey);\n const freshStatus = freshChunk.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || freshChunk.frontmatter.supersededBy) continue;\n\n await args.storage.writeMemoryFrontmatter(\n freshChunk,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt: args.supersededAt,\n updated: args.supersededAt,\n ...(args.invalidAt && !freshChunk.frontmatter.invalid_at\n ? { invalid_at: args.invalidAt }\n : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update-child-chunk\",\n relatedMemoryIds: [args.newMemoryId, args.parentId],\n },\n );\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to expire child chunk ${chunk.frontmatter.id} for parent ${args.parentId}: ${err}`,\n );\n }\n }\n}\n\nexport interface TemporalSupersessionResult {\n supersededIds: string[];\n matchedKeys: string[];\n}\n\n/**\n * Scan existing memories and mark any that are superseded by the\n * just-written memory. Fails open on I/O errors — the new memory is already\n * on disk, and supersession is a best-effort hygiene step.\n */\nexport async function applyTemporalSupersession(args: {\n storage: StorageManager;\n newMemoryId: string;\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n createdAt: string;\n enabled: boolean;\n /**\n * When true, skip the persisted `frontmatter.created` lookup and use\n * `args.createdAt` directly as the ordering anchor. Set this on the\n * hash-dedup short-circuit path where `newMemoryId` points to an existing\n * OLD fact (no new file is written) and its persisted timestamp would be\n * stale relative to the incoming promotion event (PR #402 Finding Uyui).\n */\n useCallerTimestamp?: boolean;\n}): Promise<TemporalSupersessionResult> {\n const empty: TemporalSupersessionResult = { supersededIds: [], matchedKeys: [] };\n if (!args.enabled) return empty;\n if (!args.entityRef) return empty;\n if (!args.structuredAttributes) return empty;\n if (Object.keys(args.structuredAttributes).length === 0) return empty;\n\n const newKeys = supersessionKeysForFact({\n entityRef: args.entityRef,\n structuredAttributes: args.structuredAttributes,\n });\n if (newKeys.length === 0) return empty;\n\n let hotMemories: MemoryFile[];\n try {\n hotMemories = await args.storage.readAllMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllMemories failed: ${err}`);\n return empty;\n }\n\n // Finding 1 fix: use the on-disk effective validity start of the\n // newly-written memory rather than a wall-clock timestamp sampled after\n // `writeMemory` returns. In concurrent writers the two can differ by enough\n // to cause wrong-direction supersession. If source replay/import provided\n // valid_at, it must drive ordering; otherwise created remains the legacy\n // fallback. If the memory is not yet visible in the cache (edge case during\n // fast concurrent writes) fall back to args.createdAt.\n //\n // PR #402 round-12 (Finding Uyui): on the hash-dedup early-return path the\n // caller supplies the OLD matching fact's id as `newMemoryId` (no new file is\n // written). That makes `newMemoryFile.frontmatter.created` an arbitrarily\n // old timestamp. When `args.useCallerTimestamp` is set the caller explicitly\n // opts out of the persisted-timestamp lookup so `args.createdAt` (the\n // incoming event time: source valid_at when present, otherwise wall-clock) is\n // used directly, keeping ordering correct regardless of how old the matching\n // fact is.\n const newMemoryFile = hotMemories.find((m) => m.frontmatter.id === args.newMemoryId);\n const persistedCreatedAt = args.useCallerTimestamp\n ? args.createdAt\n : (newMemoryFile ? effectiveValidAt(newMemoryFile.frontmatter) : args.createdAt);\n\n const supersededIds: string[] = [];\n const matchedKeys = new Set<string>();\n\n // Process hot then cold. Hot-then-cold ordering is safer because hot\n // writes are more frequent and the CAS re-read guards against double-writes.\n // A Set<string> of already-processed ids ensures that a memory visible in\n // both tiers (same logical memory with different filesystem paths during a\n // migration race) is processed at most once. Keying on `frontmatter.id`\n // is correct because the same logical memory has the same id regardless of\n // which tier's directory it currently lives in (PR #402 Finding 1 fix).\n // Fall back to path-based keying when id is absent (defensive).\n const processedIds = new Set<string>();\n\n // Finding UOGi fix (round-6): readAllColdMemories() performs a full uncached\n // recursive directory scan of cold/. After Finding UTsP broadened the scan\n // to cover the entire cold root (not just facts/+corrections/), the per-call\n // cost grows with the cold tree size.\n //\n // The fix is a TTL-based in-memory cache inside StorageManager\n // (readAllColdMemories caches its result for COLD_SCAN_CACHE_TTL_MS) that is\n // shared across consecutive supersession calls within the same write burst.\n // The cache is invalidated automatically on any hot→cold demotion (which\n // calls invalidateAllMemoriesCache, which also clears the cold cache) and\n // expires after the TTL as a safety net.\n //\n // This means back-to-back structured-attribute writes in the same burst\n // (e.g. batch extraction) pay the cold I/O cost at most once, not N times.\n // Correctness is preserved because the cache TTL ensures eventual consistency\n // and the invalidation hook covers the hot→cold path.\n\n let coldMemories: MemoryFile[];\n try {\n coldMemories = await args.storage.readAllColdMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllColdMemories failed: ${err}`);\n coldMemories = [];\n }\n\n // Combine hot and cold memories into a single scan. New memory itself is\n // excluded inline. We do NOT skip cold scan when hot produced zero\n // supersessions — the P1 finding is precisely that stale cold facts leak\n // when hot has no hits.\n const allCandidates: MemoryFile[] = [...hotMemories, ...coldMemories];\n\n for (const memory of allCandidates) {\n if (memory.frontmatter.id === args.newMemoryId) continue;\n const dedupeKey = memory.frontmatter.id ?? memory.path;\n if (processedIds.has(dedupeKey)) continue;\n const snapshotStatus = memory.frontmatter.status ?? \"active\";\n if (snapshotStatus !== \"active\") {\n // A stale non-active snapshot entry must not suppress an active copy of\n // the same logical memory that appears later in another tier. This can\n // happen during hot/cold migration races where the hot snapshot is already\n // superseded but the cold copy is still active and should be evaluated.\n continue;\n }\n // NOTE: do NOT call processedIds.add(dedupeKey) here. We defer marking\n // the id as processed until AFTER the CAS re-read succeeds. If we mark\n // it here and the re-read fails (e.g. the hot entry has already been\n // migrated to cold storage), the same logical id that appears later in\n // the cold tier scan would be silently skipped, leaving a stale cold\n // fact unsuperseded. Deferring ensures that a failed primary-tier read\n // grants the alternate tier a chance to process the same id (PR #402\n // round-6 Finding 1 fix).\n\n const decision = shouldSupersedeExisting({\n candidate: memory.frontmatter,\n newEntityRef: args.entityRef,\n newAttributes: args.structuredAttributes,\n newCreatedAt: persistedCreatedAt,\n newMemoryId: args.newMemoryId,\n });\n if (!decision) {\n // No supersession decision — safe to mark as processed now so the\n // alternate tier doesn't re-evaluate an identical non-matching entry.\n processedIds.add(dedupeKey);\n continue;\n }\n\n try {\n // CAS-style re-read immediately before the write. `readAllMemories()`\n // is a snapshot — with concurrent writers, another run may have already\n // superseded this candidate since we loaded it. If we blindly trust the\n // snapshot we can clobber a newer `supersededBy` link with a stale one.\n //\n // File storage offers no true locking, so the best we can do is:\n // 1. re-read the exact file we're about to mutate\n // 2. verify status is still \"active\" and no `supersededBy` is set\n // 3. only then issue the write\n // If the re-read shows a newer concurrent writer beat us to it, skip.\n // This CAS pattern applies equally to hot and cold tier candidates.\n // Mark as processed AFTER confirming the candidate is readable so that\n // a migration-race read failure on the hot entry does not silently\n // prevent the cold entry from being evaluated (Finding 1, round 6).\n const fresh = await args.storage.readMemoryByPath(memory.path);\n if (!fresh) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: no longer readable at ${memory.path} — leaving id available for alternate tier`,\n );\n // Do NOT add to processedIds — allow the cold-tier copy to be\n // evaluated in the next iteration of the same scan.\n continue;\n }\n // Candidate is readable — mark the id as processed now to prevent the\n // alternate tier from double-writing.\n processedIds.add(dedupeKey);\n const freshStatus = fresh.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || fresh.frontmatter.supersededBy) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: already superseded by concurrent writer`,\n );\n continue;\n }\n\n // Finding 2 fix: the `supersededAt` / `updated` timestamps written to the\n // old fact must never run backwards relative to its own persisted\n // `created` timestamp. If the caller-supplied `args.createdAt` (which\n // represents \"when the new replacing fact was authored\") is earlier than\n // either the new fact's persisted `created` (T_new) or the old fact's\n // persisted `created` (T_old), we'd be writing a nonsensical\n // `supersededAt` that precedes the old memory's own creation. Clamp to\n // the monotonic maximum so time only moves forward.\n // This monotonic clamp is applied for both hot and cold tier writes.\n const oldCreatedMs = new Date(fresh.frontmatter.created).getTime();\n const newCreatedMs = new Date(persistedCreatedAt).getTime();\n const argCreatedMs = new Date(args.createdAt).getTime();\n const maxMs = Math.max(\n Number.isFinite(oldCreatedMs) ? oldCreatedMs : 0,\n Number.isFinite(newCreatedMs) ? newCreatedMs : 0,\n Number.isFinite(argCreatedMs) ? argCreatedMs : 0,\n );\n const supersededAt = new Date(maxMs).toISOString();\n\n // Issue #680 — explicit fact lifecycle. When the new fact\n // supersedes this one, set the predecessor's `invalid_at` to the\n // successor's effective valid_at. Skip when the predecessor\n // already carries an `invalid_at` so manual / earlier values\n // are preserved (idempotent).\n //\n // Codex P1 on PR #713: in the hash-dedup early-return path\n // (`useCallerTimestamp: true`), `newMemoryFile` is actually the\n // OLD matching fact — no new file was written — so its\n // `valid_at` is the predecessor's own old timestamp, not the\n // successor's effective time. Use `persistedCreatedAt`\n // directly in that path so the predecessor's invalid_at lines\n // up with the caller's wall-clock, not the matching fact's old\n // valid_at. The non-dedup path keeps the previous behavior\n // (prefer the new file's explicit valid_at, fall back to its\n // persisted created).\n let invalidAtPatch: string | undefined;\n if (!fresh.frontmatter.invalid_at) {\n if (args.useCallerTimestamp) {\n invalidAtPatch = persistedCreatedAt;\n } else {\n const newValidAt = newMemoryFile?.frontmatter.valid_at?.trim();\n invalidAtPatch =\n newValidAt && newValidAt.length > 0 ? newValidAt : persistedCreatedAt;\n }\n }\n const wrote = await args.storage.writeMemoryFrontmatter(\n fresh,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt,\n updated: supersededAt,\n ...(invalidAtPatch ? { invalid_at: invalidAtPatch } : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update\",\n relatedMemoryIds: [args.newMemoryId],\n },\n );\n if (wrote) {\n supersededIds.push(memory.frontmatter.id);\n for (const key of decision.matchedKeys) matchedKeys.add(key);\n await expireChildChunksForSupersededParent({\n storage: args.storage,\n allCandidates,\n parentId: fresh.frontmatter.id,\n newMemoryId: args.newMemoryId,\n supersededAt,\n invalidAt: invalidAtPatch ?? fresh.frontmatter.invalid_at,\n });\n }\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to mark ${memory.frontmatter.id} superseded: ${err}`,\n );\n }\n }\n\n if (supersededIds.length > 0) {\n log.debug(\n `temporal-supersession: marked ${supersededIds.length} memories superseded by ${args.newMemoryId} (keys=${Array.from(matchedKeys).join(\",\")})`,\n );\n }\n\n return { supersededIds, matchedKeys: Array.from(matchedKeys) };\n}\n\n/**\n * Recall-side filter: returns true when the candidate should be excluded\n * from recall because it has been temporally superseded. When\n * `includeInRecall` is true, this always returns false (the fact is kept),\n * matching the audit/history opt-in described in the config.\n */\nexport function shouldFilterSupersededFromRecall(\n frontmatter: MemoryFrontmatter,\n options: { enabled: boolean; includeInRecall: boolean },\n): boolean {\n if (!options.enabled) return false;\n if (options.includeInRecall) return false;\n return frontmatter.status === \"superseded\";\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/temporal-supersession.ts"],"sourcesContent":["/**\n * Temporal Supersession (issue #375)\n *\n * When a new fact lands with `structuredAttributes` keyed on a known\n * `entityRef`, any prior fact whose supersession key collides with the new\n * fact's key is marked `status: \"superseded\"` and linked via\n * `supersededBy` / `supersededAt`. Recall filters those superseded memories\n * by default so agents see only the \"current\" value per entity attribute.\n *\n * The algorithm is intentionally O(N) over the memory corpus per write, but\n * skips cheaply when the new fact has no structuredAttributes. It reuses the\n * cached `readAllMemories()` path so cost is amortized with the rest of the\n * write pipeline.\n */\nimport type { MemoryFile, MemoryFrontmatter } from \"./types.js\";\nimport type { StorageManager } from \"./storage.js\";\nimport { log } from \"./logger.js\";\nimport { effectiveValidAt } from \"./temporal-validity.js\";\n\n/**\n * Shared normalization for supersession key components.\n *\n * Trims surrounding whitespace, lowercases, then collapses any run of\n * whitespace OR hyphens to a single hyphen, and strips any leading/trailing\n * hyphens that result. Both `computeSupersessionKey` and\n * `lookupAttributeByNormalizedKey` must use this so that keys produced at\n * write time and keys used at lookup time are identical regardless of how\n * the LLM encoded whitespace, hyphens, or casing (Finding B fix).\n *\n * Symmetry guarantee: `\"foo bar\"`, `\"foo-bar\"`, `\"foo - bar\"`, and\n * `\"foo bar\"` all canonicalize to `\"foo-bar\"`.\n *\n * Exported so external tests can verify the canonical form.\n */\nexport function normalizeSupersessionKey(raw: string): string {\n return raw\n .trim()\n .toLowerCase()\n .replace(/[\\s\\-]+/g, \"-\")\n // The previous line already collapsed runs to single hyphens, so ^-|-$ is\n // equivalent to ^-+|-+$ here and drops the anchored quantifier flagged by\n // CodeQL js/polynomial-redos.\n .replace(/^-|-$/g, \"\");\n}\n\n/**\n * Stable supersession key for an (entityRef, attributeName) pair.\n *\n * The algorithm is:\n * - normalize the entityRef (trim, lower-case, collapse whitespace)\n * - normalize the attributeName the same way\n * - join with `::`\n *\n * Exported so tests and tools can recompute it without depending on storage.\n */\nexport function computeSupersessionKey(\n entityRef: string | undefined,\n attributeName: string,\n): string | null {\n if (!entityRef || typeof entityRef !== \"string\") return null;\n if (!attributeName || typeof attributeName !== \"string\") return null;\n const entity = normalizeSupersessionKey(entityRef);\n const attr = normalizeSupersessionKey(attributeName);\n if (entity.length === 0 || attr.length === 0) return null;\n return `${entity}::${attr}`;\n}\n\n/**\n * Compute the full set of supersession keys for a fact with structured\n * attributes. Returns an empty array if no keys can be derived.\n */\nexport function supersessionKeysForFact(spec: {\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n}): string[] {\n if (!spec.entityRef) return [];\n if (!spec.structuredAttributes) return [];\n const keys: string[] = [];\n for (const attrName of Object.keys(spec.structuredAttributes)) {\n const key = computeSupersessionKey(spec.entityRef, attrName);\n if (key) keys.push(key);\n }\n return keys;\n}\n\n/**\n * Look up a structured-attribute value by a raw key, normalizing both sides\n * with `normalizeSupersessionKey` before comparing. This ensures that keys\n * written by the LLM with mixed case, surrounding whitespace, or internal\n * whitespace (e.g. `\"City\"`, `\" city \"`, `\"job title\"`, `\"job-title\"`)\n * are all matched against normalized keys produced by `computeSupersessionKey`\n * (Finding B fix — uses the same helper so both sides are identical).\n *\n * The storage format is NOT changed — we only normalize at lookup time.\n */\nexport function lookupAttributeByNormalizedKey(\n attributes: Record<string, unknown>,\n rawKey: string,\n): unknown {\n const normalizedTarget = normalizeSupersessionKey(rawKey);\n for (const [k, v] of Object.entries(attributes)) {\n if (normalizeSupersessionKey(k) === normalizedTarget) return v;\n }\n return undefined;\n}\n\n/**\n * Decide whether an existing memory should be superseded by a newly-written\n * memory that carries the supplied supersession key set.\n *\n * Only memories that:\n * - are currently `active`\n * - share an `entityRef` with the new fact\n * - share at least one supersession key with the new fact\n * - are older than the new fact\n * - have a conflicting value (different string) for the overlapping key\n * are eligible. This keeps supersession local to the attribute that actually\n * changed — if fact A sets `{city: Austin, tool: vim}` and fact B sets\n * `{city: NYC}`, only the city attribute is superseded, not the tool.\n */\nexport function shouldSupersedeExisting(args: {\n candidate: MemoryFrontmatter;\n newEntityRef: string;\n newAttributes: Record<string, string>;\n newCreatedAt: string;\n newMemoryId: string;\n}): { matchedKeys: string[] } | null {\n const { candidate, newEntityRef, newAttributes, newCreatedAt, newMemoryId } = args;\n\n if (candidate.id === newMemoryId) return null;\n if (candidate.status && candidate.status !== \"active\") return null;\n if (!candidate.entityRef) return null;\n if (!candidate.structuredAttributes) return null;\n\n // Reuse the shared `normalizeSupersessionKey` helper so this comparison\n // cannot drift from the canonical form used to build supersession keys\n // elsewhere in this file.\n const candidateEntityNorm = normalizeSupersessionKey(candidate.entityRef);\n const newEntityNorm = normalizeSupersessionKey(newEntityRef);\n if (candidateEntityNorm !== newEntityNorm) return null;\n\n // Must be older than the new fact's effective validity start — equal\n // timestamps are ignored to avoid races within the same millisecond. When\n // replay/import supplies source time, valid_at must drive ordering instead\n // of wall-clock persistence time.\n const candidateCreated = Date.parse(effectiveValidAt(candidate));\n const newCreated = Date.parse(newCreatedAt);\n if (!Number.isFinite(candidateCreated) || !Number.isFinite(newCreated)) return null;\n if (candidateCreated >= newCreated) return null;\n\n const matchedKeys: string[] = [];\n for (const [attrName, newValue] of Object.entries(newAttributes)) {\n // Use normalized key lookup so mixed-case or whitespace-padded keys\n // stored by the LLM are matched correctly (Finding 2 fix).\n const candidateValue = lookupAttributeByNormalizedKey(\n candidate.structuredAttributes,\n attrName,\n );\n if (candidateValue === undefined) continue;\n // Only supersede on conflicting values — identical values are a no-op.\n if (normalizeValue(String(candidateValue)) === normalizeValue(newValue)) continue;\n const key = computeSupersessionKey(newEntityRef, attrName);\n if (key) matchedKeys.push(key);\n }\n\n return matchedKeys.length > 0 ? { matchedKeys } : null;\n}\n\nfunction normalizeValue(v: string): string {\n return v.trim().toLowerCase();\n}\n\nasync function expireChildChunksForSupersededParent(args: {\n storage: StorageManager;\n allCandidates: MemoryFile[];\n parentId: string;\n newMemoryId: string;\n supersededAt: string;\n invalidAt?: string;\n}): Promise<void> {\n const processedChunkIds = new Set<string>();\n const chunks = args.allCandidates.filter(\n (candidate) => candidate.frontmatter.parentId === args.parentId,\n );\n\n for (const chunk of chunks) {\n const chunkKey = chunk.frontmatter.id ?? chunk.path;\n if (processedChunkIds.has(chunkKey)) continue;\n\n try {\n const freshChunk = await args.storage.readMemoryByPath(chunk.path);\n if (!freshChunk) continue;\n processedChunkIds.add(chunkKey);\n const freshStatus = freshChunk.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || freshChunk.frontmatter.supersededBy) continue;\n\n await args.storage.writeMemoryFrontmatter(\n freshChunk,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt: args.supersededAt,\n updated: args.supersededAt,\n ...(args.invalidAt && !freshChunk.frontmatter.invalid_at\n ? { invalid_at: args.invalidAt }\n : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update-child-chunk\",\n relatedMemoryIds: [args.newMemoryId, args.parentId],\n },\n );\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to expire child chunk ${chunk.frontmatter.id} for parent ${args.parentId}: ${err}`,\n );\n }\n }\n}\n\nexport interface TemporalSupersessionResult {\n supersededIds: string[];\n matchedKeys: string[];\n}\n\n/**\n * Scan existing memories and mark any that are superseded by the\n * just-written memory. Fails open on I/O errors — the new memory is already\n * on disk, and supersession is a best-effort hygiene step.\n */\nexport async function applyTemporalSupersession(args: {\n storage: StorageManager;\n newMemoryId: string;\n entityRef?: string;\n structuredAttributes?: Record<string, string>;\n createdAt: string;\n enabled: boolean;\n /**\n * When true, skip the persisted `frontmatter.created` lookup and use\n * `args.createdAt` directly as the ordering anchor. Set this on the\n * hash-dedup short-circuit path where `newMemoryId` points to an existing\n * OLD fact (no new file is written) and its persisted timestamp would be\n * stale relative to the incoming promotion event (PR #402 Finding Uyui).\n */\n useCallerTimestamp?: boolean;\n}): Promise<TemporalSupersessionResult> {\n const empty: TemporalSupersessionResult = { supersededIds: [], matchedKeys: [] };\n if (!args.enabled) return empty;\n if (!args.entityRef) return empty;\n if (!args.structuredAttributes) return empty;\n if (Object.keys(args.structuredAttributes).length === 0) return empty;\n\n const newKeys = supersessionKeysForFact({\n entityRef: args.entityRef,\n structuredAttributes: args.structuredAttributes,\n });\n if (newKeys.length === 0) return empty;\n\n let hotMemories: MemoryFile[];\n try {\n hotMemories = await args.storage.readAllMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllMemories failed: ${err}`);\n return empty;\n }\n\n // Finding 1 fix: use the on-disk effective validity start of the\n // newly-written memory rather than a wall-clock timestamp sampled after\n // `writeMemory` returns. In concurrent writers the two can differ by enough\n // to cause wrong-direction supersession. If source replay/import provided\n // valid_at, it must drive ordering; otherwise created remains the legacy\n // fallback. If the memory is not yet visible in the cache (edge case during\n // fast concurrent writes) fall back to args.createdAt.\n //\n // PR #402 round-12 (Finding Uyui): on the hash-dedup early-return path the\n // caller supplies the OLD matching fact's id as `newMemoryId` (no new file is\n // written). That makes `newMemoryFile.frontmatter.created` an arbitrarily\n // old timestamp. When `args.useCallerTimestamp` is set the caller explicitly\n // opts out of the persisted-timestamp lookup so `args.createdAt` (the\n // incoming event time: source valid_at when present, otherwise wall-clock) is\n // used directly, keeping ordering correct regardless of how old the matching\n // fact is.\n const newMemoryFile = hotMemories.find((m) => m.frontmatter.id === args.newMemoryId);\n const persistedCreatedAt = args.useCallerTimestamp\n ? args.createdAt\n : (newMemoryFile ? effectiveValidAt(newMemoryFile.frontmatter) : args.createdAt);\n\n const supersededIds: string[] = [];\n const matchedKeys = new Set<string>();\n\n // Process hot then cold. Hot-then-cold ordering is safer because hot\n // writes are more frequent and the CAS re-read guards against double-writes.\n // A Set<string> of already-processed ids ensures that a memory visible in\n // both tiers (same logical memory with different filesystem paths during a\n // migration race) is processed at most once. Keying on `frontmatter.id`\n // is correct because the same logical memory has the same id regardless of\n // which tier's directory it currently lives in (PR #402 Finding 1 fix).\n // Fall back to path-based keying when id is absent (defensive).\n const processedIds = new Set<string>();\n\n // Finding UOGi fix (round-6): readAllColdMemories() performs a full uncached\n // recursive directory scan of cold/. After Finding UTsP broadened the scan\n // to cover the entire cold root (not just facts/+corrections/), the per-call\n // cost grows with the cold tree size.\n //\n // The fix is a TTL-based in-memory cache inside StorageManager\n // (readAllColdMemories caches its result for COLD_SCAN_CACHE_TTL_MS) that is\n // shared across consecutive supersession calls within the same write burst.\n // The cache is invalidated automatically on any hot→cold demotion (which\n // calls invalidateAllMemoriesCache, which also clears the cold cache) and\n // expires after the TTL as a safety net.\n //\n // This means back-to-back structured-attribute writes in the same burst\n // (e.g. batch extraction) pay the cold I/O cost at most once, not N times.\n // Correctness is preserved because the cache TTL ensures eventual consistency\n // and the invalidation hook covers the hot→cold path.\n\n let coldMemories: MemoryFile[];\n try {\n coldMemories = await args.storage.readAllColdMemories();\n } catch (err) {\n log.warn(`temporal-supersession: readAllColdMemories failed: ${err}`);\n coldMemories = [];\n }\n\n // Combine hot and cold memories into a single scan. New memory itself is\n // excluded inline. We do NOT skip cold scan when hot produced zero\n // supersessions — the P1 finding is precisely that stale cold facts leak\n // when hot has no hits.\n const allCandidates: MemoryFile[] = [...hotMemories, ...coldMemories];\n\n for (const memory of allCandidates) {\n if (memory.frontmatter.id === args.newMemoryId) continue;\n const dedupeKey = memory.frontmatter.id ?? memory.path;\n if (processedIds.has(dedupeKey)) continue;\n const snapshotStatus = memory.frontmatter.status ?? \"active\";\n if (snapshotStatus !== \"active\") {\n // A stale non-active snapshot entry must not suppress an active copy of\n // the same logical memory that appears later in another tier. This can\n // happen during hot/cold migration races where the hot snapshot is already\n // superseded but the cold copy is still active and should be evaluated.\n continue;\n }\n // NOTE: do NOT call processedIds.add(dedupeKey) here. We defer marking\n // the id as processed until AFTER the CAS re-read succeeds. If we mark\n // it here and the re-read fails (e.g. the hot entry has already been\n // migrated to cold storage), the same logical id that appears later in\n // the cold tier scan would be silently skipped, leaving a stale cold\n // fact unsuperseded. Deferring ensures that a failed primary-tier read\n // grants the alternate tier a chance to process the same id (PR #402\n // round-6 Finding 1 fix).\n\n const decision = shouldSupersedeExisting({\n candidate: memory.frontmatter,\n newEntityRef: args.entityRef,\n newAttributes: args.structuredAttributes,\n newCreatedAt: persistedCreatedAt,\n newMemoryId: args.newMemoryId,\n });\n if (!decision) {\n // No supersession decision — safe to mark as processed now so the\n // alternate tier doesn't re-evaluate an identical non-matching entry.\n processedIds.add(dedupeKey);\n continue;\n }\n\n try {\n // CAS-style re-read immediately before the write. `readAllMemories()`\n // is a snapshot — with concurrent writers, another run may have already\n // superseded this candidate since we loaded it. If we blindly trust the\n // snapshot we can clobber a newer `supersededBy` link with a stale one.\n //\n // File storage offers no true locking, so the best we can do is:\n // 1. re-read the exact file we're about to mutate\n // 2. verify status is still \"active\" and no `supersededBy` is set\n // 3. only then issue the write\n // If the re-read shows a newer concurrent writer beat us to it, skip.\n // This CAS pattern applies equally to hot and cold tier candidates.\n // Mark as processed AFTER confirming the candidate is readable so that\n // a migration-race read failure on the hot entry does not silently\n // prevent the cold entry from being evaluated (Finding 1, round 6).\n const fresh = await args.storage.readMemoryByPath(memory.path);\n if (!fresh) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: no longer readable at ${memory.path} — leaving id available for alternate tier`,\n );\n // Do NOT add to processedIds — allow the cold-tier copy to be\n // evaluated in the next iteration of the same scan.\n continue;\n }\n // Candidate is readable — mark the id as processed now to prevent the\n // alternate tier from double-writing.\n processedIds.add(dedupeKey);\n const freshStatus = fresh.frontmatter.status ?? \"active\";\n if (freshStatus !== \"active\" || fresh.frontmatter.supersededBy) {\n log.debug(\n `[engram] temporal supersession skipped candidate ${memory.frontmatter.id}: already superseded by concurrent writer`,\n );\n continue;\n }\n\n // Finding 2 fix: the `supersededAt` / `updated` timestamps written to the\n // old fact must never run backwards relative to its own persisted\n // `created` timestamp. If the caller-supplied `args.createdAt` (which\n // represents \"when the new replacing fact was authored\") is earlier than\n // either the new fact's persisted `created` (T_new) or the old fact's\n // persisted `created` (T_old), we'd be writing a nonsensical\n // `supersededAt` that precedes the old memory's own creation. Clamp to\n // the monotonic maximum so time only moves forward.\n // This monotonic clamp is applied for both hot and cold tier writes.\n const oldCreatedMs = new Date(fresh.frontmatter.created).getTime();\n const newCreatedMs = new Date(persistedCreatedAt).getTime();\n const argCreatedMs = new Date(args.createdAt).getTime();\n const maxMs = Math.max(\n Number.isFinite(oldCreatedMs) ? oldCreatedMs : 0,\n Number.isFinite(newCreatedMs) ? newCreatedMs : 0,\n Number.isFinite(argCreatedMs) ? argCreatedMs : 0,\n );\n const supersededAt = new Date(maxMs).toISOString();\n\n // Issue #680 — explicit fact lifecycle. When the new fact\n // supersedes this one, set the predecessor's `invalid_at` to the\n // successor's effective valid_at. Skip when the predecessor\n // already carries an `invalid_at` so manual / earlier values\n // are preserved (idempotent).\n //\n // Codex P1 on PR #713: in the hash-dedup early-return path\n // (`useCallerTimestamp: true`), `newMemoryFile` is actually the\n // OLD matching fact — no new file was written — so its\n // `valid_at` is the predecessor's own old timestamp, not the\n // successor's effective time. Use `persistedCreatedAt`\n // directly in that path so the predecessor's invalid_at lines\n // up with the caller's wall-clock, not the matching fact's old\n // valid_at. The non-dedup path keeps the previous behavior\n // (prefer the new file's explicit valid_at, fall back to its\n // persisted created).\n let invalidAtPatch: string | undefined;\n if (!fresh.frontmatter.invalid_at) {\n if (args.useCallerTimestamp) {\n invalidAtPatch = persistedCreatedAt;\n } else {\n const newValidAt = newMemoryFile?.frontmatter.valid_at?.trim();\n invalidAtPatch =\n newValidAt && newValidAt.length > 0 ? newValidAt : persistedCreatedAt;\n }\n }\n const wrote = await args.storage.writeMemoryFrontmatter(\n fresh,\n {\n status: \"superseded\",\n supersededBy: args.newMemoryId,\n supersededAt,\n updated: supersededAt,\n ...(invalidAtPatch ? { invalid_at: invalidAtPatch } : {}),\n },\n {\n actor: \"temporal-supersession\",\n reasonCode: \"structured-attribute-update\",\n relatedMemoryIds: [args.newMemoryId],\n },\n );\n if (wrote) {\n supersededIds.push(memory.frontmatter.id);\n for (const key of decision.matchedKeys) matchedKeys.add(key);\n await expireChildChunksForSupersededParent({\n storage: args.storage,\n allCandidates,\n parentId: fresh.frontmatter.id,\n newMemoryId: args.newMemoryId,\n supersededAt,\n invalidAt: invalidAtPatch ?? fresh.frontmatter.invalid_at,\n });\n // Issue #1579 — emit a tombstone for the superseded memory so it\n // cannot resurrect through re-extraction / import / consolidation /\n // dreams / pattern-reinforcement. The supersession key is the first\n // matched key (the write-time supersession identity). Best-effort:\n // a tombstone append failure is logged but never fails supersession\n // (gotcha #13).\n // Issue #1579 thread ObteS: persist ALL matched supersession keys, not\n // just the first. A later paraphrased re-observation may derive the\n // same entity but place the tombstoned attribute at a different\n // position in structuredAttributes; without an entry for every matched\n // key, the keyed tier would miss and the fact could resurrect as\n // active. Pass the canonical contentHash from the retired memory's\n // frontmatter so the tombstone's exact tier matches re-extraction\n // (review: citation-hash alignment). The rawContent is the stored body;\n // the StorageManager.appendTombstone chokepoint strips citation\n // annotations for the normalized-text tier.\n const keysToTombstone =\n decision.matchedKeys.length > 0 ? decision.matchedKeys : [undefined];\n for (const key of keysToTombstone) {\n try {\n await args.storage.appendTombstone({\n reason: \"supersession\",\n createdBy: \"supersession\",\n sourceMemoryId: fresh.frontmatter.id,\n ...(fresh.frontmatter.contentHash\n ? { contentHash: fresh.frontmatter.contentHash }\n : {}),\n rawContent: fresh.content,\n ...(fresh.frontmatter.entityRef ? { entityRef: fresh.frontmatter.entityRef } : {}),\n ...(key ? { supersessionKey: key } : {}),\n createdAt: supersededAt,\n });\n } catch (tombErr) {\n log.warn(\n `temporal-supersession: tombstone emit failed for ${fresh.frontmatter.id}${key ? ` (key=${key})` : \"\"} : ${tombErr}`,\n );\n }\n }\n }\n } catch (err) {\n log.warn(\n `temporal-supersession: failed to mark ${memory.frontmatter.id} superseded: ${err}`,\n );\n }\n }\n\n if (supersededIds.length > 0) {\n log.debug(\n `temporal-supersession: marked ${supersededIds.length} memories superseded by ${args.newMemoryId} (keys=${Array.from(matchedKeys).join(\",\")})`,\n );\n }\n\n return { supersededIds, matchedKeys: Array.from(matchedKeys) };\n}\n\n/**\n * Recall-side filter: returns true when the candidate should be excluded\n * from recall because it has been temporally superseded. When\n * `includeInRecall` is true, this always returns false (the fact is kept),\n * matching the audit/history opt-in described in the config.\n */\nexport function shouldFilterSupersededFromRecall(\n frontmatter: MemoryFrontmatter,\n options: { enabled: boolean; includeInRecall: boolean },\n): boolean {\n if (!options.enabled) return false;\n if (options.includeInRecall) return false;\n return frontmatter.status === \"superseded\";\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ACTION_CONFIDENCE_CONTEXT_READINESS,
|
|
16
16
|
ACTION_CONFIDENCE_RISK_CATEGORIES,
|
|
17
17
|
ACTION_CONFIDENCE_RULE_KINDS
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-Z2YFLXHU.js";
|
|
19
19
|
|
|
20
20
|
// src/access-schema.ts
|
|
21
21
|
import { z } from "zod";
|
|
@@ -433,4 +433,4 @@ export {
|
|
|
433
433
|
actionConfidenceRequestSchema,
|
|
434
434
|
validateRequest
|
|
435
435
|
};
|
|
436
|
-
//# sourceMappingURL=chunk-
|
|
436
|
+
//# sourceMappingURL=chunk-Q4O3ET6F.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StorageManager
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SFMRLXIV.js";
|
|
4
4
|
import {
|
|
5
5
|
getCachedRuleMemories,
|
|
6
6
|
setCachedRuleMemories
|
|
@@ -117,4 +117,4 @@ export {
|
|
|
117
117
|
compareVerifiedSemanticRuleResults,
|
|
118
118
|
searchVerifiedSemanticRules
|
|
119
119
|
};
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-QP37KL5H.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isRecallDisclosure
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-IKNQAGBV.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizeRetrievedMemoryProvenance
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-I4WBNM3M.js";
|
|
7
7
|
|
|
8
8
|
// src/recall-xray.ts
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
@@ -269,4 +269,4 @@ export {
|
|
|
269
269
|
RecallXrayBuilder,
|
|
270
270
|
summarizeDisclosureTokens
|
|
271
271
|
};
|
|
272
|
-
//# sourceMappingURL=chunk-
|
|
272
|
+
//# sourceMappingURL=chunk-SDPDU2PM.js.map
|