@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
|
@@ -2,13 +2,26 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { realpath as fsRealpath } from "node:fs/promises";
|
|
5
6
|
import { test } from "node:test";
|
|
6
7
|
|
|
7
8
|
import {
|
|
9
|
+
bodyIsEscaped,
|
|
8
10
|
composeDayTranscriptBody,
|
|
9
11
|
composeDayTranscriptMeta,
|
|
12
|
+
parseDayTranscript,
|
|
10
13
|
serializeDayTranscript,
|
|
11
14
|
} from "./day-store.js";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_SOURCE_TRUST,
|
|
17
|
+
FUSION_ALGO_VERSION,
|
|
18
|
+
FusionArtifactStore,
|
|
19
|
+
canonicalDayKey,
|
|
20
|
+
composeFusionDayMeta,
|
|
21
|
+
parseFusionDay,
|
|
22
|
+
reconstructFusionInputs,
|
|
23
|
+
serializeFusionDay,
|
|
24
|
+
} from "./fusion/index.js";
|
|
12
25
|
import { emptySpeakerRegistry } from "./speakers.js";
|
|
13
26
|
import {
|
|
14
27
|
createWearableMemoryWriter,
|
|
@@ -34,6 +47,46 @@ function makeStorage(memoryDir: string): WearableStorageIo & {
|
|
|
34
47
|
content: string;
|
|
35
48
|
}>;
|
|
36
49
|
} {
|
|
50
|
+
const fusedFiles = new Map<string, string>();
|
|
51
|
+
const fusionStore = new FusionArtifactStore(path.join(memoryDir, "wearables"), memoryDir, {
|
|
52
|
+
writeFile: async (filePath, content) => {
|
|
53
|
+
fusedFiles.set(filePath, content);
|
|
54
|
+
},
|
|
55
|
+
readFile: async (filePath) => {
|
|
56
|
+
if (!fusedFiles.has(filePath)) {
|
|
57
|
+
const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
|
|
58
|
+
err.code = "ENOENT";
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
return fusedFiles.get(filePath)!;
|
|
62
|
+
},
|
|
63
|
+
readDir: async (dirPath) =>
|
|
64
|
+
[...fusedFiles.keys()]
|
|
65
|
+
.filter((key) => path.dirname(key) === dirPath)
|
|
66
|
+
.map((key) => path.basename(key)),
|
|
67
|
+
deleteFile: async (filePath) => {
|
|
68
|
+
if (!fusedFiles.delete(filePath)) {
|
|
69
|
+
const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
|
|
70
|
+
err.code = "ENOENT";
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
realpath: (filePath) => fsRealpath(filePath),
|
|
75
|
+
lstat: async (filePath) => {
|
|
76
|
+
// The in-memory mock models no real symlinks: a `_fusion` dir
|
|
77
|
+
// "exists" when at least one fused file lives beneath it, and is
|
|
78
|
+
// never itself a symbolic link.
|
|
79
|
+
const hasChildren = [...fusedFiles.keys()].some(
|
|
80
|
+
(k) => path.dirname(k) === filePath,
|
|
81
|
+
);
|
|
82
|
+
if (!hasChildren) {
|
|
83
|
+
const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
|
|
84
|
+
err.code = "ENOENT";
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
return { isSymbolicLink: false };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
37
90
|
const files = new Map<string, string>();
|
|
38
91
|
const storage = {
|
|
39
92
|
dir: memoryDir,
|
|
@@ -65,6 +118,9 @@ function makeStorage(memoryDir: string): WearableStorageIo & {
|
|
|
65
118
|
.filter((entry) => sourceId === undefined || entry.source === sourceId)
|
|
66
119
|
.sort((a, b) => (a.date > b.date ? -1 : a.date < b.date ? 1 : 0));
|
|
67
120
|
},
|
|
121
|
+
fusionArtifactStore() {
|
|
122
|
+
return fusionStore;
|
|
123
|
+
},
|
|
68
124
|
async readAllMemories() {
|
|
69
125
|
return storage.memories;
|
|
70
126
|
},
|
|
@@ -110,6 +166,7 @@ function storeDay(
|
|
|
110
166
|
sourceId: string,
|
|
111
167
|
date: string,
|
|
112
168
|
texts: string[],
|
|
169
|
+
timezone = "UTC",
|
|
113
170
|
): void {
|
|
114
171
|
const registry = emptySpeakerRegistry();
|
|
115
172
|
const conversations: WearableConversation[] = [
|
|
@@ -126,11 +183,11 @@ function storeDay(
|
|
|
126
183
|
})),
|
|
127
184
|
},
|
|
128
185
|
];
|
|
129
|
-
const body = composeDayTranscriptBody(sourceId, date,
|
|
186
|
+
const body = composeDayTranscriptBody(sourceId, date, timezone, conversations, registry);
|
|
130
187
|
const meta = composeDayTranscriptMeta(
|
|
131
188
|
sourceId,
|
|
132
189
|
date,
|
|
133
|
-
|
|
190
|
+
timezone,
|
|
134
191
|
conversations,
|
|
135
192
|
registry,
|
|
136
193
|
body,
|
|
@@ -273,6 +330,59 @@ test("zero in-scope indexed hits fall back to the bounded scan", async () => {
|
|
|
273
330
|
}
|
|
274
331
|
});
|
|
275
332
|
|
|
333
|
+
test("indexed search escapes the query and decodes the snippet (#1849)", async () => {
|
|
334
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
|
|
335
|
+
try {
|
|
336
|
+
const storage = makeStorage(dir);
|
|
337
|
+
// Segment text carries a literal backslash. In the stored file the
|
|
338
|
+
// backslash is doubled, so the indexed text has C:\\GAPDIR while
|
|
339
|
+
// the original had C:\GAPDIR. The indexed backend must receive
|
|
340
|
+
// the escaped form to match the indexed representation.
|
|
341
|
+
storeDay(storage, "limitless", "2026-06-10", ["File at C:\\GAPDIR\\report.txt"]);
|
|
342
|
+
|
|
343
|
+
const receivedQueries: string[] = [];
|
|
344
|
+
const service = new WearablesService({
|
|
345
|
+
config: { ...defaultWearablesConfig(), enabled: true },
|
|
346
|
+
getStorage: async () => storage,
|
|
347
|
+
extract: null,
|
|
348
|
+
searchBackend: {
|
|
349
|
+
async search(query: string) {
|
|
350
|
+
receivedQueries.push(query);
|
|
351
|
+
// Simulate a hit whose preview carries the escaped form.
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
path: "/memory/wearables/limitless/2026-06-10.md",
|
|
355
|
+
score: 0.9,
|
|
356
|
+
preview: "**Me (you)** [10:00]: File at C:\\\\GAPDIR\\\\report.txt",
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const results = await service.searchTranscripts("C:\\GAPDIR");
|
|
364
|
+
assert.equal(results.length, 1);
|
|
365
|
+
assert.equal(results[0].backend, "indexed");
|
|
366
|
+
assert.equal(results[0].source, "limitless");
|
|
367
|
+
// The escaped query form (doubled backslash) must reach the backend.
|
|
368
|
+
assert.ok(
|
|
369
|
+
receivedQueries.some((q) => q === "C:\\\\GAPDIR"),
|
|
370
|
+
"indexed backend must receive escaped query for backslash content",
|
|
371
|
+
);
|
|
372
|
+
// The snippet must show the DECODED original (single backslash).
|
|
373
|
+
assert.ok(
|
|
374
|
+
results[0].snippet.includes("C:\\GAPDIR"),
|
|
375
|
+
"snippet shows decoded single backslash",
|
|
376
|
+
);
|
|
377
|
+
assert.ok(
|
|
378
|
+
!results[0].snippet.includes("C:\\\\GAPDIR"),
|
|
379
|
+
"no escaped-backslash leak in snippet",
|
|
380
|
+
);
|
|
381
|
+
} finally {
|
|
382
|
+
rmSync(dir, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
276
386
|
test("transcriptMemories filters by wearable source and day", async () => {
|
|
277
387
|
const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
|
|
278
388
|
try {
|
|
@@ -595,3 +705,921 @@ test("speaker and correction management round-trips through the service", async
|
|
|
595
705
|
rmSync(dir, { recursive: true, force: true });
|
|
596
706
|
}
|
|
597
707
|
});
|
|
708
|
+
|
|
709
|
+
test("fuseDay is gated by wearables.fusion.enabled", async () => {
|
|
710
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
711
|
+
try {
|
|
712
|
+
storeDay(storage, "limitless", "2026-06-10", ["Hello world."]);
|
|
713
|
+
const service = makeService(storage); // fusion defaults to disabled
|
|
714
|
+
await assert.rejects(() => service.fuseDay("2026-06-10"), /fusion is not enabled/);
|
|
715
|
+
} finally {
|
|
716
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("fuseDay honors the wearables master gate before the fusion gate (issue #1849)", async () => {
|
|
721
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
722
|
+
try {
|
|
723
|
+
storeDay(storage, "limitless", "2026-06-10", ["Hello world."]);
|
|
724
|
+
// wearables.enabled=false but fusion.enabled=true — fuseDay must refuse
|
|
725
|
+
// at the master gate (the same assertEnabled() sync/checkAuth use),
|
|
726
|
+
// before reading any source or touching the derived artifact store.
|
|
727
|
+
const service = makeService(storage, {
|
|
728
|
+
enabled: false,
|
|
729
|
+
sources: {
|
|
730
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
731
|
+
},
|
|
732
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
733
|
+
});
|
|
734
|
+
await assert.rejects(
|
|
735
|
+
() => service.fuseDay("2026-06-10"),
|
|
736
|
+
/wearables are not enabled/,
|
|
737
|
+
);
|
|
738
|
+
// The master-gate refusal must not write or delete any fusion artifact.
|
|
739
|
+
assert.equal(
|
|
740
|
+
await storage.fusionArtifactStore().readFusedDay("2026-06-10"),
|
|
741
|
+
null,
|
|
742
|
+
"master-gate refusal must not mutate derived fusion state",
|
|
743
|
+
);
|
|
744
|
+
} finally {
|
|
745
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("fuseDay writes a derived artifact, is idempotent, and leaves raw transcripts untouched", async () => {
|
|
750
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
751
|
+
try {
|
|
752
|
+
// Two enabled sources recorded the same overlapping window.
|
|
753
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
754
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
755
|
+
const service = makeService(storage, {
|
|
756
|
+
sources: {
|
|
757
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
758
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
759
|
+
},
|
|
760
|
+
fusion: {
|
|
761
|
+
enabled: true,
|
|
762
|
+
proximityGapMs: 300_000,
|
|
763
|
+
windowToleranceMs: 30_000,
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// No fused artifact yet.
|
|
768
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
769
|
+
|
|
770
|
+
const first = await service.fuseDay("2026-06-10");
|
|
771
|
+
assert.equal(first.written, true);
|
|
772
|
+
assert.ok(first.conversationCount >= 1, "overlapping sources fuse into >=1 conversation");
|
|
773
|
+
assert.deepEqual([...first.sources].sort(), ["bee", "limitless"]);
|
|
774
|
+
|
|
775
|
+
// The derived artifact is readable via the listing surface.
|
|
776
|
+
const conversations = await service.fusedConversations("2026-06-10");
|
|
777
|
+
assert.equal(conversations.length, first.conversationCount);
|
|
778
|
+
assert.ok(conversations[0]!.id.startsWith("fusion-"));
|
|
779
|
+
assert.deepEqual(
|
|
780
|
+
[...conversations[0]!.sources].sort(),
|
|
781
|
+
["bee", "limitless"],
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
// Idempotent re-run: identical inputs -> not written again.
|
|
785
|
+
const second = await service.fuseDay("2026-06-10");
|
|
786
|
+
assert.equal(second.written, false, "unchanged inputs must not rewrite the artifact");
|
|
787
|
+
assert.equal(second.contentHash, first.contentHash);
|
|
788
|
+
|
|
789
|
+
// Raw per-source transcripts remain readable and untouched.
|
|
790
|
+
const limitlessRaw = await storage.readWearableDayTranscript("limitless", "2026-06-10");
|
|
791
|
+
const beeRaw = await storage.readWearableDayTranscript("bee", "2026-06-10");
|
|
792
|
+
assert.notEqual(limitlessRaw, null);
|
|
793
|
+
assert.notEqual(beeRaw, null);
|
|
794
|
+
// Raw transcripts are byte-identical to what was stored (fusion never
|
|
795
|
+
// overwrites source transcripts).
|
|
796
|
+
assert.ok(limitlessRaw?.includes("We agreed to ship Friday."));
|
|
797
|
+
assert.ok(beeRaw?.includes("We agreed to ship Friday."));
|
|
798
|
+
|
|
799
|
+
// listFusedDays surfaces the fused date.
|
|
800
|
+
assert.deepEqual(await service.listFusedDays(), ["2026-06-10"]);
|
|
801
|
+
} finally {
|
|
802
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("fuseDay rebuilds the artifact when fusion config changes (not skipped)", async () => {
|
|
807
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
808
|
+
try {
|
|
809
|
+
// Two enabled sources recorded the same overlapping window.
|
|
810
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
811
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
812
|
+
|
|
813
|
+
const baseSources = {
|
|
814
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
815
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const serviceGap5m = makeService(storage, {
|
|
819
|
+
sources: baseSources,
|
|
820
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Initial fuse writes the artifact.
|
|
824
|
+
const first = await serviceGap5m.fuseDay("2026-06-10");
|
|
825
|
+
assert.equal(first.written, true);
|
|
826
|
+
const firstHash = first.contentHash;
|
|
827
|
+
|
|
828
|
+
// Unchanged inputs + config => idempotent skip.
|
|
829
|
+
const rerun = await serviceGap5m.fuseDay("2026-06-10");
|
|
830
|
+
assert.equal(rerun.written, false, "unchanged inputs+config must not rewrite");
|
|
831
|
+
assert.equal(rerun.contentHash, firstHash);
|
|
832
|
+
|
|
833
|
+
// Tune proximityGapMs => new content hash => artifact rebuilt, not skipped.
|
|
834
|
+
const serviceGap10m = makeService(storage, {
|
|
835
|
+
sources: baseSources,
|
|
836
|
+
fusion: { enabled: true, proximityGapMs: 600_000, windowToleranceMs: 30_000 },
|
|
837
|
+
});
|
|
838
|
+
const afterGap = await serviceGap10m.fuseDay("2026-06-10");
|
|
839
|
+
assert.equal(afterGap.written, true, "proximityGapMs change must trigger a rebuild");
|
|
840
|
+
assert.notEqual(afterGap.contentHash, firstHash);
|
|
841
|
+
|
|
842
|
+
// Re-run under the new config => idempotent skip again.
|
|
843
|
+
const afterGapRerun = await serviceGap10m.fuseDay("2026-06-10");
|
|
844
|
+
assert.equal(afterGapRerun.written, false);
|
|
845
|
+
assert.equal(afterGapRerun.contentHash, afterGap.contentHash);
|
|
846
|
+
|
|
847
|
+
// Tune per-source trust => new content hash => artifact rebuilt.
|
|
848
|
+
const serviceTrust = makeService(storage, {
|
|
849
|
+
sources: {
|
|
850
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true, sourceTrust: 0.95 },
|
|
851
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
852
|
+
},
|
|
853
|
+
fusion: { enabled: true, proximityGapMs: 600_000, windowToleranceMs: 30_000 },
|
|
854
|
+
});
|
|
855
|
+
const afterTrust = await serviceTrust.fuseDay("2026-06-10");
|
|
856
|
+
assert.equal(afterTrust.written, true, "per-source trust change must trigger a rebuild");
|
|
857
|
+
assert.notEqual(afterTrust.contentHash, afterGap.contentHash);
|
|
858
|
+
} finally {
|
|
859
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("fuseDay regenerates a stale artifact written under an older algorithm version (issue #1849)", async () => {
|
|
864
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
865
|
+
try {
|
|
866
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
867
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
868
|
+
const baseSources = {
|
|
869
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
870
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
871
|
+
};
|
|
872
|
+
const fusionConfig = { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 };
|
|
873
|
+
const service = makeService(storage, { sources: baseSources, fusion: fusionConfig });
|
|
874
|
+
|
|
875
|
+
// Initial fuse writes the artifact under the current algorithm version.
|
|
876
|
+
const first = await service.fuseDay("2026-06-10");
|
|
877
|
+
assert.equal(first.written, true);
|
|
878
|
+
const currentHash = first.contentHash;
|
|
879
|
+
|
|
880
|
+
// Unchanged inputs + same version => idempotent skip.
|
|
881
|
+
const rerun = await service.fuseDay("2026-06-10");
|
|
882
|
+
assert.equal(rerun.written, false, "same version must be idempotent");
|
|
883
|
+
|
|
884
|
+
// Simulate a pre-bump artifact: reconstruct the same inputs the service
|
|
885
|
+
// fuses and compute the day hash under a STALE algorithm version, then
|
|
886
|
+
// overwrite the stored artifact with the SAME body but that stale hash.
|
|
887
|
+
const bodies = await Promise.all(
|
|
888
|
+
["limitless", "bee"].map(async (source) => {
|
|
889
|
+
const raw = await storage.readWearableDayTranscript(source, "2026-06-10");
|
|
890
|
+
const parsed = parseDayTranscript(raw ?? "");
|
|
891
|
+
return {
|
|
892
|
+
source,
|
|
893
|
+
body: parsed?.body ?? raw ?? "",
|
|
894
|
+
escaped: bodyIsEscaped(parsed?.meta),
|
|
895
|
+
};
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
898
|
+
const reconstructed = reconstructFusionInputs("2026-06-10", bodies);
|
|
899
|
+
const staleHash = canonicalDayKey(
|
|
900
|
+
"2026-06-10",
|
|
901
|
+
reconstructed,
|
|
902
|
+
{
|
|
903
|
+
proximityGapMs: 300_000,
|
|
904
|
+
windowToleranceMs: 30_000,
|
|
905
|
+
sourceTrust: { limitless: DEFAULT_SOURCE_TRUST, bee: DEFAULT_SOURCE_TRUST },
|
|
906
|
+
},
|
|
907
|
+
"2000-01-01-stale",
|
|
908
|
+
);
|
|
909
|
+
assert.notEqual(staleHash, currentHash, "sanity: stale-version hash differs");
|
|
910
|
+
// Sanity: the current-version hash matches what the service produced —
|
|
911
|
+
// the only difference between staleHash and currentHash is the version.
|
|
912
|
+
assert.equal(
|
|
913
|
+
canonicalDayKey(
|
|
914
|
+
"2026-06-10",
|
|
915
|
+
reconstructed,
|
|
916
|
+
{
|
|
917
|
+
proximityGapMs: 300_000,
|
|
918
|
+
windowToleranceMs: 30_000,
|
|
919
|
+
sourceTrust: { limitless: DEFAULT_SOURCE_TRUST, bee: DEFAULT_SOURCE_TRUST },
|
|
920
|
+
},
|
|
921
|
+
FUSION_ALGO_VERSION,
|
|
922
|
+
),
|
|
923
|
+
currentHash,
|
|
924
|
+
"sanity: canonicalDayKey with the current version matches fuseDay output",
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
// Overwrite the stored artifact with the SAME conversations but the stale
|
|
928
|
+
// contentHash (same body => same bodyHash; only the version-derived
|
|
929
|
+
// contentHash differs).
|
|
930
|
+
const store = storage.fusionArtifactStore();
|
|
931
|
+
const rawArtifact = await store.readFusedDay("2026-06-10");
|
|
932
|
+
const parsedArtifact = parseFusionDay(rawArtifact ?? "");
|
|
933
|
+
assert.ok(parsedArtifact, "artifact must exist after initial fuse");
|
|
934
|
+
const staleMeta = composeFusionDayMeta(
|
|
935
|
+
"2026-06-10",
|
|
936
|
+
parsedArtifact!.conversations,
|
|
937
|
+
[...first.sources].sort(),
|
|
938
|
+
staleHash,
|
|
939
|
+
parsedArtifact!.meta.fusedAt,
|
|
940
|
+
);
|
|
941
|
+
await store.writeFusedDay(
|
|
942
|
+
"2026-06-10",
|
|
943
|
+
serializeFusionDay(staleMeta, parsedArtifact!.conversations),
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Re-fuse: the recomputed hash (current version) != stale stored hash, so
|
|
947
|
+
// the artifact is regenerated rather than skipped.
|
|
948
|
+
const afterStale = await service.fuseDay("2026-06-10");
|
|
949
|
+
assert.equal(afterStale.written, true, "stale algo-version artifact must be regenerated");
|
|
950
|
+
assert.equal(afterStale.contentHash, currentHash, "regenerated hash matches the current algorithm");
|
|
951
|
+
} finally {
|
|
952
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
test("fuseDay rewrites an artifact whose body is corrupt despite a matching contentHash", async () => {
|
|
956
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
957
|
+
try {
|
|
958
|
+
// Two enabled sources recorded the same overlapping window.
|
|
959
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
960
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
961
|
+
const service = makeService(storage, {
|
|
962
|
+
sources: {
|
|
963
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
964
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
965
|
+
},
|
|
966
|
+
fusion: {
|
|
967
|
+
enabled: true,
|
|
968
|
+
proximityGapMs: 300_000,
|
|
969
|
+
windowToleranceMs: 30_000,
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Initial fuse writes a valid artifact.
|
|
974
|
+
const first = await service.fuseDay("2026-06-10");
|
|
975
|
+
assert.equal(first.written, true);
|
|
976
|
+
assert.ok(first.conversationCount >= 1, "overlapping sources fuse into >=1 conversation");
|
|
977
|
+
const hash = first.contentHash;
|
|
978
|
+
|
|
979
|
+
// Corrupt the stored body while leaving the frontmatter (incl.
|
|
980
|
+
// contentHash) intact: a truncated/garbled JSON body that the lenient
|
|
981
|
+
// read parser reduces to zero conversations.
|
|
982
|
+
const fusionStore = storage.fusionArtifactStore();
|
|
983
|
+
const validRaw = await fusionStore.readFusedDay("2026-06-10");
|
|
984
|
+
assert.ok(validRaw);
|
|
985
|
+
const closeIdx = validRaw!.indexOf("\n---\n", 4);
|
|
986
|
+
assert.ok(closeIdx !== -1, "fused artifact has a closing frontmatter delimiter");
|
|
987
|
+
const corruptRaw = `${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`;
|
|
988
|
+
await fusionStore.writeFusedDay("2026-06-10", corruptRaw);
|
|
989
|
+
|
|
990
|
+
// The corrupted artifact surfaces as a corrupt-artifact error, not a
|
|
991
|
+
// clean empty list (issue #1849).
|
|
992
|
+
await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
|
|
993
|
+
|
|
994
|
+
// Re-fuse: matching contentHash but a corrupt body => self-repair
|
|
995
|
+
// (written: true), not a silent skip that leaves the bad file in place.
|
|
996
|
+
const repaired = await service.fuseDay("2026-06-10");
|
|
997
|
+
assert.equal(repaired.written, true, "a corrupt body must be rewritten despite a matching hash");
|
|
998
|
+
assert.equal(repaired.contentHash, hash, "inputs+config are unchanged so the hash is stable");
|
|
999
|
+
assert.equal(repaired.conversationCount, first.conversationCount);
|
|
1000
|
+
|
|
1001
|
+
// Subsequent read returns the correct conversations again.
|
|
1002
|
+
const conversations = await service.fusedConversations("2026-06-10");
|
|
1003
|
+
assert.equal(conversations.length, first.conversationCount);
|
|
1004
|
+
assert.ok(conversations[0]!.id.startsWith("fusion-"));
|
|
1005
|
+
assert.deepEqual(
|
|
1006
|
+
[...conversations[0]!.sources].sort(),
|
|
1007
|
+
["bee", "limitless"],
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
// A final re-run is idempotent again: the repaired body is valid, so
|
|
1011
|
+
// it is skipped (written: false), proving the rewrite was a one-off
|
|
1012
|
+
// self-repair rather than a permanent forced write.
|
|
1013
|
+
const idempotent = await service.fuseDay("2026-06-10");
|
|
1014
|
+
assert.equal(idempotent.written, false, "a repaired artifact must skip on the next run");
|
|
1015
|
+
assert.equal(idempotent.contentHash, hash);
|
|
1016
|
+
} finally {
|
|
1017
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("fuseDay rewrites a corrupt body even when the expected conversation count is zero", async () => {
|
|
1022
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1023
|
+
try {
|
|
1024
|
+
// No stored transcripts for this day -> fusion yields ZERO conversations.
|
|
1025
|
+
const service = makeService(storage, {
|
|
1026
|
+
sources: {
|
|
1027
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1028
|
+
},
|
|
1029
|
+
fusion: {
|
|
1030
|
+
enabled: true,
|
|
1031
|
+
proximityGapMs: 300_000,
|
|
1032
|
+
windowToleranceMs: 30_000,
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// First fuse writes a valid (empty) artifact.
|
|
1037
|
+
const first = await service.fuseDay("2026-06-10");
|
|
1038
|
+
assert.equal(first.written, true);
|
|
1039
|
+
assert.equal(first.conversationCount, 0);
|
|
1040
|
+
const hash = first.contentHash;
|
|
1041
|
+
|
|
1042
|
+
// A legitimately-empty ([] body) artifact must SKIP on re-run: same
|
|
1043
|
+
// inputs + config, clean parse, 0 == 0 -> not rewritten.
|
|
1044
|
+
const idempotent = await service.fuseDay("2026-06-10");
|
|
1045
|
+
assert.equal(idempotent.written, false, "a valid empty body must skip");
|
|
1046
|
+
assert.equal(idempotent.contentHash, hash);
|
|
1047
|
+
|
|
1048
|
+
// Corrupt the stored body while leaving the frontmatter (incl.
|
|
1049
|
+
// contentHash) intact: truncated JSON that the lenient read parser
|
|
1050
|
+
// reduces to zero conversations.
|
|
1051
|
+
const fusionStore = storage.fusionArtifactStore();
|
|
1052
|
+
const validRaw = await fusionStore.readFusedDay("2026-06-10");
|
|
1053
|
+
assert.ok(validRaw);
|
|
1054
|
+
const closeIdx = validRaw!.indexOf("\n---\n", 4);
|
|
1055
|
+
assert.ok(closeIdx !== -1, "fused artifact has a closing frontmatter delimiter");
|
|
1056
|
+
const corruptRaw = `${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`;
|
|
1057
|
+
await fusionStore.writeFusedDay("2026-06-10", corruptRaw);
|
|
1058
|
+
|
|
1059
|
+
// The corrupted body surfaces as a corrupt-artifact error (not a clean
|
|
1060
|
+
// empty), and the recomputed result is ALSO zero — without the parseOk
|
|
1061
|
+
// signal this 0 == 0 match plus a matching hash would silently skip
|
|
1062
|
+
// and leave the bad file.
|
|
1063
|
+
await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
|
|
1064
|
+
|
|
1065
|
+
// Re-fuse: matching hash + 0 == 0 but a corrupt body (parseOk:false)
|
|
1066
|
+
// => self-repair (written: true), not a silent skip.
|
|
1067
|
+
const repaired = await service.fuseDay("2026-06-10");
|
|
1068
|
+
assert.equal(
|
|
1069
|
+
repaired.written,
|
|
1070
|
+
true,
|
|
1071
|
+
"a corrupt body must be rewritten even when the expected count is zero",
|
|
1072
|
+
);
|
|
1073
|
+
assert.equal(repaired.contentHash, hash);
|
|
1074
|
+
assert.equal(repaired.conversationCount, 0);
|
|
1075
|
+
|
|
1076
|
+
// A final re-run is idempotent again: the repaired body parses cleanly,
|
|
1077
|
+
// so it skips (written: false), proving the rewrite was a one-off
|
|
1078
|
+
// self-repair rather than a permanent forced write.
|
|
1079
|
+
const final = await service.fuseDay("2026-06-10");
|
|
1080
|
+
assert.equal(final.written, false, "a repaired artifact must skip on the next run");
|
|
1081
|
+
assert.equal(final.contentHash, hash);
|
|
1082
|
+
} finally {
|
|
1083
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("fuseDay rewrites an artifact whose body has malformed elements despite matching hash + bodyHash + count", async () => {
|
|
1088
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1089
|
+
try {
|
|
1090
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1091
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1092
|
+
const service = makeService(storage, {
|
|
1093
|
+
sources: {
|
|
1094
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1095
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1096
|
+
},
|
|
1097
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const first = await service.fuseDay("2026-06-10");
|
|
1101
|
+
assert.equal(first.written, true);
|
|
1102
|
+
assert.ok(first.conversationCount >= 1);
|
|
1103
|
+
const hash = first.contentHash;
|
|
1104
|
+
|
|
1105
|
+
// Replace ONLY the body with a parseable-but-malformed array `[{}]`,
|
|
1106
|
+
// leaving every frontmatter field (contentHash, bodyHash, count)
|
|
1107
|
+
// intact. The body parses as JSON and is an array, but its element is
|
|
1108
|
+
// not a well-formed FusedWearableConversation — element validation
|
|
1109
|
+
// must drive parseOk:false so fuseDay rewrites rather than trusting
|
|
1110
|
+
// the matching hashes + count.
|
|
1111
|
+
const fusionStore = storage.fusionArtifactStore();
|
|
1112
|
+
const validRaw = await fusionStore.readFusedDay("2026-06-10");
|
|
1113
|
+
assert.ok(validRaw);
|
|
1114
|
+
const closeIdx = validRaw!.indexOf("\n---\n", 4);
|
|
1115
|
+
assert.ok(closeIdx !== -1);
|
|
1116
|
+
await fusionStore.writeFusedDay("2026-06-10", `${validRaw!.slice(0, closeIdx + 5)}\n[{}]`);
|
|
1117
|
+
|
|
1118
|
+
// The malformed body surfaces as a corrupt-artifact error (issue #1849).
|
|
1119
|
+
await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
|
|
1120
|
+
|
|
1121
|
+
// Re-fuse: matching hash + bodyHash + count but parseOk:false => rewrite.
|
|
1122
|
+
const repaired = await service.fuseDay("2026-06-10");
|
|
1123
|
+
assert.equal(repaired.written, true, "a malformed-element body must be rewritten");
|
|
1124
|
+
assert.equal(repaired.contentHash, hash);
|
|
1125
|
+
assert.equal(repaired.conversationCount, first.conversationCount);
|
|
1126
|
+
|
|
1127
|
+
// Repaired body is valid again, so the next run skips idempotently.
|
|
1128
|
+
const idempotent = await service.fuseDay("2026-06-10");
|
|
1129
|
+
assert.equal(idempotent.written, false, "a repaired artifact must skip on the next run");
|
|
1130
|
+
assert.equal(idempotent.contentHash, hash);
|
|
1131
|
+
} finally {
|
|
1132
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
test("fusedConversations surfaces a corrupt artifact distinctly, not as a clean empty (issue #1849)", async () => {
|
|
1137
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1138
|
+
try {
|
|
1139
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1140
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1141
|
+
const service = makeService(storage, {
|
|
1142
|
+
sources: {
|
|
1143
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1144
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1145
|
+
},
|
|
1146
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Initial fuse writes a valid artifact with >= 1 conversation.
|
|
1150
|
+
const first = await service.fuseDay("2026-06-10");
|
|
1151
|
+
assert.ok(first.conversationCount >= 1);
|
|
1152
|
+
|
|
1153
|
+
// A day that was never fused returns [] — the "no artifact" path.
|
|
1154
|
+
assert.deepEqual(await service.fusedConversations("2026-07-01"), []);
|
|
1155
|
+
|
|
1156
|
+
// Corrupt the body: valid frontmatter, garbled JSON body.
|
|
1157
|
+
const fusionStore = storage.fusionArtifactStore();
|
|
1158
|
+
const validRaw = await fusionStore.readFusedDay("2026-06-10");
|
|
1159
|
+
assert.ok(validRaw);
|
|
1160
|
+
const closeIdx = validRaw!.indexOf("\n---\n", 4);
|
|
1161
|
+
assert.ok(closeIdx !== -1);
|
|
1162
|
+
await fusionStore.writeFusedDay(
|
|
1163
|
+
"2026-06-10",
|
|
1164
|
+
`${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`,
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
// The corrupt artifact must NOT look like "no conversations" — it must
|
|
1168
|
+
// throw so the caller/CLI can distinguish corruption from absence and
|
|
1169
|
+
// prompt a re-fuse, rather than silently returning an empty list.
|
|
1170
|
+
await assert.rejects(
|
|
1171
|
+
service.fusedConversations("2026-06-10"),
|
|
1172
|
+
/corrupt/,
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
// The "never fused" path is unchanged — still returns [].
|
|
1176
|
+
assert.deepEqual(await service.fusedConversations("2026-07-01"), []);
|
|
1177
|
+
} finally {
|
|
1178
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
test("fuseDay rewrites an artifact whose body bytes drifted despite matching contentHash + count", async () => {
|
|
1183
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1184
|
+
try {
|
|
1185
|
+
storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1186
|
+
storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
|
|
1187
|
+
const service = makeService(storage, {
|
|
1188
|
+
sources: {
|
|
1189
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1190
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1191
|
+
},
|
|
1192
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
const first = await service.fuseDay("2026-06-10");
|
|
1196
|
+
assert.equal(first.written, true);
|
|
1197
|
+
assert.ok(first.conversationCount >= 1);
|
|
1198
|
+
const hash = first.contentHash;
|
|
1199
|
+
|
|
1200
|
+
// Tamper: keep the body structurally valid (parses, every element is a
|
|
1201
|
+
// well-formed conversation, same conversation count) but ALTER its
|
|
1202
|
+
// bytes, leaving the frontmatter (contentHash + bodyHash + count)
|
|
1203
|
+
// untouched. parseOk stays true and contentHash still matches, so only
|
|
1204
|
+
// the body-hash recompute over the stored body can catch the drift.
|
|
1205
|
+
const fusionStore = storage.fusionArtifactStore();
|
|
1206
|
+
const validRaw = await fusionStore.readFusedDay("2026-06-10");
|
|
1207
|
+
assert.ok(validRaw);
|
|
1208
|
+
const closeIdx = validRaw!.indexOf("\n---\n", 4);
|
|
1209
|
+
assert.ok(closeIdx !== -1);
|
|
1210
|
+
const header = validRaw!.slice(0, closeIdx + 5);
|
|
1211
|
+
const bodyJson = validRaw!.slice(closeIdx + 5).replace(/^\n/, "").trimEnd();
|
|
1212
|
+
const convs = JSON.parse(bodyJson) as Array<{ segments: Array<{ text: string }> }>;
|
|
1213
|
+
convs[0]!.segments[0]!.text += " [tampered]";
|
|
1214
|
+
await fusionStore.writeFusedDay(
|
|
1215
|
+
"2026-06-10",
|
|
1216
|
+
`${header}\n${JSON.stringify(convs, null, 2)}\n`,
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
// The tampered body still parses cleanly (valid structure + elements),
|
|
1220
|
+
// so it is served until the next fuseDay repairs it.
|
|
1221
|
+
const tampered = await service.fusedConversations("2026-06-10");
|
|
1222
|
+
assert.equal(tampered.length, first.conversationCount);
|
|
1223
|
+
assert.ok(tampered[0]!.segments[0]!.text.includes("[tampered]"));
|
|
1224
|
+
|
|
1225
|
+
// Re-fuse: contentHash matches + parseOk true, but the stored body
|
|
1226
|
+
// hash no longer matches a recompute over the stored body => rewrite.
|
|
1227
|
+
const repaired = await service.fuseDay("2026-06-10");
|
|
1228
|
+
assert.equal(
|
|
1229
|
+
repaired.written,
|
|
1230
|
+
true,
|
|
1231
|
+
"a byte-drifted body must be rewritten via the body-hash check",
|
|
1232
|
+
);
|
|
1233
|
+
assert.equal(repaired.contentHash, hash);
|
|
1234
|
+
assert.equal(repaired.conversationCount, first.conversationCount);
|
|
1235
|
+
|
|
1236
|
+
// The repaired body no longer carries the tampered text.
|
|
1237
|
+
const restored = await service.fusedConversations("2026-06-10");
|
|
1238
|
+
assert.ok(!restored[0]!.segments[0]!.text.includes("[tampered]"));
|
|
1239
|
+
|
|
1240
|
+
const idempotent = await service.fuseDay("2026-06-10");
|
|
1241
|
+
assert.equal(idempotent.written, false);
|
|
1242
|
+
assert.equal(idempotent.contentHash, hash);
|
|
1243
|
+
} finally {
|
|
1244
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
test("fuseDay skips a day whose sources were rendered under conflicting timezones", async () => {
|
|
1249
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1250
|
+
try {
|
|
1251
|
+
// Two enabled sources recorded the same window but in DIFFERENT
|
|
1252
|
+
// timezones whose UTC offsets genuinely differ on this date
|
|
1253
|
+
// (America/Los_Angeles is UTC-7 in summer; Asia/Tokyo is UTC+9).
|
|
1254
|
+
storeDay(
|
|
1255
|
+
storage,
|
|
1256
|
+
"limitless",
|
|
1257
|
+
"2026-06-10",
|
|
1258
|
+
["We agreed to ship Friday."],
|
|
1259
|
+
"America/Los_Angeles",
|
|
1260
|
+
);
|
|
1261
|
+
storeDay(
|
|
1262
|
+
storage,
|
|
1263
|
+
"bee",
|
|
1264
|
+
"2026-06-10",
|
|
1265
|
+
["We agreed to ship Friday."],
|
|
1266
|
+
"Asia/Tokyo",
|
|
1267
|
+
);
|
|
1268
|
+
const service = makeService(storage, {
|
|
1269
|
+
sources: {
|
|
1270
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1271
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1272
|
+
},
|
|
1273
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1277
|
+
// Fail-safe: the day is skipped with a reason, not silently misaligned.
|
|
1278
|
+
assert.equal(result.written, false);
|
|
1279
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1280
|
+
assert.equal(result.sources.length, 0);
|
|
1281
|
+
// No artifact is written for the conflicting day.
|
|
1282
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1283
|
+
} finally {
|
|
1284
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
test("fuseDay clears a stale fused artifact when a later run skips on conflicting timezones", async () => {
|
|
1289
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1290
|
+
try {
|
|
1291
|
+
// First, both sources share one timezone -> the day fuses successfully.
|
|
1292
|
+
storeDay(
|
|
1293
|
+
storage,
|
|
1294
|
+
"limitless",
|
|
1295
|
+
"2026-06-10",
|
|
1296
|
+
["We agreed to ship Friday."],
|
|
1297
|
+
"America/Los_Angeles",
|
|
1298
|
+
);
|
|
1299
|
+
storeDay(
|
|
1300
|
+
storage,
|
|
1301
|
+
"bee",
|
|
1302
|
+
"2026-06-10",
|
|
1303
|
+
["We agreed to ship Friday."],
|
|
1304
|
+
"America/Los_Angeles",
|
|
1305
|
+
);
|
|
1306
|
+
const service = makeService(storage, {
|
|
1307
|
+
sources: {
|
|
1308
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1309
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1310
|
+
},
|
|
1311
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const first = await service.fuseDay("2026-06-10");
|
|
1315
|
+
assert.equal(first.written, true);
|
|
1316
|
+
assert.ok(first.conversationCount >= 1, "same-timezone sources fuse");
|
|
1317
|
+
// A fused artifact now exists and is served by the listing surface.
|
|
1318
|
+
assert.notEqual(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1319
|
+
assert.ok((await service.fusedConversations("2026-06-10")).length >= 1);
|
|
1320
|
+
|
|
1321
|
+
// Later sync: bee's source is re-rendered under a DIFFERENT timezone
|
|
1322
|
+
// (Asia/Tokyo, UTC+9), conflicting with limitless (UTC-7 in summer).
|
|
1323
|
+
storeDay(
|
|
1324
|
+
storage,
|
|
1325
|
+
"bee",
|
|
1326
|
+
"2026-06-10",
|
|
1327
|
+
["We agreed to ship Friday."],
|
|
1328
|
+
"Asia/Tokyo",
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
// Re-fuse: the day is now skipped, and the stale artifact MUST be
|
|
1332
|
+
// cleared so fusedConversations() stops serving it (issue #1849).
|
|
1333
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1334
|
+
assert.equal(result.written, false);
|
|
1335
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1336
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1337
|
+
assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
|
|
1338
|
+
} finally {
|
|
1339
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
test("fuseDay skips sources that share a UTC offset but differ in timezone id", async () => {
|
|
1344
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1345
|
+
try {
|
|
1346
|
+
// America/Los_Angeles and America/Tijuana are both UTC-7 on this
|
|
1347
|
+
// summer date, so the OLD noon-offset guard fused them. Under the
|
|
1348
|
+
// tz-identity model their DIFFERENT IANA ids must skip instead:
|
|
1349
|
+
// reconstructFusionInputs only compares local HH:MM clocks safely
|
|
1350
|
+
// when every source carries one explicit, identical tz id.
|
|
1351
|
+
storeDay(
|
|
1352
|
+
storage,
|
|
1353
|
+
"limitless",
|
|
1354
|
+
"2026-06-10",
|
|
1355
|
+
["We agreed to ship Friday."],
|
|
1356
|
+
"America/Los_Angeles",
|
|
1357
|
+
);
|
|
1358
|
+
storeDay(
|
|
1359
|
+
storage,
|
|
1360
|
+
"bee",
|
|
1361
|
+
"2026-06-10",
|
|
1362
|
+
["We agreed to ship Friday."],
|
|
1363
|
+
"America/Tijuana",
|
|
1364
|
+
);
|
|
1365
|
+
const service = makeService(storage, {
|
|
1366
|
+
sources: {
|
|
1367
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1368
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1369
|
+
},
|
|
1370
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1374
|
+
assert.equal(result.written, false, "differing tz ids skip even at equal offset");
|
|
1375
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1376
|
+
assert.equal(result.sources.length, 0);
|
|
1377
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1378
|
+
assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
|
|
1379
|
+
} finally {
|
|
1380
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test("fuseDay skips a DST-date pair whose noon offsets coincide (LA vs Phoenix on 2026-03-08)", async () => {
|
|
1385
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1386
|
+
try {
|
|
1387
|
+
// Regression for the codex finding: on 2026-03-08 America/Los_Angeles
|
|
1388
|
+
// springs forward (UTC-8 -> UTC-7) at 02:00 while America/Phoenix
|
|
1389
|
+
// stays UTC-7 all day. Both zones are UTC-7 by local NOON, so the old
|
|
1390
|
+
// single-offset guard fused the day — yet recordings before LA's
|
|
1391
|
+
// switch sat an hour apart and were misaligned on the fused timeline.
|
|
1392
|
+
// Exact tz-id identity now refuses this pair up front.
|
|
1393
|
+
storeDay(
|
|
1394
|
+
storage,
|
|
1395
|
+
"limitless",
|
|
1396
|
+
"2026-03-08",
|
|
1397
|
+
["Morning standup before the DST switch."],
|
|
1398
|
+
"America/Los_Angeles",
|
|
1399
|
+
);
|
|
1400
|
+
storeDay(
|
|
1401
|
+
storage,
|
|
1402
|
+
"bee",
|
|
1403
|
+
"2026-03-08",
|
|
1404
|
+
["Morning standup before the DST switch."],
|
|
1405
|
+
"America/Phoenix",
|
|
1406
|
+
);
|
|
1407
|
+
const service = makeService(storage, {
|
|
1408
|
+
sources: {
|
|
1409
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1410
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1411
|
+
},
|
|
1412
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const result = await service.fuseDay("2026-03-08");
|
|
1416
|
+
assert.equal(result.written, false, "DST-date coincident-offset pair skips");
|
|
1417
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1418
|
+
assert.equal(result.sources.length, 0);
|
|
1419
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-03-08"), null);
|
|
1420
|
+
assert.equal((await service.fusedConversations("2026-03-08")).length, 0);
|
|
1421
|
+
} finally {
|
|
1422
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test("fuseDay skips when a source lacks a resolvable timezone id", async () => {
|
|
1427
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1428
|
+
try {
|
|
1429
|
+
// limitless carries an explicit tz id; bee's transcript is missing the
|
|
1430
|
+
// timezone field entirely. The guard must NOT coerce the missing id to
|
|
1431
|
+
// a default and silently match — any unresolvable tz fails safe.
|
|
1432
|
+
storeDay(
|
|
1433
|
+
storage,
|
|
1434
|
+
"limitless",
|
|
1435
|
+
"2026-06-10",
|
|
1436
|
+
["We agreed to ship Friday."],
|
|
1437
|
+
"America/Los_Angeles",
|
|
1438
|
+
);
|
|
1439
|
+
// Build bee's transcript the normal way, then strip the timezone line
|
|
1440
|
+
// so the persisted frontmatter has no resolvable tz id.
|
|
1441
|
+
const registry = emptySpeakerRegistry();
|
|
1442
|
+
const conversations: WearableConversation[] = [
|
|
1443
|
+
{
|
|
1444
|
+
id: "bee-2026-06-10",
|
|
1445
|
+
source: "bee",
|
|
1446
|
+
title: "Stored conversation",
|
|
1447
|
+
startIso: "2026-06-10T10:00:00.000Z",
|
|
1448
|
+
endIso: "2026-06-10T10:30:00.000Z",
|
|
1449
|
+
segments: [{ speakerKey: "user", isWearer: true, text: "We agreed to ship Friday." }],
|
|
1450
|
+
},
|
|
1451
|
+
];
|
|
1452
|
+
const beeBody = composeDayTranscriptBody(
|
|
1453
|
+
"bee",
|
|
1454
|
+
"2026-06-10",
|
|
1455
|
+
"UTC",
|
|
1456
|
+
conversations,
|
|
1457
|
+
registry,
|
|
1458
|
+
);
|
|
1459
|
+
const beeMeta = composeDayTranscriptMeta(
|
|
1460
|
+
"bee",
|
|
1461
|
+
"2026-06-10",
|
|
1462
|
+
"UTC",
|
|
1463
|
+
conversations,
|
|
1464
|
+
registry,
|
|
1465
|
+
beeBody,
|
|
1466
|
+
"2026-06-11T01:00:00.000Z",
|
|
1467
|
+
);
|
|
1468
|
+
const beeRaw = serializeDayTranscript(beeMeta, beeBody).replace(/^timezone: .*\n/m, "");
|
|
1469
|
+
storage.files.set("bee/2026-06-10", beeRaw);
|
|
1470
|
+
|
|
1471
|
+
const service = makeService(storage, {
|
|
1472
|
+
sources: {
|
|
1473
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1474
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1475
|
+
},
|
|
1476
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1480
|
+
assert.equal(result.written, false, "a missing tz id fails safe");
|
|
1481
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1482
|
+
assert.equal(result.sources.length, 0);
|
|
1483
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1484
|
+
assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
|
|
1485
|
+
} finally {
|
|
1486
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
test("fuseDay ignores a zero-conversation source when checking timezone identity (issue #1849)", async () => {
|
|
1491
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1492
|
+
try {
|
|
1493
|
+
// limitless CONTRIBUTES a real conversation under one timezone.
|
|
1494
|
+
storeDay(
|
|
1495
|
+
storage,
|
|
1496
|
+
"limitless",
|
|
1497
|
+
"2026-06-10",
|
|
1498
|
+
["We agreed to ship Friday."],
|
|
1499
|
+
"America/Los_Angeles",
|
|
1500
|
+
);
|
|
1501
|
+
// bee is an EMPTY (all-elided / zero-conversation) transcript — the
|
|
1502
|
+
// sync path writes one explicitly for days whose segments were all
|
|
1503
|
+
// dropped. It carries a DIFFERENT timezone, but contributes NO
|
|
1504
|
+
// conversations/clocks, so it must not block the fusion.
|
|
1505
|
+
const registry = emptySpeakerRegistry();
|
|
1506
|
+
const beeBody = composeDayTranscriptBody(
|
|
1507
|
+
"bee",
|
|
1508
|
+
"2026-06-10",
|
|
1509
|
+
"Asia/Tokyo",
|
|
1510
|
+
[],
|
|
1511
|
+
registry,
|
|
1512
|
+
);
|
|
1513
|
+
const beeMeta = composeDayTranscriptMeta(
|
|
1514
|
+
"bee",
|
|
1515
|
+
"2026-06-10",
|
|
1516
|
+
"Asia/Tokyo",
|
|
1517
|
+
[],
|
|
1518
|
+
registry,
|
|
1519
|
+
beeBody,
|
|
1520
|
+
"2026-06-11T01:00:00.000Z",
|
|
1521
|
+
);
|
|
1522
|
+
storage.files.set("bee/2026-06-10", serializeDayTranscript(beeMeta, beeBody));
|
|
1523
|
+
|
|
1524
|
+
const service = makeService(storage, {
|
|
1525
|
+
sources: {
|
|
1526
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1527
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1528
|
+
},
|
|
1529
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1533
|
+
// The zero-conversation source is ignored; the single contributing
|
|
1534
|
+
// source fuses normally (NOT skipped).
|
|
1535
|
+
assert.equal(result.written, true, "a zero-conversation source must not block fusion");
|
|
1536
|
+
assert.equal(result.skipped, undefined);
|
|
1537
|
+
assert.ok(result.conversationCount >= 1, "the contributing source still fuses");
|
|
1538
|
+
assert.deepEqual([...result.sources].sort(), ["limitless"]);
|
|
1539
|
+
} finally {
|
|
1540
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
test("fuseDay still skips when two CONTRIBUTING sources disagree on timezone (issue #1849)", async () => {
|
|
1545
|
+
const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
|
|
1546
|
+
try {
|
|
1547
|
+
// Both sources CONTRIBUTE a real conversation but under DIFFERENT
|
|
1548
|
+
// timezones — the guard must still skip. This is the contrasting case
|
|
1549
|
+
// to ignoring zero-conversation sources: only sources that actually
|
|
1550
|
+
// contribute clocks are compared, and two disagreeing contributors skip.
|
|
1551
|
+
storeDay(
|
|
1552
|
+
storage,
|
|
1553
|
+
"limitless",
|
|
1554
|
+
"2026-06-10",
|
|
1555
|
+
["We agreed to ship Friday."],
|
|
1556
|
+
"America/Los_Angeles",
|
|
1557
|
+
);
|
|
1558
|
+
storeDay(
|
|
1559
|
+
storage,
|
|
1560
|
+
"bee",
|
|
1561
|
+
"2026-06-10",
|
|
1562
|
+
["We agreed to ship Friday."],
|
|
1563
|
+
"Asia/Tokyo",
|
|
1564
|
+
);
|
|
1565
|
+
const service = makeService(storage, {
|
|
1566
|
+
sources: {
|
|
1567
|
+
limitless: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1568
|
+
bee: { ...defaultWearableSourceSettings(), enabled: true },
|
|
1569
|
+
},
|
|
1570
|
+
fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
const result = await service.fuseDay("2026-06-10");
|
|
1574
|
+
assert.equal(result.written, false, "two disagreeing contributing sources must skip");
|
|
1575
|
+
assert.equal(result.skipped?.reason, "conflicting-timezones");
|
|
1576
|
+
assert.equal(result.sources.length, 0);
|
|
1577
|
+
assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
|
|
1578
|
+
assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
|
|
1579
|
+
} finally {
|
|
1580
|
+
rmSync(storage.dir, { recursive: true, force: true });
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
test("dayTranscript returns decoded segment text, not the escaped storage form (#1849)", async () => {
|
|
1586
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
|
|
1587
|
+
try {
|
|
1588
|
+
const storage = makeStorage(dir);
|
|
1589
|
+
// Segment text carries a real newline + a literal backslash. The stored
|
|
1590
|
+
// file escapes both; the user-facing view must decode them back.
|
|
1591
|
+
const original = "Line one.\nLine two with a backslash \\ path.";
|
|
1592
|
+
storeDay(storage, "limitless", "2026-06-10", [original]);
|
|
1593
|
+
const service = makeService(storage);
|
|
1594
|
+
const views = await service.dayTranscript("2026-06-10");
|
|
1595
|
+
assert.equal(views.length, 1);
|
|
1596
|
+
assert.ok(views[0].body.includes(original), "view shows the original (decoded) text");
|
|
1597
|
+
assert.ok(
|
|
1598
|
+
views[0].body.includes("Line one.\\nLine two") === false,
|
|
1599
|
+
"no escaped-newline leak in the user-facing view",
|
|
1600
|
+
);
|
|
1601
|
+
} finally {
|
|
1602
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
test("searchTranscripts scan decodes segment text so snippets show the original (#1849)", async () => {
|
|
1607
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
|
|
1608
|
+
try {
|
|
1609
|
+
const storage = makeStorage(dir);
|
|
1610
|
+
// Segment text spans a newline; the escaped storage form would leak a
|
|
1611
|
+
// literal backslash-n into the snippet unless the scan decodes first.
|
|
1612
|
+
storeDay(storage, "limitless", "2026-06-10", ["Alpha part.\nBeta GAPWORD here."]);
|
|
1613
|
+
const service = makeService(storage);
|
|
1614
|
+
const results = await service.searchTranscripts("GAPWORD");
|
|
1615
|
+
assert.equal(results.length, 1);
|
|
1616
|
+
assert.equal(results[0].backend, "scan");
|
|
1617
|
+
assert.ok(results[0].snippet.includes("GAPWORD"));
|
|
1618
|
+
assert.ok(
|
|
1619
|
+
results[0].snippet.includes("Alpha part.\\nBeta") === false,
|
|
1620
|
+
"snippet has no escaped-newline leak",
|
|
1621
|
+
);
|
|
1622
|
+
} finally {
|
|
1623
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1624
|
+
}
|
|
1625
|
+
});
|