@remnic/core 9.3.712 → 9.3.714
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 -6
- package/dist/access-boundary.js +17 -16
- package/dist/access-cli.js +40 -39
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +24 -23
- package/dist/access-mcp.d.ts +18 -5
- package/dist/access-mcp.js +21 -20
- package/dist/access-operations-batch.js +18 -17
- package/dist/access-operations.d.ts +6 -5
- package/dist/access-operations.js +20 -19
- package/dist/access-schema.d.ts +10 -0
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-Dx2rSJjH.d.ts → access-service-lddjZoRh.d.ts} +20 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +16 -15
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +6 -1
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-memory-bridge.js +2 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -3
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- 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 +8 -7
- 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-CadPvH29.d.ts → catalog-CbR0CsUw.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 +9 -8
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-OMSTU233.js → chunk-2KAYTPPT.js} +4 -4
- package/dist/{chunk-VYZNWH4J.js → chunk-2SXMCQEQ.js} +3 -3
- package/dist/{chunk-NXL5CVE7.js → chunk-2Y6LMOQQ.js} +2 -2
- package/dist/{chunk-AMXOCZHE.js → chunk-3VUPWDSE.js} +15 -3
- package/dist/{chunk-AMXOCZHE.js.map → chunk-3VUPWDSE.js.map} +1 -1
- package/dist/{chunk-FVQJYWH7.js → chunk-5GKPFL3S.js} +1 -1
- package/dist/{chunk-FVQJYWH7.js.map → chunk-5GKPFL3S.js.map} +1 -1
- package/dist/{chunk-LQ4J7ELC.js → chunk-67HG7KFE.js} +2 -2
- package/dist/{chunk-ZBG4K36I.js → chunk-6FUJ2VYY.js} +3 -3
- package/dist/{chunk-GMRNKPWO.js → chunk-6QUPUTT7.js} +2 -2
- package/dist/{chunk-Z56IHRVV.js → chunk-6UU43ZGD.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-OWOKOFXK.js → chunk-AJOIM6UR.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-YVWZW37D.js → chunk-BNOAB5HH.js} +62 -12
- package/dist/{chunk-YVWZW37D.js.map → chunk-BNOAB5HH.js.map} +1 -1
- package/dist/{chunk-A2R5NV6O.js → chunk-CTAHKIG6.js} +205 -29
- package/dist/chunk-CTAHKIG6.js.map +1 -0
- package/dist/{chunk-K22IJO4R.js → chunk-CTOQEZSN.js} +2 -2
- package/dist/{chunk-SINGJCUR.js → chunk-CX77GNMB.js} +14 -3
- package/dist/chunk-CX77GNMB.js.map +1 -0
- package/dist/{chunk-HSDJCT3V.js → chunk-DXGS5YOQ.js} +3 -3
- package/dist/{chunk-HV57RHMD.js → chunk-EH5ED3C6.js} +2 -2
- package/dist/{chunk-NUWZFSKC.js → chunk-FVZ2ISIX.js} +9 -1
- package/dist/chunk-FVZ2ISIX.js.map +1 -0
- package/dist/{chunk-GKPIY7EQ.js → chunk-FXZKXW7C.js} +117 -46
- package/dist/chunk-FXZKXW7C.js.map +1 -0
- package/dist/{chunk-7NDYFAJS.js → chunk-GM3BMWKR.js} +2 -2
- package/dist/{chunk-ZVDRL6SK.js → chunk-HHLWSWWF.js} +2 -2
- package/dist/chunk-HHLWSWWF.js.map +1 -0
- package/dist/{chunk-5QUS24F7.js → chunk-I56LKMF7.js} +2 -2
- package/dist/{chunk-Z3B2YW56.js → chunk-IHMT6XTY.js} +9 -5
- package/dist/chunk-IHMT6XTY.js.map +1 -0
- package/dist/chunk-JAKIDQB2.js +173 -0
- package/dist/chunk-JAKIDQB2.js.map +1 -0
- package/dist/{chunk-35YJ6KCV.js → chunk-JEIROMLX.js} +2 -2
- package/dist/{chunk-SKQCFAYU.js → chunk-JVEMUX6L.js} +2 -2
- package/dist/{chunk-CXMXAC5R.js → chunk-K44MIN6D.js} +3 -3
- package/dist/{chunk-5KQCOIPW.js → chunk-MECU6WSH.js} +188 -31
- package/dist/chunk-MECU6WSH.js.map +1 -0
- package/dist/{chunk-BQCFXAMN.js → chunk-MPHDVHIB.js} +2 -2
- package/dist/{chunk-BOGENF7P.js → chunk-MSVSP5VO.js} +1 -1
- package/dist/chunk-MSVSP5VO.js.map +1 -0
- package/dist/{chunk-QXNFQKWU.js → chunk-MTHNJCOJ.js} +2 -2
- package/dist/{chunk-3TCRU4JA.js → chunk-O62P2LFL.js} +2 -2
- package/dist/{chunk-JSDZMOT7.js → chunk-OC56UWV5.js} +11 -11
- package/dist/{chunk-7IBEWQLG.js → chunk-QOIONSOI.js} +2 -2
- package/dist/{chunk-4P5UAL3C.js → chunk-RBHANGVX.js} +6 -6
- package/dist/{chunk-SK2CR6MW.js → chunk-RVLB4E6N.js} +2 -2
- package/dist/{chunk-I3BT2IDW.js → chunk-SYDHFFAW.js} +1110 -28
- package/dist/chunk-SYDHFFAW.js.map +1 -0
- package/dist/{chunk-VILEUJXC.js → chunk-UC4YOAC2.js} +74 -2
- package/dist/{chunk-VILEUJXC.js.map → chunk-UC4YOAC2.js.map} +1 -1
- package/dist/{chunk-CNVIWMQI.js → chunk-UY4JMEAK.js} +2 -2
- package/dist/{chunk-S6FQLQGH.js → chunk-V7GDTQ6I.js} +2 -2
- package/dist/{chunk-JNOYYWCA.js → chunk-VO4UICII.js} +3 -3
- package/dist/{chunk-54TI5GLV.js → chunk-WI4QX7GH.js} +3 -3
- package/dist/{chunk-3XHD3XGK.js → chunk-XCTDHF7U.js} +30 -2
- package/dist/chunk-XCTDHF7U.js.map +1 -0
- package/dist/{chunk-XKU4YE6Z.js → chunk-XZTUMWKQ.js} +2 -2
- package/dist/{cli-DTWRoJEI.d.ts → cli-iAiVkbYJ.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +43 -42
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +8 -7
- 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 +4 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +8 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +9 -8
- 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 +8 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +3 -2
- 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 +4 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +62 -61
- 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/index.js +3 -3
- 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 +8 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -8
- 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-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 +12 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +8 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -9
- package/dist/{orchestrator-B1iVjEyc.d.ts → orchestrator-iVZWi8aW.d.ts} +21 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +32 -31
- 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 +3 -2
- 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 +3 -3
- package/dist/recall-handles.d.ts +228 -0
- package/dist/recall-handles.js +42 -0
- package/dist/recall-handles.js.map +1 -0
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +40 -2
- package/dist/recall-state.js +3 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -4
- 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/sanitize.d.ts +2 -0
- package/dist/sanitize.js +6 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +4 -4
- 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-CY8uVRNJ.d.ts → semantic-consolidation-DSjMDZ1u.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +9 -8
- package/dist/semantic-rule-promotion.js +8 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +8 -7
- 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 +1 -1
- package/dist/storage.js +7 -6
- 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/import-sqlite.js +2 -2
- package/dist/{types-Dr0Cw3L9.d.ts → types-D9dwzU_x.d.ts} +25 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +8 -7
- package/package.json +2 -2
- package/src/access-boundary.ts +4 -1
- package/src/access-http.ts +78 -20
- package/src/access-mcp.test.ts +29 -0
- package/src/access-mcp.ts +99 -17
- package/src/access-operations-batch.ts +14 -0
- package/src/access-operations.ts +5 -0
- package/src/access-schema.ts +8 -0
- package/src/access-service-observe-idempotency.test.ts +308 -0
- package/src/access-service.ts +106 -5
- package/src/access-surface-catalog.test.ts +4 -2
- package/src/access-surface-catalog.ts +5 -0
- package/src/active-memory-bridge.test.ts +72 -0
- package/src/active-memory-bridge.ts +21 -2
- package/src/chat/chat-cli.test.ts +142 -0
- package/src/chat/chat-cli.ts +206 -0
- package/src/chat/chat-config.ts +57 -0
- package/src/chat/chat-engine.test.ts +272 -0
- package/src/chat/chat-engine.ts +518 -0
- package/src/chat/chat-executor.ts +133 -0
- package/src/chat/chat-factory.ts +161 -0
- package/src/chat/chat-http.test.ts +197 -0
- package/src/chat/chat-http.ts +222 -0
- package/src/chat/chat-llm.ts +229 -0
- package/src/chat/chat-mcp.test.ts +135 -0
- package/src/chat/chat-security.test.ts +491 -0
- package/src/chat/chat-session.ts +296 -0
- package/src/chat/chat-system-prompt.ts +76 -0
- package/src/chat/chat-tools.ts +255 -0
- package/src/chat/chat-types.ts +167 -0
- package/src/cli.ts +20 -0
- package/src/config.ts +9 -0
- package/src/orchestrator.ts +106 -5
- package/src/recall-handles-wiring.test.ts +477 -0
- package/src/recall-handles.test.ts +283 -0
- package/src/recall-handles.ts +414 -0
- package/src/recall-state.ts +99 -0
- package/src/sanitize.ts +6 -0
- package/src/types.ts +29 -0
- package/dist/chunk-3XHD3XGK.js.map +0 -1
- package/dist/chunk-5KQCOIPW.js.map +0 -1
- package/dist/chunk-A2R5NV6O.js.map +0 -1
- package/dist/chunk-BOGENF7P.js.map +0 -1
- package/dist/chunk-GKPIY7EQ.js.map +0 -1
- package/dist/chunk-I3BT2IDW.js.map +0 -1
- package/dist/chunk-NUWZFSKC.js.map +0 -1
- package/dist/chunk-SINGJCUR.js.map +0 -1
- package/dist/chunk-Z3B2YW56.js.map +0 -1
- package/dist/chunk-ZVDRL6SK.js.map +0 -1
- /package/dist/{chunk-OMSTU233.js.map → chunk-2KAYTPPT.js.map} +0 -0
- /package/dist/{chunk-VYZNWH4J.js.map → chunk-2SXMCQEQ.js.map} +0 -0
- /package/dist/{chunk-NXL5CVE7.js.map → chunk-2Y6LMOQQ.js.map} +0 -0
- /package/dist/{chunk-LQ4J7ELC.js.map → chunk-67HG7KFE.js.map} +0 -0
- /package/dist/{chunk-ZBG4K36I.js.map → chunk-6FUJ2VYY.js.map} +0 -0
- /package/dist/{chunk-GMRNKPWO.js.map → chunk-6QUPUTT7.js.map} +0 -0
- /package/dist/{chunk-Z56IHRVV.js.map → chunk-6UU43ZGD.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-OWOKOFXK.js.map → chunk-AJOIM6UR.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-K22IJO4R.js.map → chunk-CTOQEZSN.js.map} +0 -0
- /package/dist/{chunk-HSDJCT3V.js.map → chunk-DXGS5YOQ.js.map} +0 -0
- /package/dist/{chunk-HV57RHMD.js.map → chunk-EH5ED3C6.js.map} +0 -0
- /package/dist/{chunk-7NDYFAJS.js.map → chunk-GM3BMWKR.js.map} +0 -0
- /package/dist/{chunk-5QUS24F7.js.map → chunk-I56LKMF7.js.map} +0 -0
- /package/dist/{chunk-35YJ6KCV.js.map → chunk-JEIROMLX.js.map} +0 -0
- /package/dist/{chunk-SKQCFAYU.js.map → chunk-JVEMUX6L.js.map} +0 -0
- /package/dist/{chunk-CXMXAC5R.js.map → chunk-K44MIN6D.js.map} +0 -0
- /package/dist/{chunk-BQCFXAMN.js.map → chunk-MPHDVHIB.js.map} +0 -0
- /package/dist/{chunk-QXNFQKWU.js.map → chunk-MTHNJCOJ.js.map} +0 -0
- /package/dist/{chunk-3TCRU4JA.js.map → chunk-O62P2LFL.js.map} +0 -0
- /package/dist/{chunk-JSDZMOT7.js.map → chunk-OC56UWV5.js.map} +0 -0
- /package/dist/{chunk-7IBEWQLG.js.map → chunk-QOIONSOI.js.map} +0 -0
- /package/dist/{chunk-4P5UAL3C.js.map → chunk-RBHANGVX.js.map} +0 -0
- /package/dist/{chunk-SK2CR6MW.js.map → chunk-RVLB4E6N.js.map} +0 -0
- /package/dist/{chunk-CNVIWMQI.js.map → chunk-UY4JMEAK.js.map} +0 -0
- /package/dist/{chunk-S6FQLQGH.js.map → chunk-V7GDTQ6I.js.map} +0 -0
- /package/dist/{chunk-JNOYYWCA.js.map → chunk-VO4UICII.js.map} +0 -0
- /package/dist/{chunk-54TI5GLV.js.map → chunk-WI4QX7GH.js.map} +0 -0
- /package/dist/{chunk-XKU4YE6Z.js.map → chunk-XZTUMWKQ.js.map} +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
appendHandle,
|
|
6
|
+
DEFAULT_HANDLE_SNAPSHOT_DEPTH,
|
|
7
|
+
HANDLE_DEFAULT_WIDTH,
|
|
8
|
+
HANDLE_EXTENDED_WIDTH,
|
|
9
|
+
handleFor,
|
|
10
|
+
isHandleToken,
|
|
11
|
+
MEMORY_ID_PATTERN,
|
|
12
|
+
normalizeHandle,
|
|
13
|
+
parseHandles,
|
|
14
|
+
parseIdOrHandle,
|
|
15
|
+
renderHandle,
|
|
16
|
+
renderHandlesForInjection,
|
|
17
|
+
resolveHandle,
|
|
18
|
+
stripHandles,
|
|
19
|
+
type RecallSnapshotIds,
|
|
20
|
+
} from "./recall-handles.js";
|
|
21
|
+
|
|
22
|
+
// Deterministic handle for a fixed id, computed once for assertions.
|
|
23
|
+
const ID_A = "fact-1770469224307-eelr";
|
|
24
|
+
const HANDLE_A = handleFor(ID_A);
|
|
25
|
+
|
|
26
|
+
// ─── handleFor: determinism + width ───────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
test("handleFor is deterministic for the same id", () => {
|
|
29
|
+
assert.equal(handleFor(ID_A), handleFor(ID_A));
|
|
30
|
+
assert.equal(handleFor(ID_A).length, HANDLE_DEFAULT_WIDTH);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("handleFor yields a non-empty 4-hex string", () => {
|
|
34
|
+
const got = handleFor("fact-1");
|
|
35
|
+
assert.match(got, /^[0-9a-f]{4}$/);
|
|
36
|
+
assert.equal(got, handleFor("fact-1"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("handleFor wider width is a prefix-preserving extension", () => {
|
|
40
|
+
const h4 = handleFor(ID_A, 4);
|
|
41
|
+
const h6 = handleFor(ID_A, 6);
|
|
42
|
+
const h8 = handleFor(ID_A, 8);
|
|
43
|
+
assert.equal(h6.slice(0, 4), h4);
|
|
44
|
+
assert.equal(h8.slice(0, 6), h6);
|
|
45
|
+
assert.equal(h4.length, 4);
|
|
46
|
+
assert.equal(h6.length, 6);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("handleFor clamps out-of-range widths", () => {
|
|
50
|
+
assert.equal(handleFor(ID_A, 2), handleFor(ID_A, 4));
|
|
51
|
+
assert.equal(handleFor(ID_A, 100), handleFor(ID_A, 8));
|
|
52
|
+
assert.equal(handleFor(ID_A, Number.NaN), handleFor(ID_A, 4));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("handleFor is derived from id, not content — stable across edits", () => {
|
|
56
|
+
// Two ids that differ only by a suffix get different handles.
|
|
57
|
+
assert.notEqual(handleFor("fact-1"), handleFor("fact-2"));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── renderHandle / appendHandle ──────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
test("renderHandle produces the bracketed token", () => {
|
|
63
|
+
assert.equal(renderHandle(ID_A), `[m:${HANDLE_A}]`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("appendHandle adds a single space before the handle and trims trailing ws", () => {
|
|
67
|
+
assert.equal(
|
|
68
|
+
appendHandle("API rate limit is 1000 rpm. ", ID_A),
|
|
69
|
+
`API rate limit is 1000 rpm. [m:${HANDLE_A}]`,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─── renderHandlesForInjection: collision extension ───────────────────────
|
|
74
|
+
|
|
75
|
+
test("renderHandlesForInjection widens EVERY member of a colliding group to 6 chars", () => {
|
|
76
|
+
// Engineer a collision: two synthetic ids whose sha256 prefix collides at 4
|
|
77
|
+
// chars. Search a small id space for a real collision so the test is honest.
|
|
78
|
+
let idA = "";
|
|
79
|
+
let idB = "";
|
|
80
|
+
const seen = new Map<string, string>();
|
|
81
|
+
for (let i = 0; i < 200_000 && !idA; i += 1) {
|
|
82
|
+
const id = `fact-${i}-probe`;
|
|
83
|
+
const h = handleFor(id, 4);
|
|
84
|
+
const prev = seen.get(h);
|
|
85
|
+
if (prev && prev !== id) {
|
|
86
|
+
idA = prev;
|
|
87
|
+
idB = id;
|
|
88
|
+
} else {
|
|
89
|
+
seen.set(h, id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (idA && idB) {
|
|
93
|
+
const entries = renderHandlesForInjection([idA, idB]);
|
|
94
|
+
assert.equal(entries.length, 2);
|
|
95
|
+
// BOTH colliding members widen: leaving the first at 4 chars would make its
|
|
96
|
+
// displayed handle ambiguous to resolve against the group (codex review).
|
|
97
|
+
assert.equal(entries[0]!.width, HANDLE_EXTENDED_WIDTH);
|
|
98
|
+
assert.equal(entries[1]!.width, HANDLE_EXTENDED_WIDTH);
|
|
99
|
+
// Both rendered tokens are unique in the set (6-char handles differ).
|
|
100
|
+
const tokens = entries.map((e) => e.handle);
|
|
101
|
+
assert.equal(new Set(tokens).size, tokens.length);
|
|
102
|
+
assert.notEqual(tokens[0], tokens[1]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("renderHandlesForInjection: no collision → all default width, unique", () => {
|
|
107
|
+
const ids = ["fact-1", "fact-2", "fact-3"];
|
|
108
|
+
const entries = renderHandlesForInjection(ids);
|
|
109
|
+
assert.equal(entries.length, 3);
|
|
110
|
+
for (const e of entries) assert.equal(e.width, HANDLE_DEFAULT_WIDTH);
|
|
111
|
+
const tokens = entries.map((e) => e.handle);
|
|
112
|
+
assert.equal(new Set(tokens).size, tokens.length);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("renderHandlesForInjection is idempotent and skips empty ids", () => {
|
|
116
|
+
const ids = ["fact-1", "", "fact-2"];
|
|
117
|
+
const e1 = renderHandlesForInjection(ids);
|
|
118
|
+
const e2 = renderHandlesForInjection(ids);
|
|
119
|
+
assert.deepEqual(e1, e2);
|
|
120
|
+
assert.equal(e1.length, 2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── parseHandles ─────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test("parseHandles extracts handles from prose, ignoring malformed", () => {
|
|
126
|
+
const text = "see [m:4f2a] (also [m:1b9e2f]) — not [m:XYZ!] or [m:ab] done";
|
|
127
|
+
assert.deepEqual(parseHandles(text), ["[m:4f2a]", "[m:1b9e2f]"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("parseHandles handles mid-sentence and punctuation-adjacent tokens", () => {
|
|
131
|
+
assert.deepEqual(parseHandles("[m:4f2a], [m:1b9e]; [m:0c7d]."), [
|
|
132
|
+
"[m:4f2a]",
|
|
133
|
+
"[m:1b9e]",
|
|
134
|
+
"[m:0c7d]",
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("parseHandles preserves duplicates and order", () => {
|
|
139
|
+
assert.deepEqual(parseHandles("[m:4f2a] and again [m:4f2a]"), [
|
|
140
|
+
"[m:4f2a]",
|
|
141
|
+
"[m:4f2a]",
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("parseHandles returns [] for non-string / empty", () => {
|
|
146
|
+
assert.deepEqual(parseHandles(""), []);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── normalizeHandle / isHandleToken ──────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
test("normalizeHandle accepts bracketed, prefixed, and bare hex forms", () => {
|
|
152
|
+
assert.equal(normalizeHandle("[m:4f2a]"), "4f2a");
|
|
153
|
+
assert.equal(normalizeHandle("m:4f2a"), "4f2a");
|
|
154
|
+
assert.equal(normalizeHandle("4f2a"), "4f2a");
|
|
155
|
+
assert.equal(normalizeHandle("[M:4F2A]"), "4f2a"); // case-insensitive
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("normalizeHandle rejects non-handles", () => {
|
|
159
|
+
assert.equal(normalizeHandle("fact-1-abc"), null);
|
|
160
|
+
assert.equal(normalizeHandle("[m:xyz!]"), null);
|
|
161
|
+
assert.equal(normalizeHandle("[m:ab]"), null); // too short
|
|
162
|
+
assert.equal(normalizeHandle(""), null);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("isHandleToken mirrors normalizeHandle", () => {
|
|
166
|
+
assert.equal(isHandleToken("[m:4f2a]"), true);
|
|
167
|
+
assert.equal(isHandleToken("fact-1"), false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("parseIdOrHandle classifies refs", () => {
|
|
171
|
+
const h = parseIdOrHandle("[m:4f2a]");
|
|
172
|
+
assert.equal(h.isHandle, true);
|
|
173
|
+
assert.equal(h.value, "4f2a");
|
|
174
|
+
const id = parseIdOrHandle("fact-1");
|
|
175
|
+
assert.equal(id.isHandle, false);
|
|
176
|
+
assert.equal(id.value, "fact-1");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── stripHandles ─────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
test("stripHandles removes tokens and tidies spacing", () => {
|
|
182
|
+
assert.equal(
|
|
183
|
+
stripHandles("API limit 1000 rpm. [m:4f2a] Also [m:1b9e]."),
|
|
184
|
+
"API limit 1000 rpm. Also.",
|
|
185
|
+
);
|
|
186
|
+
assert.equal(stripHandles("no handles here"), "no handles here");
|
|
187
|
+
assert.equal(stripHandles(""), "");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── resolveHandle ────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function snap(...ids: string[]): RecallSnapshotIds {
|
|
193
|
+
return { memoryIds: ids };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test("resolveHandle: hit returns the exact memoryId", () => {
|
|
197
|
+
const res = resolveHandle(HANDLE_A, [snap(ID_A, "fact-other")]);
|
|
198
|
+
assert.equal(res.ok, true);
|
|
199
|
+
if (res.ok) assert.equal(res.memoryId, ID_A);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("resolveHandle: miss is tagged not_found, never guessed", () => {
|
|
203
|
+
const res = resolveHandle("dead", [snap("fact-1")]);
|
|
204
|
+
assert.equal(res.ok, false);
|
|
205
|
+
if (!res.ok) assert.equal(res.reason, "not_found");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("resolveHandle: ambiguous collision across snapshots lists candidates", () => {
|
|
209
|
+
// Two distinct ids that share the same 4-char handle, in different snapshots.
|
|
210
|
+
let idA = "";
|
|
211
|
+
let idB = "";
|
|
212
|
+
const seen = new Map<string, string>();
|
|
213
|
+
for (let i = 0; i < 200_000 && !idA; i += 1) {
|
|
214
|
+
const id = `fact-${i}-amb`;
|
|
215
|
+
const h = handleFor(id, 4);
|
|
216
|
+
const prev = seen.get(h);
|
|
217
|
+
if (prev && prev !== id) {
|
|
218
|
+
idA = prev;
|
|
219
|
+
idB = id;
|
|
220
|
+
} else {
|
|
221
|
+
seen.set(h, id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (idA && idB) {
|
|
225
|
+
const res = resolveHandle(handleFor(idA, 4), [snap(idA), snap(idB)]);
|
|
226
|
+
assert.equal(res.ok, false);
|
|
227
|
+
if (!res.ok && res.reason === "ambiguous") {
|
|
228
|
+
assert.ok(res.candidates.includes(idA));
|
|
229
|
+
assert.ok(res.candidates.includes(idB));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("resolveHandle: snapshot depth is respected (older-than-N → miss)", () => {
|
|
235
|
+
// ID_A only in the 6th snapshot; depth 5 must miss.
|
|
236
|
+
const snapshots = [
|
|
237
|
+
snap("other-1"),
|
|
238
|
+
snap("other-2"),
|
|
239
|
+
snap("other-3"),
|
|
240
|
+
snap("other-4"),
|
|
241
|
+
snap("other-5"),
|
|
242
|
+
snap(ID_A),
|
|
243
|
+
];
|
|
244
|
+
const within = resolveHandle(HANDLE_A, snapshots, DEFAULT_HANDLE_SNAPSHOT_DEPTH);
|
|
245
|
+
assert.equal(within.ok, false); // depth 5 skips the 6th
|
|
246
|
+
if (!within.ok) assert.equal(within.reason, "not_found");
|
|
247
|
+
// Widening depth finds it.
|
|
248
|
+
const found = resolveHandle(HANDLE_A, snapshots, 6);
|
|
249
|
+
assert.equal(found.ok, true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("resolveHandle: a non-handle input is not_found, not a throw", () => {
|
|
253
|
+
const res = resolveHandle("fact-1", [snap("fact-1")]);
|
|
254
|
+
assert.equal(res.ok, false);
|
|
255
|
+
if (!res.ok) assert.equal(res.reason, "not_found");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("resolveHandle: newest-first ordering prefers the most recent recall", () => {
|
|
259
|
+
// Same id appears in two snapshots; resolution still yields a single match.
|
|
260
|
+
const res = resolveHandle(HANDLE_A, [snap(ID_A), snap(ID_A)]);
|
|
261
|
+
assert.equal(res.ok, true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
// ─── MEMORY_ID_PATTERN: handle eligibility gate (codex review) ──────────────
|
|
266
|
+
|
|
267
|
+
test("MEMORY_ID_PATTERN accepts underscore categories like reasoning_trace", () => {
|
|
268
|
+
// MemoryCategory includes reasoning_trace; StorageManager writes ids as
|
|
269
|
+
// `${category}-${Date.now()}-...`, so these must be handle-eligible.
|
|
270
|
+
assert.equal(MEMORY_ID_PATTERN.test("reasoning_trace-1770469224307-eelr"), true);
|
|
271
|
+
assert.equal(MEMORY_ID_PATTERN.test("fact-1770469224307-eelr"), true);
|
|
272
|
+
assert.equal(MEMORY_ID_PATTERN.test("artifact-1770469224308-ab12"), true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("MEMORY_ID_PATTERN rejects entity basenames and non-memory rows", () => {
|
|
276
|
+
// Entity reconstructions (entities/Widget.md) and bare names must NOT receive
|
|
277
|
+
// a handle — citing one would resolve to an unloadable basename.
|
|
278
|
+
assert.equal(MEMORY_ID_PATTERN.test("Widget"), false);
|
|
279
|
+
assert.equal(MEMORY_ID_PATTERN.test("entities/Widget.md"), false);
|
|
280
|
+
assert.equal(MEMORY_ID_PATTERN.test("Widget.md"), false);
|
|
281
|
+
// Category must start lowercase; capitalized names fail.
|
|
282
|
+
assert.equal(MEMORY_ID_PATTERN.test("Reasoning_trace-1-abc"), false);
|
|
283
|
+
});
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-handles.ts — injection-time memory handles (issue #1582).
|
|
3
|
+
*
|
|
4
|
+
* Every injected memory gets a stable short handle — `[m:4f2a]` — so a user
|
|
5
|
+
* (or the agent) can react to ONE specific memory in-band ("[m:4f2a] is
|
|
6
|
+
* stale", thumbs-down, "correct that") without a context switch to a CLI or
|
|
7
|
+
* console. The handle is derived from the memory **id** (never the content),
|
|
8
|
+
* so it is stable across edits/versioning.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE — no I/O, no side effects, no state. It owns:
|
|
11
|
+
* - {@link handleFor} — deterministic 4-hex handle from a memory id.
|
|
12
|
+
* - {@link renderHandle} / {@link renderHandlesForInjection} — render the
|
|
13
|
+
* `[m:xxxx]` token for one memory (or a whole injection set, extending to
|
|
14
|
+
* 6 chars on intra-injection collision).
|
|
15
|
+
* - {@link parseHandles} — extract handle tokens from prose.
|
|
16
|
+
* - {@link normalizeHandle} / {@link isHandleToken} — classify a string.
|
|
17
|
+
* - {@link resolveHandle} — map a handle back to a memory id against the
|
|
18
|
+
* session's recent recall snapshots (hit / not-found / ambiguous).
|
|
19
|
+
*
|
|
20
|
+
* Design rules honored (issue #1582 design + pitfalls):
|
|
21
|
+
* - Handles are derived from the id only — never hashed with content, never
|
|
22
|
+
* persisted into memory files or rawContent (rule 23). Rendering happens
|
|
23
|
+
* at injection time only.
|
|
24
|
+
* - Resolution is per-session and snapshot-scoped — there is NEVER a global
|
|
25
|
+
* handle→id map (collision space too small globally, leaks across
|
|
26
|
+
* principals — rule 42). Misses are tagged, never guessed (rule 34/51).
|
|
27
|
+
* - The formatter is allocation-light: a string append at render time, no
|
|
28
|
+
* per-memory object churn, because it runs on every recall.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createHash } from "node:crypto";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default handle width in hex characters (4 → 65 536-handle space).
|
|
35
|
+
* Per-injection sets are ~10–40 memories, so in-context collisions are
|
|
36
|
+
* vanishingly rare; {@link renderHandlesForInjection} widens the colliding
|
|
37
|
+
* member to {@link HANDLE_EXTENDED_WIDTH} when two ids in ONE injection would
|
|
38
|
+
* collide.
|
|
39
|
+
*/
|
|
40
|
+
export const HANDLE_DEFAULT_WIDTH = 4;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Width used to disambiguate two ids that collide at the default width within
|
|
44
|
+
* a single injection. 6 hex chars → ~16 M-handle space, which is comfortably
|
|
45
|
+
* beyond per-injection set sizes.
|
|
46
|
+
*/
|
|
47
|
+
export const HANDLE_EXTENDED_WIDTH = 6;
|
|
48
|
+
|
|
49
|
+
/** Minimum/maximum accepted hex widths for a rendered/parsed handle. */
|
|
50
|
+
export const HANDLE_MIN_WIDTH = 4;
|
|
51
|
+
export const HANDLE_MAX_WIDTH = 8;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Regex matching a rendered handle token anywhere in prose: `[m:` followed by
|
|
55
|
+
* 4–8 lowercase hex chars and a closing `]`. Malformed tokens (`[m:xyz!]`,
|
|
56
|
+
* `[m:abc]` with uppercase, missing bracket) are intentionally NOT matched.
|
|
57
|
+
* Used by {@link parseHandles} and the sanitizer.
|
|
58
|
+
*/
|
|
59
|
+
export const HANDLE_REGEX = /\[m:[0-9a-f]{4,8}\]/g;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Memory ids are `<category>-<timestamp>-<suffix>` (e.g. `fact-1770469224307-eelr`,
|
|
63
|
+
* `artifact-...`, `reasoning_trace-...`, plus parent-`-chunk-N` variants). The
|
|
64
|
+
* category segment allows underscores because `MemoryCategory` includes
|
|
65
|
+
* `reasoning_trace` (codex review); bare names (`Widget`) still fail. Entity
|
|
66
|
+
* reconstructions and other non-memory `.md` rows use bare names (`Widget`)
|
|
67
|
+
* that must NOT receive a handle — citing one would resolve to a basename no
|
|
68
|
+
* storage can load. This pattern gates handle rendering/recording to plausible
|
|
69
|
+
* memory ids only (issue #1582, codex review).
|
|
70
|
+
*/
|
|
71
|
+
export const MEMORY_ID_PATTERN = /^[a-z][a-z0-9_]*-\d+-[a-z0-9-]+$/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Derive the deterministic handle hex for a memory id at a given width.
|
|
75
|
+
*
|
|
76
|
+
* `handleFor(id) === handleFor(id)` always, and `handleFor(id, w)` is a prefix
|
|
77
|
+
* of `handleFor(id, w+1)` (both slice the same sha256 hex digest), so widening
|
|
78
|
+
* for collision-disambiguation never changes the shorter prefix.
|
|
79
|
+
*
|
|
80
|
+
* @param memoryId Stable memory id (e.g. `fact-1770469224307-eelr`).
|
|
81
|
+
* @param width Hex width. Defaults to {@link HANDLE_DEFAULT_WIDTH}.
|
|
82
|
+
* Clamped to [4, 8]; the digest has 64 hex chars available.
|
|
83
|
+
*/
|
|
84
|
+
export function handleFor(memoryId: string, width: number = HANDLE_DEFAULT_WIDTH): string {
|
|
85
|
+
const w = clampWidth(width);
|
|
86
|
+
return createHash("sha256").update(memoryId).digest("hex").slice(0, w);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clampWidth(width: number): number {
|
|
90
|
+
if (!Number.isFinite(width)) return HANDLE_DEFAULT_WIDTH;
|
|
91
|
+
return Math.min(HANDLE_MAX_WIDTH, Math.max(HANDLE_MIN_WIDTH, Math.floor(width)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render the `[m:xxxx]` token for one memory id. Width override is for the
|
|
96
|
+
* collision-extension path in {@link renderHandlesForInjection}; callers that
|
|
97
|
+
* render a single handle in isolation should omit it.
|
|
98
|
+
*/
|
|
99
|
+
export function renderHandle(memoryId: string, widthOverride?: number): string {
|
|
100
|
+
return `[m:${handleFor(memoryId, widthOverride ?? HANDLE_DEFAULT_WIDTH)}]`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Append a handle to a memory line, allocation-light. When `widthOverride` is
|
|
105
|
+
* omitted the default-width handle is used; pass the widened width from
|
|
106
|
+
* {@link renderHandlesForInjection} for a colliding member.
|
|
107
|
+
*
|
|
108
|
+
* appendHandle("API rate limit is 1000 rpm.", "fact-1-abc")
|
|
109
|
+
* // → "API rate limit is 1000 rpm. [m:4f2a]"
|
|
110
|
+
*/
|
|
111
|
+
export function appendHandle(line: string, memoryId: string, widthOverride?: number): string {
|
|
112
|
+
const handle = renderHandle(memoryId, widthOverride);
|
|
113
|
+
// Single trailing space before the handle; collapse a double space if the
|
|
114
|
+
// line already ended in whitespace so output stays clean.
|
|
115
|
+
const trimmed = line.replace(/\s+$/, "");
|
|
116
|
+
return `${trimmed} ${handle}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Result of {@link renderHandlesForInjection}: for each memory id, the width
|
|
121
|
+
* used (default 4, or 6 when widened to break an intra-injection collision)
|
|
122
|
+
* and the rendered token. Ordered for stable iteration.
|
|
123
|
+
*/
|
|
124
|
+
export interface InjectionHandleEntry {
|
|
125
|
+
memoryId: string;
|
|
126
|
+
width: number;
|
|
127
|
+
handle: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build handles for a whole injection set, widening to 6 chars when two ids
|
|
132
|
+
* collide at the default width within THIS set (so each rendered token is
|
|
133
|
+
* unique in context). Idempotent for the same input.
|
|
134
|
+
*
|
|
135
|
+
* The collision case is vanishingly rare (~10–40 memories per injection), but
|
|
136
|
+
* widening guarantees a user can always point at exactly one memory in-band.
|
|
137
|
+
*/
|
|
138
|
+
export function renderHandlesForInjection(
|
|
139
|
+
memoryIds: readonly string[],
|
|
140
|
+
): InjectionHandleEntry[] {
|
|
141
|
+
// Group ids by their default-width handle so every member of a 4-char
|
|
142
|
+
// collision group widens to 6 together — not just the later one. Widening
|
|
143
|
+
// only the second id would leave the first rendering a 4-char token whose
|
|
144
|
+
// resolution is ambiguous against the group (codex review).
|
|
145
|
+
const idsByDefaultHandle = new Map<string, string[]>();
|
|
146
|
+
for (const memoryId of memoryIds) {
|
|
147
|
+
if (!memoryId) continue;
|
|
148
|
+
const defaultHandle = handleFor(memoryId, HANDLE_DEFAULT_WIDTH);
|
|
149
|
+
const group = idsByDefaultHandle.get(defaultHandle);
|
|
150
|
+
if (group) group.push(memoryId);
|
|
151
|
+
else idsByDefaultHandle.set(defaultHandle, [memoryId]);
|
|
152
|
+
}
|
|
153
|
+
const entries: InjectionHandleEntry[] = [];
|
|
154
|
+
for (const memoryId of memoryIds) {
|
|
155
|
+
if (!memoryId) continue;
|
|
156
|
+
const defaultHandle = handleFor(memoryId, HANDLE_DEFAULT_WIDTH);
|
|
157
|
+
const group = idsByDefaultHandle.get(defaultHandle) ?? [memoryId];
|
|
158
|
+
const width = group.length > 1 ? HANDLE_EXTENDED_WIDTH : HANDLE_DEFAULT_WIDTH;
|
|
159
|
+
entries.push({ memoryId, width, handle: `[m:${handleFor(memoryId, width)}]` });
|
|
160
|
+
}
|
|
161
|
+
// Reconciliation pass: if two DIFFERENT ids still produce the same rendered
|
|
162
|
+
// token at their assigned width — the pathological case where they also
|
|
163
|
+
// collide at the extended 6-char width — widen ALL members of the collision
|
|
164
|
+
// group together, never just the later one. Leaving the first at 6 chars
|
|
165
|
+
// while widening the second to 7 makes the first token resolve ambiguously
|
|
166
|
+
// (both ids match at width 6). Iterate until no collision remains or every
|
|
167
|
+
// member reaches HANDLE_MAX_WIDTH (codex review).
|
|
168
|
+
let widened = true;
|
|
169
|
+
while (widened) {
|
|
170
|
+
widened = false;
|
|
171
|
+
const tokenToIndices = new Map<string, number[]>();
|
|
172
|
+
for (let i = 0; i < entries.length; i++) {
|
|
173
|
+
const token = entries[i]!.handle;
|
|
174
|
+
const arr = tokenToIndices.get(token);
|
|
175
|
+
if (arr) arr.push(i);
|
|
176
|
+
else tokenToIndices.set(token, [i]);
|
|
177
|
+
}
|
|
178
|
+
for (const indices of tokenToIndices.values()) {
|
|
179
|
+
if (indices.length < 2) continue;
|
|
180
|
+
const uniqueIds = new Set(indices.map((idx) => entries[idx]!.memoryId));
|
|
181
|
+
if (uniqueIds.size < 2) continue; // same id listed twice — same handle is fine
|
|
182
|
+
// Only continue widening if at least one entry has room to grow.
|
|
183
|
+
// When every member has reached HANDLE_MAX_WIDTH the collision is
|
|
184
|
+
// irreducible (two ids with identical sha256 8-char prefixes —
|
|
185
|
+
// astronomically unlikely for real data); stop to avoid an infinite loop.
|
|
186
|
+
let anyWidened = false;
|
|
187
|
+
for (const idx of indices) {
|
|
188
|
+
const entry = entries[idx]!;
|
|
189
|
+
if (entry.width >= HANDLE_MAX_WIDTH) continue;
|
|
190
|
+
entry.width += 1;
|
|
191
|
+
entry.handle = `[m:${handleFor(entry.memoryId, entry.width)}]`;
|
|
192
|
+
anyWidened = true;
|
|
193
|
+
}
|
|
194
|
+
if (anyWidened) widened = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return entries;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build a `resultIndex → handle` map for a QMD result set. Returns an empty
|
|
202
|
+
* map when handles are disabled or no result carries a handle-eligible memory
|
|
203
|
+
* id. Extracted from the orchestrator's `formatQmdResults` so the rendering
|
|
204
|
+
* logic lives in the pure handles module, not the god file (issue #1582,
|
|
205
|
+
* rule 4).
|
|
206
|
+
*
|
|
207
|
+
* Only real memory ids (matching {@link MEMORY_ID_PATTERN}) are handle-eligible;
|
|
208
|
+
* entity reconstructions and other non-memory `.md` rows get no handle.
|
|
209
|
+
*
|
|
210
|
+
* @param results QMD search results (each has a `path` like `…/fact-x.md`).
|
|
211
|
+
* @param enabled Whether handle rendering is on. The caller gates on both the
|
|
212
|
+
* config flag and session-key availability — handles rendered
|
|
213
|
+
* without a session key can never be resolved, so they must
|
|
214
|
+
* not be shown (cursor review of #1582).
|
|
215
|
+
*/
|
|
216
|
+
export function buildHandleIndexForResults(
|
|
217
|
+
results: readonly { path: string }[],
|
|
218
|
+
enabled: boolean,
|
|
219
|
+
): Map<number, string> {
|
|
220
|
+
if (!enabled) return new Map();
|
|
221
|
+
const ids: string[] = [];
|
|
222
|
+
const idIndexByResult: Array<number | null> = results.map((r) => {
|
|
223
|
+
const match = r.path.match(/([^/]+)\.md$/);
|
|
224
|
+
if (!match) return null;
|
|
225
|
+
const candidate = match[1] as string;
|
|
226
|
+
if (!MEMORY_ID_PATTERN.test(candidate)) return null;
|
|
227
|
+
ids.push(candidate);
|
|
228
|
+
return ids.length - 1;
|
|
229
|
+
});
|
|
230
|
+
const entries = renderHandlesForInjection(ids);
|
|
231
|
+
const handleByIndex = new Map<number, string>();
|
|
232
|
+
results.forEach((_r, i) => {
|
|
233
|
+
const idIndex = idIndexByResult[i];
|
|
234
|
+
if (idIndex === null) return;
|
|
235
|
+
const entry = entries[idIndex];
|
|
236
|
+
if (entry) handleByIndex.set(i, entry.handle);
|
|
237
|
+
});
|
|
238
|
+
return handleByIndex;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extract every handle token from prose, in order of appearance. Returns the
|
|
243
|
+
* full rendered tokens (e.g. `[m:4f2a]`) so callers can locate them in text.
|
|
244
|
+
* Malformed tokens are ignored. Duplicates are preserved (a user may cite the
|
|
245
|
+
* same memory twice).
|
|
246
|
+
*
|
|
247
|
+
* parseHandles("see [m:4f2a] (also [m:1b9e]) — not [m:xyz!]")
|
|
248
|
+
* // → ["[m:4f2a]", "[m:1b9e]"]
|
|
249
|
+
*/
|
|
250
|
+
export function parseHandles(text: string): string[] {
|
|
251
|
+
if (typeof text !== "string" || text.length === 0) return [];
|
|
252
|
+
const out: string[] = [];
|
|
253
|
+
// Reset lastIndex because HANDLE_REGEX is /g and may be reused.
|
|
254
|
+
HANDLE_REGEX.lastIndex = 0;
|
|
255
|
+
let match: RegExpExecArray | null;
|
|
256
|
+
while ((match = HANDLE_REGEX.exec(text)) !== null) {
|
|
257
|
+
out.push(match[0]);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Strip every handle token from text, collapsing the spacing it introduced.
|
|
264
|
+
* Used by the sanitizer so handles observed back into the pipeline never
|
|
265
|
+
* become memory content (issue #1582 hygiene §2).
|
|
266
|
+
*
|
|
267
|
+
* stripHandles("API limit 1000 rpm. [m:4f2a]") → "API limit 1000 rpm."
|
|
268
|
+
*/
|
|
269
|
+
export function stripHandles(text: string): string {
|
|
270
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
271
|
+
// Remove the token and the single preceding space that {@link appendHandle}
|
|
272
|
+
// added, then tidy any double space left behind.
|
|
273
|
+
// \\s? (not \\s*): the renderer appends exactly one preceding space, and a
|
|
274
|
+
// bounded quantifier avoids the polynomial-ReDoS flag on uncontrolled input.
|
|
275
|
+
// A stray run of spaces is collapsed by the following line.
|
|
276
|
+
return text
|
|
277
|
+
.replace(/\s?\[m:[0-9a-f]{4,8}\]/g, "")
|
|
278
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
279
|
+
.replace(/[ \t]+$/g, "");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Normalize a handle token (or bare hex) to its lowercase hex core, or `null`
|
|
284
|
+
* when the input is not a handle. Accepts both the rendered `[m:4f2a]` form
|
|
285
|
+
* and the bare `m:4f2a` / `4f2a` form a user might type.
|
|
286
|
+
*
|
|
287
|
+
* normalizeHandle("[m:4f2a]") → "4f2a"
|
|
288
|
+
* normalizeHandle("m:4f2a") → "4f2a"
|
|
289
|
+
* normalizeHandle("4f2a") → "4f2a"
|
|
290
|
+
* normalizeHandle("fact-1") → null
|
|
291
|
+
*/
|
|
292
|
+
export function normalizeHandle(token: string): string | null {
|
|
293
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
294
|
+
const trimmed = token.trim().toLowerCase();
|
|
295
|
+
// [m:4f2a]
|
|
296
|
+
const bracketed = /^\[m:([0-9a-f]{4,8})\]$/.exec(trimmed);
|
|
297
|
+
if (bracketed) return bracketed[1] ?? null;
|
|
298
|
+
// m:4f2a
|
|
299
|
+
const prefixed = /^m:([0-9a-f]{4,8})$/.exec(trimmed);
|
|
300
|
+
if (prefixed) return prefixed[1] ?? null;
|
|
301
|
+
// bare 4f2a — only treat as a handle when it is pure hex in the width range,
|
|
302
|
+
// so we don't mis-classify a real memory id that happens to be 4 hex chars.
|
|
303
|
+
const bare = /^([0-9a-f]{4,8})$/.exec(trimmed);
|
|
304
|
+
if (bare) return bare[1] ?? null;
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Whether a string looks like a handle token (rendered or bare). Cheaper
|
|
310
|
+
* boolean form of {@link normalizeHandle} for callers that only need to branch.
|
|
311
|
+
*/
|
|
312
|
+
export function isHandleToken(token: string): boolean {
|
|
313
|
+
return normalizeHandle(token) !== null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* A flattened id-or-handle reference: either a raw memory id or the hex core
|
|
318
|
+
* of a handle. {@link resolveMemoryIdOrHandle} produces this so the snapshot
|
|
319
|
+
* lookup only runs for actual handles.
|
|
320
|
+
*/
|
|
321
|
+
export interface ParsedIdOrHandle {
|
|
322
|
+
/** The original reference as supplied by the caller. */
|
|
323
|
+
raw: string;
|
|
324
|
+
/** `true` when `raw` is a handle that still needs snapshot resolution. */
|
|
325
|
+
isHandle: boolean;
|
|
326
|
+
/** For a handle, its hex core; for a raw id, the id itself. */
|
|
327
|
+
value: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Classify a single caller reference as id-or-handle.
|
|
332
|
+
*/
|
|
333
|
+
export function parseIdOrHandle(ref: string): ParsedIdOrHandle {
|
|
334
|
+
const hex = normalizeHandle(ref);
|
|
335
|
+
if (hex !== null) {
|
|
336
|
+
return { raw: ref, isHandle: true, value: hex };
|
|
337
|
+
}
|
|
338
|
+
return { raw: ref, isHandle: false, value: ref };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A flattened view of the memory-id sets a session has recently recalled.
|
|
343
|
+
* Each entry is one past recall's admitted memory ids (newest first).
|
|
344
|
+
* Resolution never sees raw {@link LastRecallSnapshot}s — callers flatten to
|
|
345
|
+
* this so the pure resolver has no dependency on the snapshot type.
|
|
346
|
+
*/
|
|
347
|
+
export interface RecallSnapshotIds {
|
|
348
|
+
memoryIds: readonly string[];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Result of resolving a handle against recent recall snapshots.
|
|
353
|
+
* - `{ ok: true, memoryId }` — exactly one match within the lookback window.
|
|
354
|
+
* - `{ ok: false, reason: "not_found" }` — no memory id produced this handle
|
|
355
|
+
* within the window (misses are acceptable and tagged, never guessed).
|
|
356
|
+
* - `{ ok: false, reason: "ambiguous", candidates }` — two DIFFERENT memory
|
|
357
|
+
* ids in the window collide on the same handle; the caller must disambiguate
|
|
358
|
+
* (rule 34/51: never guess).
|
|
359
|
+
*/
|
|
360
|
+
export type ResolveHandleResult =
|
|
361
|
+
| { ok: true; memoryId: string }
|
|
362
|
+
| { ok: false; reason: "not_found" }
|
|
363
|
+
| { ok: false; reason: "ambiguous"; candidates: string[] };
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Resolve a handle hex (or a rendered token / bare form) to its memory id
|
|
367
|
+
* against the session's recent recall snapshots.
|
|
368
|
+
*
|
|
369
|
+
* @param handle A handle token, bare hex, or `m:hex` form.
|
|
370
|
+
* @param snapshots Recent recall snapshots for the session, newest first.
|
|
371
|
+
* Only the first `depth` entries are searched.
|
|
372
|
+
* @param depth How many snapshots (newest-first) to search. Defaults to
|
|
373
|
+
* {@link DEFAULT_HANDLE_SNAPSHOT_DEPTH}.
|
|
374
|
+
*/
|
|
375
|
+
export function resolveHandle(
|
|
376
|
+
handle: string,
|
|
377
|
+
snapshots: readonly RecallSnapshotIds[],
|
|
378
|
+
depth: number = DEFAULT_HANDLE_SNAPSHOT_DEPTH,
|
|
379
|
+
): ResolveHandleResult {
|
|
380
|
+
const hex = normalizeHandle(handle);
|
|
381
|
+
if (hex === null) {
|
|
382
|
+
return { ok: false, reason: "not_found" };
|
|
383
|
+
}
|
|
384
|
+
// A handle resolves when a memory id's handle is a PREFIX of `hex` (handles
|
|
385
|
+
// widened to 6 chars for collision still match their own 4-char core, and a
|
|
386
|
+
// user citing the short form must still resolve the widened memory).
|
|
387
|
+
const width = hex.length;
|
|
388
|
+
const limit = Math.max(0, Math.min(depth, snapshots.length));
|
|
389
|
+
const matches = new Set<string>();
|
|
390
|
+
for (let i = 0; i < limit; i += 1) {
|
|
391
|
+
const snap = snapshots[i];
|
|
392
|
+
if (!snap) continue;
|
|
393
|
+
for (const memoryId of snap.memoryIds) {
|
|
394
|
+
if (!memoryId) continue;
|
|
395
|
+
if (handleFor(memoryId, width) === hex) {
|
|
396
|
+
matches.add(memoryId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (matches.size === 0) {
|
|
401
|
+
return { ok: false, reason: "not_found" };
|
|
402
|
+
}
|
|
403
|
+
if (matches.size === 1) {
|
|
404
|
+
return { ok: true, memoryId: matches.values().next().value as string };
|
|
405
|
+
}
|
|
406
|
+
return { ok: false, reason: "ambiguous", candidates: [...matches] };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Default snapshot lookback depth for handle resolution (issue #1582 config
|
|
411
|
+
* `recall.handleSnapshotDepth`). Older-than-N snapshots are not searched and a
|
|
412
|
+
* miss is tagged rather than widening the window.
|
|
413
|
+
*/
|
|
414
|
+
export const DEFAULT_HANDLE_SNAPSHOT_DEPTH = 5;
|