@remnic/core 9.4.2 → 9.6.0
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-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +28 -28
- package/dist/access-boundary.d.ts +27 -8
- package/dist/access-boundary.js +33 -29
- package/dist/access-cli.js +83 -81
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +52 -6
- package/dist/access-http.js +40 -38
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +28 -28
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +28 -28
- package/dist/access-mcp.d.ts +16 -6
- package/dist/access-mcp.js +37 -35
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +28 -28
- package/dist/access-operations-batch.js +33 -32
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +28 -28
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +174 -0
- package/dist/access-token-capabilities.js +33 -0
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +9 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
- package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
- package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
- package/dist/chunk-4GNEDXDI.js.map +1 -0
- package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
- package/dist/chunk-4OKLXWDZ.js.map +1 -0
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
- package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
- package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
- package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
- package/dist/chunk-B3ABVQ36.js.map +1 -0
- package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
- package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/chunk-D7FOKIY5.js +173 -0
- package/dist/chunk-D7FOKIY5.js.map +1 -0
- package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
- package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
- package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
- package/dist/chunk-IC2TCZJ2.js +54 -0
- package/dist/chunk-IC2TCZJ2.js.map +1 -0
- package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
- package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
- package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
- package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
- package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
- package/dist/chunk-LADFWYHY.js.map +1 -0
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
- package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
- package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
- package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
- package/dist/chunk-PCLIWBGD.js.map +1 -0
- package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
- package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
- package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
- package/dist/chunk-UFUKHGZD.js.map +1 -0
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
- package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
- package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
- package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +61 -59
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +9 -9
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- 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/dashboard-runtime.js +2 -2
- 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 +2 -2
- package/dist/entity-retrieval.js +9 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- 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/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +271 -17
- package/dist/index.js +163 -99
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/mcp-read-only-tools.d.ts +16 -0
- package/dist/mcp-read-only-tools.js +8 -0
- package/dist/mcp-read-only-tools.js.map +1 -0
- 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 +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +19 -19
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +25 -25
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +85 -83
- 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-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/document-scanner.js +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -3
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +9 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- 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-C1maCNbc.d.ts +1591 -0
- package/dist/storage.d.ts +5 -1239
- package/dist/storage.js +8 -8
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
- package/dist/tokens.d.ts +17 -2
- package/dist/tokens.js +3 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-boundary.test.ts +62 -0
- package/src/access-boundary.ts +165 -126
- package/src/access-http.test.ts +781 -0
- package/src/access-http.ts +334 -108
- package/src/access-mcp.test.ts +264 -0
- package/src/access-mcp.ts +45 -70
- package/src/access-operations-batch.ts +35 -12
- package/src/access-surface-catalog.test.ts +22 -14
- package/src/access-surface-catalog.ts +6 -3
- package/src/access-token-capabilities.test.ts +844 -0
- package/src/access-token-capabilities.ts +391 -0
- package/src/chat/chat-mcp.test.ts +232 -0
- package/src/chat/chat-session.ts +34 -0
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +14 -0
- package/src/mcp-read-only-tools.ts +61 -0
- package/src/storage.ts +16 -0
- package/src/tokens.ts +32 -3
- package/src/wearables/cli.test.ts +102 -0
- package/src/wearables/cli.ts +65 -0
- package/src/wearables/config.test.ts +32 -0
- package/src/wearables/config.ts +52 -0
- package/src/wearables/day-store.test.ts +246 -0
- package/src/wearables/day-store.ts +272 -3
- package/src/wearables/fusion/cluster.ts +230 -0
- package/src/wearables/fusion/fuse.ts +218 -0
- package/src/wearables/fusion/fusion.test.ts +2575 -0
- package/src/wearables/fusion/index.ts +50 -0
- package/src/wearables/fusion/reconcile.ts +831 -0
- package/src/wearables/fusion/reconstruct.ts +251 -0
- package/src/wearables/fusion/store.ts +480 -0
- package/src/wearables/fusion/types.ts +228 -0
- package/src/wearables/index.ts +41 -0
- package/src/wearables/service.test.ts +1030 -2
- package/src/wearables/service.ts +258 -16
- package/src/wearables/speakers.test.ts +128 -0
- package/src/wearables/speakers.ts +58 -4
- package/src/wearables/storage-io.test.ts +146 -0
- package/src/wearables/types.ts +31 -0
- package/dist/chunk-2FOD2YY3.js.map +0 -1
- package/dist/chunk-7QWMTOIF.js.map +0 -1
- package/dist/chunk-DR6PEJU3.js.map +0 -1
- package/dist/chunk-E2SPGGUI.js.map +0 -1
- package/dist/chunk-FZGGZAAU.js.map +0 -1
- package/dist/chunk-ILFJ7AVS.js.map +0 -1
- package/dist/chunk-M7GGMGMZ.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
- /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
- /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
- /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
- /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
- /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
- /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
package/src/wearables/service.ts
CHANGED
|
@@ -13,8 +13,25 @@ import {
|
|
|
13
13
|
} from "./corrections.js";
|
|
14
14
|
import { describeErrorForOperator, WearablesInputError } from "./errors.js";
|
|
15
15
|
import { inferMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
bodyIsEscaped,
|
|
18
|
+
decodeTranscriptBody,
|
|
19
|
+
escapeSegmentText,
|
|
20
|
+
isValidTranscriptDate,
|
|
21
|
+
parseDayTranscript,
|
|
22
|
+
} from "./day-store.js";
|
|
23
|
+
import {
|
|
24
|
+
composeFusionDayMeta,
|
|
25
|
+
type FusionArtifactStore,
|
|
26
|
+
fuseDay as fuseDayInputs,
|
|
27
|
+
hashFusionBody,
|
|
28
|
+
parseFusionDay,
|
|
29
|
+
reconstructFusionInputs,
|
|
30
|
+
serializeFusionDay,
|
|
31
|
+
type FusedWearableConversation,
|
|
32
|
+
} from "./fusion/index.js";
|
|
17
33
|
import { stripAttributesSuffix } from "../storage.js";
|
|
34
|
+
import { log } from "../logger.js";
|
|
18
35
|
import type { MemoryFrontmatter } from "../types.js";
|
|
19
36
|
import type { WearableMemoryGenDeps } from "./memory-gen.js";
|
|
20
37
|
import { WEARABLE_SOURCE_PREFIX, wearableSourceLabel } from "./memory-gen.js";
|
|
@@ -59,6 +76,7 @@ export interface WearableStorageIo {
|
|
|
59
76
|
listWearableTranscriptDays(
|
|
60
77
|
sourceId?: string,
|
|
61
78
|
): Promise<Array<{ source: string; date: string }>>;
|
|
79
|
+
fusionArtifactStore(): FusionArtifactStore;
|
|
62
80
|
readAllMemories(): Promise<
|
|
63
81
|
Array<{
|
|
64
82
|
path: string;
|
|
@@ -443,7 +461,13 @@ export class WearablesService {
|
|
|
443
461
|
source,
|
|
444
462
|
date,
|
|
445
463
|
meta: parsed?.meta ?? null,
|
|
446
|
-
|
|
464
|
+
// Decode escaped segment text for display: the stored body is an
|
|
465
|
+
// internal line-based serialization (newlines/backslashes escaped
|
|
466
|
+
// so segment text survives the serialize -> reconstruct round
|
|
467
|
+
// trip); user-facing view surfaces must show the original text.
|
|
468
|
+
// The fusion reconstruct path reads raw bodies separately and
|
|
469
|
+
// decodes once during parse, so this never double-decodes (#1849).
|
|
470
|
+
body: decodeTranscriptBody(parsed?.body ?? raw, bodyIsEscaped(parsed?.meta)),
|
|
447
471
|
overlapsWith: [],
|
|
448
472
|
});
|
|
449
473
|
}
|
|
@@ -464,6 +488,192 @@ export class WearablesService {
|
|
|
464
488
|
return storage.listWearableTranscriptDays(sourceId);
|
|
465
489
|
}
|
|
466
490
|
|
|
491
|
+
// -- cross-source fusion (#1810) -----------------------------------------
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Fuse all enabled sources' stored transcripts for one day into a
|
|
495
|
+
* derived `FusedWearableConversation[]` artifact. Deterministic and
|
|
496
|
+
* idempotent: unchanged inputs and fusion config produce a stable
|
|
497
|
+
* content hash and the derived file is left untouched on re-run. Requires
|
|
498
|
+
* `wearables.fusion.enabled`; otherwise it throws. Raw per-source
|
|
499
|
+
* transcripts are never modified.
|
|
500
|
+
*/
|
|
501
|
+
async fuseDay(date: string): Promise<{
|
|
502
|
+
date: string;
|
|
503
|
+
sources: string[];
|
|
504
|
+
conversationCount: number;
|
|
505
|
+
contentHash: string;
|
|
506
|
+
written: boolean;
|
|
507
|
+
skipped?: { reason: string };
|
|
508
|
+
}> {
|
|
509
|
+
this.assertEnabled();
|
|
510
|
+
if (!this.deps.config.fusion.enabled) {
|
|
511
|
+
throw new WearablesInputError(
|
|
512
|
+
"wearables fusion is not enabled — set `wearables.fusion.enabled: true` in the plugin config",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (!isValidTranscriptDate(date)) {
|
|
516
|
+
throw new WearablesInputError(`invalid date '${date}' — expected YYYY-MM-DD`);
|
|
517
|
+
}
|
|
518
|
+
const storage = await this.deps.getStorage();
|
|
519
|
+
const enabled = this.enabledSources();
|
|
520
|
+
const bodies: Array<{ source: string; body: string; escaped?: boolean }> = [];
|
|
521
|
+
const sourceTimezones: string[] = [];
|
|
522
|
+
for (const [source] of enabled) {
|
|
523
|
+
const raw = await storage.readWearableDayTranscript(source, date);
|
|
524
|
+
if (raw === null) continue;
|
|
525
|
+
const parsed = parseDayTranscript(raw);
|
|
526
|
+
bodies.push({
|
|
527
|
+
source,
|
|
528
|
+
body: parsed?.body ?? raw,
|
|
529
|
+
escaped: bodyIsEscaped(parsed?.meta),
|
|
530
|
+
});
|
|
531
|
+
sourceTimezones.push(parsed?.meta.timezone ?? "");
|
|
532
|
+
}
|
|
533
|
+
// Reconstruct the normalized inputs once and derive the sources that
|
|
534
|
+
// actually CONTRIBUTE conversations/clocks for the day. The sync path
|
|
535
|
+
// can persist an explicit EMPTY (all-elided / zero-conversation)
|
|
536
|
+
// transcript for days whose segments were all dropped; such a file
|
|
537
|
+
// contributes no clocks, so it must not influence the timezone guard
|
|
538
|
+
// below.
|
|
539
|
+
const reconstructed = reconstructFusionInputs(date, bodies);
|
|
540
|
+
const contributing = bodies
|
|
541
|
+
.map((entry, i) => ({
|
|
542
|
+
source: entry.source,
|
|
543
|
+
timezone: sourceTimezones[i] ?? "",
|
|
544
|
+
}))
|
|
545
|
+
.filter((entry) =>
|
|
546
|
+
reconstructed.some((input) => input.source === entry.source),
|
|
547
|
+
);
|
|
548
|
+
// Mixed-timezone guard: reconstructFusionInputs rebuilds every clock as
|
|
549
|
+
// `${date}T${HH}:${MM}:00Z` and compares local HH:MM clocks across sources
|
|
550
|
+
// as if they shared one timezone. That comparison is ONLY valid when every
|
|
551
|
+
// CONTRIBUTING source (one that reconstructs to >=1 conversation) carries
|
|
552
|
+
// the SAME explicit IANA timezone id (meta.timezone). Sources that
|
|
553
|
+
// reconstruct to zero conversations contribute no clocks and are ignored,
|
|
554
|
+
// so an empty/all-elided transcript rendered under a different (or
|
|
555
|
+
// missing) timezone cannot block a genuine same-zone fusion. Sampling one
|
|
556
|
+
// UTC offset per source is insufficient: on DST-transition dates two
|
|
557
|
+
// different zones can share a noon offset yet differ during recorded hours
|
|
558
|
+
// (e.g. America/Los_Angeles vs America/Phoenix on 2026-03-08 — both UTC-7
|
|
559
|
+
// by noon, an hour apart before LA's spring-forward). A source whose
|
|
560
|
+
// timezone id is missing/empty is treated as unresolvable rather than
|
|
561
|
+
// silently coerced to a default, so any unknown tz fails safe. Require
|
|
562
|
+
// exact tz-id identity across all contributing sources.
|
|
563
|
+
if (contributing.length > 1) {
|
|
564
|
+
const referenceTz = contributing[0]!.timezone;
|
|
565
|
+
const allSameExplicitTz =
|
|
566
|
+
referenceTz.trim().length > 0 &&
|
|
567
|
+
contributing.every((entry) => entry.timezone === referenceTz);
|
|
568
|
+
if (!allSameExplicitTz) {
|
|
569
|
+
const detail = contributing
|
|
570
|
+
.map((entry) => `${entry.source}=${entry.timezone || "?"}`)
|
|
571
|
+
.join(", ");
|
|
572
|
+
log.warn(
|
|
573
|
+
`wearables fusion: skipping ${date} — sources were rendered under differing timezones (${detail}); reconstructFusionInputs only compares local clocks correctly when every source shares one explicit IANA timezone id`,
|
|
574
|
+
);
|
|
575
|
+
// Clear any previously-fused artifact for this day: a day that
|
|
576
|
+
// fused successfully before must not keep serving a stale view
|
|
577
|
+
// now that this run explicitly refuses to fuse (issue #1849).
|
|
578
|
+
await storage.fusionArtifactStore().deleteFusedDay(date);
|
|
579
|
+
return {
|
|
580
|
+
date,
|
|
581
|
+
sources: [],
|
|
582
|
+
conversationCount: 0,
|
|
583
|
+
contentHash: "",
|
|
584
|
+
written: false,
|
|
585
|
+
skipped: { reason: "conflicting-timezones" },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const sourceTrust: Record<string, number> = {};
|
|
590
|
+
for (const [id, settings] of enabled) {
|
|
591
|
+
sourceTrust[id] = settings.sourceTrust;
|
|
592
|
+
}
|
|
593
|
+
const result = fuseDayInputs(date, reconstructed, {
|
|
594
|
+
proximityGapMs: this.deps.config.fusion.proximityGapMs,
|
|
595
|
+
windowToleranceMs: this.deps.config.fusion.windowToleranceMs,
|
|
596
|
+
sourceTrust,
|
|
597
|
+
});
|
|
598
|
+
// Idempotent skip-unchanged: do not rewrite when the input content
|
|
599
|
+
// hash matches the stored artifact, the stored body parsed cleanly
|
|
600
|
+
// into a well-formed conversation set, AND the stored body hash still
|
|
601
|
+
// matches a recompute over the stored body itself. A
|
|
602
|
+
// truncated/corrupt/malformed-element body fails to parse
|
|
603
|
+
// (parseOk:false) even when the frontmatter hash matches, and a body
|
|
604
|
+
// whose bytes drifted fails the body-hash recompute even when the
|
|
605
|
+
// input hash + conversation count still match — so either forces a
|
|
606
|
+
// self-repair rewrite instead of trusting the hashes alone.
|
|
607
|
+
const existingRaw = await storage.fusionArtifactStore().readFusedDay(date);
|
|
608
|
+
const existing = parseFusionDay(existingRaw ?? "");
|
|
609
|
+
// parseOk is bound to a local so the skip condition can keep the
|
|
610
|
+
// explicit `existing !== null` guard for the `.meta` access below —
|
|
611
|
+
// an inline `existing.parseOk` member check there trips
|
|
612
|
+
// useOptionalChain (whose suggested fix would drop the guard and
|
|
613
|
+
// null-deref the later access), so we read it via optional chain.
|
|
614
|
+
const parsedCleanly = existing?.parseOk === true;
|
|
615
|
+
const skipUnchanged =
|
|
616
|
+
existing !== null &&
|
|
617
|
+
parsedCleanly &&
|
|
618
|
+
existing.meta.contentHash === result.contentHash &&
|
|
619
|
+
existing.meta.bodyHash === hashFusionBody(existing.conversations);
|
|
620
|
+
const written = !skipUnchanged;
|
|
621
|
+
if (written) {
|
|
622
|
+
const meta = composeFusionDayMeta(
|
|
623
|
+
date,
|
|
624
|
+
result.conversations,
|
|
625
|
+
result.sources,
|
|
626
|
+
result.contentHash,
|
|
627
|
+
new Date().toISOString(),
|
|
628
|
+
);
|
|
629
|
+
await storage.fusionArtifactStore().writeFusedDay(
|
|
630
|
+
date,
|
|
631
|
+
serializeFusionDay(meta, result.conversations),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
date: result.date,
|
|
636
|
+
sources: result.sources,
|
|
637
|
+
conversationCount: result.conversations.length,
|
|
638
|
+
contentHash: result.contentHash,
|
|
639
|
+
written,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* List fused conversations for a date (the fusion listing surface).
|
|
645
|
+
* Returns the persisted derived artifact's conversations, or an empty
|
|
646
|
+
* array when no fused artifact has been written for that day. When a
|
|
647
|
+
* file EXISTS but its body is corrupt (parseOk:false), throws
|
|
648
|
+
* WearablesInputError so the caller can distinguish a broken artifact
|
|
649
|
+
* from a day that was never fused — never silently returns an empty
|
|
650
|
+
* list that looks identical to "no artifact" (issue #1849).
|
|
651
|
+
*/
|
|
652
|
+
async fusedConversations(
|
|
653
|
+
date: string,
|
|
654
|
+
): Promise<FusedWearableConversation[]> {
|
|
655
|
+
if (!isValidTranscriptDate(date)) {
|
|
656
|
+
throw new WearablesInputError(`invalid date '${date}' — expected YYYY-MM-DD`);
|
|
657
|
+
}
|
|
658
|
+
const storage = await this.deps.getStorage();
|
|
659
|
+
const raw = await storage.fusionArtifactStore().readFusedDay(date);
|
|
660
|
+
if (raw === null) return [];
|
|
661
|
+
const parsed = parseFusionDay(raw);
|
|
662
|
+
if (parsed === null) return [];
|
|
663
|
+
if (!parsed.parseOk) {
|
|
664
|
+
throw new WearablesInputError(
|
|
665
|
+
`fused artifact for ${date} is corrupt — re-run \`wearables fuse ${date}\` to repair`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return parsed.conversations;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** List dates with stored fused artifacts, newest first. */
|
|
672
|
+
async listFusedDays(): Promise<string[]> {
|
|
673
|
+
const storage = await this.deps.getStorage();
|
|
674
|
+
return storage.fusionArtifactStore().listFusedDays();
|
|
675
|
+
}
|
|
676
|
+
|
|
467
677
|
/**
|
|
468
678
|
* Search stored transcripts. Uses the indexed backend when available
|
|
469
679
|
* and falls back to a bounded substring scan otherwise — the two
|
|
@@ -509,31 +719,54 @@ export class WearablesService {
|
|
|
509
719
|
};
|
|
510
720
|
|
|
511
721
|
if (this.deps.searchBackend) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
722
|
+
// The index stores escaped segment text (real newlines become
|
|
723
|
+
// the two characters \n, lone backslashes are doubled). A query
|
|
724
|
+
// containing those characters in their ORIGINAL decoded form will
|
|
725
|
+
// not match the indexed representation, so we ALSO search the
|
|
726
|
+
// escaped form. This keeps the indexed path at parity with the
|
|
727
|
+
// scan fallback, which decodes the body before searching (#1849).
|
|
728
|
+
const escaped = escapeSegmentText(trimmed);
|
|
729
|
+
const queries =
|
|
730
|
+
escaped === trimmed ? [trimmed] : [trimmed, escaped];
|
|
731
|
+
const seen = new Set<string>();
|
|
732
|
+
const results: WearableTranscriptSearchResult[] = [];
|
|
733
|
+
const idxStorage = await this.deps.getStorage();
|
|
734
|
+
for (const q of queries) {
|
|
735
|
+
const hits = await this.deps.searchBackend.search(q, limit * 5);
|
|
736
|
+
if (hits === null) break; // backend unavailable
|
|
515
737
|
for (const hit of hits) {
|
|
516
738
|
const located = locateTranscriptPath(hit.path);
|
|
517
739
|
if (!located) continue;
|
|
518
740
|
if (!matchesScope(located.source, located.date)) continue;
|
|
741
|
+
const dedupeKey = `${located.source}:${located.date}`;
|
|
742
|
+
if (seen.has(dedupeKey)) continue;
|
|
743
|
+
seen.add(dedupeKey);
|
|
744
|
+
// The indexed snippet mirrors the stored body's escape encoding.
|
|
745
|
+
// Read the file once to learn whether it was written by the
|
|
746
|
+
// escape-aware serializer so a LEGACY file's literal two-character
|
|
747
|
+
// \n/\r is never decoded (#1849).
|
|
748
|
+
const idxRaw =
|
|
749
|
+
await idxStorage.readWearableDayTranscript(located.source, located.date);
|
|
750
|
+
const idxMeta = parseDayTranscript(idxRaw ?? "")?.meta ?? null;
|
|
519
751
|
results.push({
|
|
520
752
|
source: located.source,
|
|
521
753
|
date: located.date,
|
|
522
754
|
score: hit.score,
|
|
523
|
-
snippet: hit.preview,
|
|
755
|
+
snippet: decodeTranscriptBody(hit.preview, bodyIsEscaped(idxMeta)),
|
|
524
756
|
backend: "indexed",
|
|
525
757
|
});
|
|
526
758
|
if (results.length >= limit) break;
|
|
527
759
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
760
|
+
if (results.length >= limit) break;
|
|
761
|
+
}
|
|
762
|
+
// The index spans the whole memory dir, so ordinary memory
|
|
763
|
+
// files can crowd transcripts out of the top hits entirely.
|
|
764
|
+
// Zero in-scope hits therefore doesn't mean "no transcript
|
|
765
|
+
// matches" — fall through to the bounded scan in that case
|
|
766
|
+
// (Codex P2 on PR #1458). Partial result sets stay indexed-only
|
|
767
|
+
// so the two backends never interleave in one response.
|
|
768
|
+
if (results.length > 0) {
|
|
769
|
+
return results;
|
|
537
770
|
}
|
|
538
771
|
}
|
|
539
772
|
|
|
@@ -546,7 +779,16 @@ export class WearablesService {
|
|
|
546
779
|
if (!matchesScope(source, date)) continue;
|
|
547
780
|
const raw = await storage.readWearableDayTranscript(source, date);
|
|
548
781
|
if (raw === null) continue;
|
|
549
|
-
const
|
|
782
|
+
const scanParsed = parseDayTranscript(raw);
|
|
783
|
+
// Decode escaped segment text so searches match AND snippets show
|
|
784
|
+
// the ORIGINAL text (e.g. a real newline or backslash), not the
|
|
785
|
+
// internal escape serialization — but ONLY for bodies written by
|
|
786
|
+
// the escape-aware serializer; legacy bodies are searched verbatim
|
|
787
|
+
// (#1849).
|
|
788
|
+
const body = decodeTranscriptBody(
|
|
789
|
+
scanParsed?.body ?? raw,
|
|
790
|
+
bodyIsEscaped(scanParsed?.meta),
|
|
791
|
+
);
|
|
550
792
|
const lower = body.toLowerCase();
|
|
551
793
|
const index = lower.indexOf(needle);
|
|
552
794
|
if (index === -1) continue;
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
DEFAULT_SELF_NAME,
|
|
9
9
|
distinctSpeakerLabels,
|
|
10
10
|
emptySpeakerRegistry,
|
|
11
|
+
hasSelfMarker,
|
|
11
12
|
loadSpeakerRegistry,
|
|
12
13
|
resolveSpeaker,
|
|
13
14
|
saveSpeakerRegistry,
|
|
14
15
|
speakerRegistryKey,
|
|
16
|
+
stripSelfMarker,
|
|
15
17
|
} from "./speakers.js";
|
|
16
18
|
|
|
17
19
|
test("resolution precedence: override > wearer flag > provider name > raw key", () => {
|
|
@@ -108,3 +110,129 @@ test("a malformed speakers file throws instead of silently resetting", async ()
|
|
|
108
110
|
rmSync(dir, { recursive: true, force: true });
|
|
109
111
|
}
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
test("the reserved (you) self marker is stripped from non-self display names (issue #1849)", () => {
|
|
115
|
+
const registry = emptySpeakerRegistry();
|
|
116
|
+
registry.selfName = "Jordan";
|
|
117
|
+
// A non-self override whose stored name already ends with the reserved
|
|
118
|
+
// marker must NOT carry it into the rendered label — otherwise the
|
|
119
|
+
// fusion reconstructor would read the suffix as self metadata and
|
|
120
|
+
// attribute this speaker's words to the wearer.
|
|
121
|
+
registry.speakers[speakerRegistryKey("bee", "SPEAKER_01")] = {
|
|
122
|
+
name: "Pat (you)",
|
|
123
|
+
updatedAt: "2026-06-10T00:00:00Z",
|
|
124
|
+
};
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
resolveSpeaker("bee", { speakerKey: "SPEAKER_01" }, registry),
|
|
127
|
+
{ label: "Pat", isSelf: false },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// A self override whose name ends with the marker renders exactly ONE
|
|
131
|
+
// marker (never "Pat (you) (you)").
|
|
132
|
+
registry.speakers[speakerRegistryKey("bee", "SPEAKER_02")] = {
|
|
133
|
+
name: "Pat (you)",
|
|
134
|
+
isSelf: true,
|
|
135
|
+
updatedAt: "2026-06-10T00:00:00Z",
|
|
136
|
+
};
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
resolveSpeaker("bee", { speakerKey: "SPEAKER_02" }, registry),
|
|
139
|
+
{ label: "Pat (you)", isSelf: true },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// A provider-supplied name carrying the marker is also stripped.
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
resolveSpeaker(
|
|
145
|
+
"omi",
|
|
146
|
+
{ speakerKey: "SPEAKER_03", speakerName: "Sam (you)" },
|
|
147
|
+
registry,
|
|
148
|
+
),
|
|
149
|
+
{ label: "Sam", isSelf: false },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("distinctSpeakerLabels never emits a non-self label ending in (you)", () => {
|
|
155
|
+
const registry = emptySpeakerRegistry();
|
|
156
|
+
registry.speakers[speakerRegistryKey("bee", "0")] = {
|
|
157
|
+
name: "Pat (you)",
|
|
158
|
+
updatedAt: "2026-06-10T00:00:00Z",
|
|
159
|
+
};
|
|
160
|
+
const labels = distinctSpeakerLabels(
|
|
161
|
+
"bee",
|
|
162
|
+
[
|
|
163
|
+
{ speakerKey: "user", isWearer: true },
|
|
164
|
+
{ speakerKey: "0" },
|
|
165
|
+
],
|
|
166
|
+
registry,
|
|
167
|
+
);
|
|
168
|
+
// The wearer keeps the marker; the non-self "Pat (you)" override does not.
|
|
169
|
+
assert.deepEqual(labels, ["Me (you)", "Pat"]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("stripSelfMarker / hasSelfMarker use linear string ops (CodeQL #1849)", () => {
|
|
173
|
+
// Standard cases — marker present and absent.
|
|
174
|
+
assert.equal(stripSelfMarker("Jordan (you)"), "Jordan");
|
|
175
|
+
assert.equal(stripSelfMarker("Jordan"), "Jordan");
|
|
176
|
+
assert.equal(hasSelfMarker("Jordan (you)"), true);
|
|
177
|
+
assert.equal(hasSelfMarker("Jordan"), false);
|
|
178
|
+
|
|
179
|
+
// Trailing whitespace around the marker is absorbed.
|
|
180
|
+
assert.equal(stripSelfMarker("Jordan (you) "), "Jordan");
|
|
181
|
+
assert.equal(hasSelfMarker("Jordan (you) \t"), true);
|
|
182
|
+
|
|
183
|
+
// Only the final marker is stripped — a second "(you)" survives.
|
|
184
|
+
assert.equal(stripSelfMarker("Jordan (you)(you)"), "Jordan (you)");
|
|
185
|
+
|
|
186
|
+
// Marker with leading whitespace but no name.
|
|
187
|
+
assert.equal(stripSelfMarker(" (you)"), "");
|
|
188
|
+
assert.equal(stripSelfMarker("(you)"), "");
|
|
189
|
+
|
|
190
|
+
// Empty / whitespace-only input — the case that caused O(n^2)
|
|
191
|
+
// backtracking with the old \s*\(you\)\s*$ regex.
|
|
192
|
+
assert.equal(stripSelfMarker(""), "");
|
|
193
|
+
assert.equal(stripSelfMarker(" "), "");
|
|
194
|
+
assert.equal(hasSelfMarker(" "), false);
|
|
195
|
+
assert.equal(hasSelfMarker(""), false);
|
|
196
|
+
|
|
197
|
+
// "(you)" not at the end is NOT a marker.
|
|
198
|
+
assert.equal(hasSelfMarker("(you) said hi"), false);
|
|
199
|
+
assert.equal(stripSelfMarker("(you) said hi"), "(you) said hi");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("raw speakerKey fallback strips the reserved (you) marker (issue #1849)", () => {
|
|
203
|
+
// A provider speakerKey that literally contains the reserved `(you)`
|
|
204
|
+
// suffix must NEVER carry it into a rendered non-self label. Without
|
|
205
|
+
// the strip, the fusion reconstructor would read the suffix as self
|
|
206
|
+
// metadata and misattribute the speaker as the wearer.
|
|
207
|
+
const registry = emptySpeakerRegistry();
|
|
208
|
+
registry.selfName = "Jordan";
|
|
209
|
+
|
|
210
|
+
// Raw key with a trailing (you) — non-self fallback path.
|
|
211
|
+
const resolved = resolveSpeaker(
|
|
212
|
+
"bee",
|
|
213
|
+
{ speakerKey: "Pat (you)" },
|
|
214
|
+
registry,
|
|
215
|
+
);
|
|
216
|
+
assert.equal(resolved.isSelf, false, "raw key is never self");
|
|
217
|
+
assert.equal(resolved.label, "Pat", "(you) stripped from raw key label");
|
|
218
|
+
assert.equal(hasSelfMarker(resolved.label), false, "no marker leaks");
|
|
219
|
+
|
|
220
|
+
// Bare-digit key with a trailing (you) becomes "Speaker <digits>"
|
|
221
|
+
// after stripping, never carrying the marker.
|
|
222
|
+
const digitResolved = resolveSpeaker(
|
|
223
|
+
"omi",
|
|
224
|
+
{ speakerKey: "5 (you)" },
|
|
225
|
+
registry,
|
|
226
|
+
);
|
|
227
|
+
assert.equal(digitResolved.isSelf, false);
|
|
228
|
+
assert.equal(digitResolved.label, "Speaker 5");
|
|
229
|
+
|
|
230
|
+
// A key that is ONLY "(you)" strips to empty -> "Unknown speaker".
|
|
231
|
+
const onlyMarker = resolveSpeaker(
|
|
232
|
+
"limitless",
|
|
233
|
+
{ speakerKey: "(you)" },
|
|
234
|
+
registry,
|
|
235
|
+
);
|
|
236
|
+
assert.equal(onlyMarker.isSelf, false);
|
|
237
|
+
assert.equal(onlyMarker.label, "Unknown speaker");
|
|
238
|
+
});
|
|
@@ -35,6 +35,43 @@ export interface SpeakerRegistry {
|
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_SELF_NAME = "Me";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Reserved self marker appended to a wearer's display label so the
|
|
40
|
+
* rendered transcript carries explicit, parseable self metadata. It is
|
|
41
|
+
* RESERVED: a non-self display label can never carry it (see
|
|
42
|
+
* `stripSelfMarker`), so when `reconstructFusionInputs` reads it back it
|
|
43
|
+
* is authoritative self metadata — never a coincidental display-name
|
|
44
|
+
* suffix that would misattribute another speaker as the wearer
|
|
45
|
+
* (issue #1849).
|
|
46
|
+
*/
|
|
47
|
+
export const SELF_MARKER_SUFFIX = " (you)";
|
|
48
|
+
const SELF_MARKER = "(you)";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove a reserved `(you)` self marker from a display name. Applied to
|
|
52
|
+
* every base name before it becomes a label so the marker can only ever
|
|
53
|
+
* appear as the renderer-appended self flag, never as part of a stored
|
|
54
|
+
* non-self display name.
|
|
55
|
+
*
|
|
56
|
+
* Implemented with plain string ops instead of a regex so the leading
|
|
57
|
+
* `\s*` (no anchor) cannot cause the O(n²) backtracking CodeQL flags
|
|
58
|
+
* on whitespace-only input (js/polynomial-redos).
|
|
59
|
+
*/
|
|
60
|
+
export function stripSelfMarker(name: string): string {
|
|
61
|
+
const trimmed = name.trimEnd();
|
|
62
|
+
if (!trimmed.endsWith(SELF_MARKER)) return trimmed;
|
|
63
|
+
return trimmed.slice(0, -SELF_MARKER.length).trimEnd();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a rendered label carries the reserved self marker. This is the
|
|
68
|
+
* single authoritative self-status detector shared by the renderer and
|
|
69
|
+
* the fusion reconstructor (issue #1849).
|
|
70
|
+
*/
|
|
71
|
+
export function hasSelfMarker(label: string): boolean {
|
|
72
|
+
return label.trimEnd().endsWith(SELF_MARKER);
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
export function emptySpeakerRegistry(): SpeakerRegistry {
|
|
39
76
|
return { version: 1, selfName: DEFAULT_SELF_NAME, speakers: {} };
|
|
40
77
|
}
|
|
@@ -130,21 +167,38 @@ export function resolveSpeaker(
|
|
|
130
167
|
const override = registry.speakers[speakerRegistryKey(sourceId, segment.speakerKey)];
|
|
131
168
|
if (override) {
|
|
132
169
|
const isSelf = override.isSelf === true;
|
|
170
|
+
// Reserve the `(you)` marker: strip any pre-existing marker from the
|
|
171
|
+
// base name so a NON-self label can never carry it. Without this, a
|
|
172
|
+
// speaker override like "Pat (you)" set WITHOUT --self would render
|
|
173
|
+
// as "Pat (you)" and the fusion reconstructor would read the suffix
|
|
174
|
+
// as self metadata, attributing Pat's words to the wearer (issue
|
|
175
|
+
// #1849).
|
|
176
|
+
const base = stripSelfMarker(override.name);
|
|
133
177
|
return {
|
|
134
|
-
label: isSelf ? `${
|
|
178
|
+
label: isSelf ? `${base}${SELF_MARKER_SUFFIX}` : base,
|
|
135
179
|
isSelf,
|
|
136
180
|
};
|
|
137
181
|
}
|
|
138
182
|
if (segment.isWearer === true) {
|
|
139
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
label: `${stripSelfMarker(registry.selfName)}${SELF_MARKER_SUFFIX}`,
|
|
185
|
+
isSelf: true,
|
|
186
|
+
};
|
|
140
187
|
}
|
|
141
188
|
if (
|
|
142
189
|
typeof segment.speakerName === "string" &&
|
|
143
190
|
segment.speakerName.trim().length > 0
|
|
144
191
|
) {
|
|
145
|
-
|
|
192
|
+
// Provider-supplied names are never self here; strip a stray marker
|
|
193
|
+
// so a provider label cannot masquerade as the reserved self flag.
|
|
194
|
+
return { label: stripSelfMarker(segment.speakerName.trim()), isSelf: false };
|
|
146
195
|
}
|
|
147
|
-
|
|
196
|
+
// Strip the reserved `(you)` marker from the raw key so a provider
|
|
197
|
+
// speakerKey like "Pat (you)" can never leak the marker into a non-self
|
|
198
|
+
// rendered label — the fusion reconstructor reads the suffix as
|
|
199
|
+
// authoritative self metadata and would misattribute the speaker as the
|
|
200
|
+
// wearer (issue #1849).
|
|
201
|
+
const key = stripSelfMarker(segment.speakerKey.trim());
|
|
148
202
|
// Bare diarization indexes read better with a prefix.
|
|
149
203
|
if (/^\d+$/.test(key)) {
|
|
150
204
|
return { label: `Speaker ${key}`, isSelf: false };
|