@remnic/core 9.22.0 → 9.22.1
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 +15 -12
- package/dist/access-admin-ops-surface.js +25 -24
- package/dist/access-audit.js +2 -2
- package/dist/access-boundary.d.ts +13 -10
- package/dist/access-boundary.js +25 -24
- package/dist/access-cli.js +84 -82
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +16 -9
- package/dist/access-http.js +33 -32
- package/dist/access-identity-continuity-surface.d.ts +9 -7
- package/dist/access-identity-continuity-surface.js +25 -24
- package/dist/access-lcm-surface.d.ts +12 -9
- package/dist/access-lcm-surface.js +25 -24
- package/dist/access-mcp-output-schemas.js +1 -1
- package/dist/access-mcp.d.ts +22 -10
- package/dist/access-mcp.js +31 -30
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-tmwQgCT6.d.ts → access-namespace-preflight-CmErR03h.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +25 -24
- package/dist/access-observe-write-surface.d.ts +12 -9
- package/dist/access-observe-write-surface.js +25 -24
- package/dist/access-operations-batch.js +27 -26
- package/dist/access-operations.d.ts +13 -10
- package/dist/access-operations.js +28 -27
- package/dist/access-recall-concurrency.d.ts +12 -9
- package/dist/access-recall-concurrency.js +25 -24
- package/dist/access-recall-response.d.ts +12 -9
- package/dist/access-recall-response.js +25 -24
- package/dist/access-recall-surface.d.ts +14 -11
- package/dist/access-recall-surface.js +25 -24
- package/dist/{access-service-BtxW52vv.d.ts → access-service-CKEROeFP.d.ts} +22 -11
- package/dist/access-service.d.ts +13 -10
- package/dist/access-service.js +25 -24
- package/dist/access-surface-catalog.d.ts +12 -9
- package/dist/access-surface-catalog.js +6 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +78 -0
- package/dist/access-wearables-meetings-surface.js +22 -0
- package/dist/access-wearables-meetings-surface.js.map +1 -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/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +9 -7
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-iWvWz61I.d.ts → catalog-BFTVo2Ez.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +9 -9
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-GF22HWS2.js → chunk-2PHX3I5C.js} +7 -7
- package/dist/{chunk-JFDEYXF7.js → chunk-3NAHZQU3.js} +2 -2
- package/dist/{chunk-XBRPEGXF.js → chunk-3UOSVTFR.js} +5 -5
- package/dist/{chunk-TTKWTT72.js → chunk-3ZW6ZQO6.js} +3 -3
- package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
- package/dist/{chunk-IC2TCZJ2.js → chunk-57QKLRBV.js} +3 -1
- package/dist/chunk-57QKLRBV.js.map +1 -0
- package/dist/{chunk-LMHDFOVV.js → chunk-5CPI2QWG.js} +3 -3
- package/dist/{chunk-P72WZMWQ.js → chunk-5FZQ74K5.js} +2 -2
- package/dist/{chunk-BRT7KEXU.js → chunk-6TWXQ4E2.js} +5 -5
- package/dist/{chunk-CV6ZYXK3.js → chunk-7I7AYIUQ.js} +3 -3
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-MONWCSVL.js → chunk-7RAJ3GXO.js} +2 -1
- package/dist/chunk-7RAJ3GXO.js.map +1 -0
- package/dist/{chunk-SQEDDWAM.js → chunk-A2RFKL3V.js} +5 -34
- package/dist/chunk-A2RFKL3V.js.map +1 -0
- package/dist/{chunk-JN57HZCD.js → chunk-ATEAL5EI.js} +6 -6
- package/dist/{chunk-4PYLBXX6.js → chunk-AYAROCWD.js} +2 -2
- package/dist/chunk-BLDBT6HZ.js +39 -0
- package/dist/chunk-BLDBT6HZ.js.map +1 -0
- package/dist/{chunk-NB2P47N7.js → chunk-CUUY5CC4.js} +6 -6
- package/dist/{chunk-KL5Y6PEV.js → chunk-E3ZMFFQZ.js} +2 -2
- package/dist/{chunk-55FDQYPS.js → chunk-ES5IHQ6M.js} +2 -2
- package/dist/{chunk-N47ZWQBO.js → chunk-G2NSSQES.js} +2 -2
- package/dist/{chunk-GSWPU5CR.js → chunk-GD3QOBMB.js} +2 -2
- package/dist/{chunk-6ANPNUVL.js → chunk-IIXLB5E7.js} +58 -36
- package/dist/chunk-IIXLB5E7.js.map +1 -0
- package/dist/{chunk-SACPRJ7F.js → chunk-JNIJBXVK.js} +2 -2
- package/dist/{chunk-W3XT32W7.js → chunk-JVGUY2AU.js} +23 -13
- package/dist/chunk-JVGUY2AU.js.map +1 -0
- package/dist/{chunk-DGLON3AL.js → chunk-KNRG6ZGJ.js} +6 -6
- package/dist/{chunk-GEUDRENF.js → chunk-MMTG3SCJ.js} +2 -2
- package/dist/{chunk-WZFAPHIK.js → chunk-MU7ZHSZE.js} +2 -2
- package/dist/{chunk-W4FBGYGK.js → chunk-NRV4Y3PM.js} +1 -1
- package/dist/{chunk-QTO2FGBT.js → chunk-OUW4PAY4.js} +191 -117
- package/dist/chunk-OUW4PAY4.js.map +1 -0
- package/dist/{chunk-KQ2XCQYN.js → chunk-P2BLHUL2.js} +2 -2
- package/dist/{chunk-ACGGTPU4.js → chunk-QCKQEBIY.js} +17 -9
- package/dist/chunk-QCKQEBIY.js.map +1 -0
- package/dist/{chunk-6L775BUB.js → chunk-QE63OHLB.js} +2 -2
- package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
- package/dist/{chunk-EQYBHXRR.js → chunk-ROUL7RZU.js} +385 -59
- package/dist/chunk-ROUL7RZU.js.map +1 -0
- package/dist/{chunk-N6M7MRVN.js → chunk-RT4TSR2C.js} +9034 -8726
- package/dist/chunk-RT4TSR2C.js.map +1 -0
- package/dist/{chunk-6NZ7U4R4.js → chunk-SCOT5DTA.js} +1 -1
- package/dist/chunk-SCOT5DTA.js.map +1 -0
- package/dist/{chunk-6RYWNBSA.js → chunk-SVZGA2MK.js} +2 -2
- package/dist/chunk-SYOIVK7K.js +63 -0
- package/dist/chunk-SYOIVK7K.js.map +1 -0
- package/dist/{chunk-DM5A4ZFL.js → chunk-TWCOYET2.js} +11 -1
- package/dist/chunk-TWCOYET2.js.map +1 -0
- package/dist/{chunk-7S5OMI76.js → chunk-WS32QRLO.js} +109 -44
- package/dist/chunk-WS32QRLO.js.map +1 -0
- package/dist/{chunk-OCMLGND7.js → chunk-WWZ35GWW.js} +5 -5
- package/dist/{chunk-QMGIQUBG.js → chunk-XWPFZX6S.js} +503 -139
- package/dist/chunk-XWPFZX6S.js.map +1 -0
- package/dist/{chunk-A5N6ISRK.js → chunk-ZN4UZWMT.js} +2 -2
- package/dist/{chunk-7M6APKJL.js → chunk-ZYU7ZFLT.js} +2 -2
- package/dist/{cli-D9vXgWwM.d.ts → cli-BrwT9YMD.d.ts} +5 -5
- package/dist/cli.d.ts +14 -11
- package/dist/cli.js +57 -56
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +5 -5
- 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 +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +5 -5
- 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/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +9 -7
- package/dist/extraction-error-classification.d.ts +22 -0
- package/dist/extraction-error-classification.js +11 -0
- package/dist/extraction-error-classification.js.map +1 -0
- 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 +3 -17
- package/dist/extraction.js +5 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-LEXL3XYL.js → first-start-migration-BKNGOFED.js} +4 -4
- 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/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +309 -519
- package/dist/index.js +176 -140
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +11 -11
- 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 +6 -6
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +7 -7
- package/dist/maintenance-C98njYb8.d.ts +366 -0
- package/dist/mcp-memory-inspector-app.d.ts +12 -9
- package/dist/mcp-read-only-tools.js +1 -1
- 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 +12 -12
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +6 -223
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-DB93S48I.d.ts → orchestrator-C9ZVppNh.d.ts} +12 -481
- package/dist/orchestrator.d.ts +10 -8
- package/dist/orchestrator.js +84 -82
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-DmGc-H66.d.ts → pipeline-id_-sqYm.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-cJT0crkF.d.ts → qmd-R6XaQOZK.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/qmd.js +4 -4
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.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-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.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/salvage-envelope.d.ts +1 -1
- package/dist/{scope-profiles-KG2nODX7.d.ts → scope-profiles-EhyuAKkd.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +5 -5
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +9 -9
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BaZlg0D8.d.ts → semantic-consolidation-B2xo26UQ.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +7 -7
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +6 -6
- package/dist/service-DgFWlZ-r.d.ts +835 -0
- 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-BKhHHnRo.d.ts → storage-XGawwqXE.d.ts} +131 -2
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +4 -4
- 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-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-GBQZuT7l.d.ts → types-Bbs0qz6b.d.ts} +2 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +6 -6
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/access-boundary.ts +3 -0
- package/src/access-http.test.ts +63 -0
- package/src/access-http.ts +26 -37
- package/src/access-mcp-output-schemas.ts +10 -0
- package/src/access-mcp.ts +4 -121
- package/src/access-operations-batch.ts +10 -5
- package/src/access-service.ts +43 -46
- package/src/access-surface-catalog.ts +6 -0
- package/src/access-wearables-meetings-surface.ts +139 -0
- package/src/activity/digest.ts +1 -1
- package/src/activity/runner.test.ts +37 -0
- package/src/activity/runner.ts +12 -0
- package/src/activity/scheduler.test.ts +1 -0
- package/src/activity/sync-registration.ts +13 -0
- package/src/cli/meetings-commands.ts +70 -0
- package/src/cli.ts +7 -6
- package/src/extraction-error-classification.ts +52 -0
- package/src/extraction.ts +2 -50
- package/src/mcp-read-only-tools.ts +2 -0
- package/src/meetings/build-memory.test.ts +364 -0
- package/src/meetings/build-scheduler.test.ts +55 -0
- package/src/meetings/build-scheduler.ts +96 -0
- package/src/meetings/day-source.test.ts +275 -0
- package/src/meetings/day-source.ts +352 -0
- package/src/meetings/http-glue.ts +126 -0
- package/src/meetings/index.ts +5 -4
- package/src/meetings/manual-sync-flush.test.ts +72 -0
- package/src/meetings/mcp-tools.ts +65 -0
- package/src/meetings/memory-gen.test.ts +498 -0
- package/src/meetings/memory-gen.ts +549 -0
- package/src/meetings/memory-generator.ts +5 -0
- package/src/meetings/namespace-isolation.test.ts +159 -0
- package/src/meetings/service.test.ts +195 -0
- package/src/meetings/service.ts +157 -0
- package/src/meetings/summary-extractor.test.ts +287 -0
- package/src/meetings/summary-extractor.ts +264 -0
- package/src/orchestration/maintenance.ts +8 -0
- package/src/orchestration/orchestrator-init.ts +6 -1
- package/src/orchestration/workspace-ops.ts +172 -8
- package/src/orchestrator.ts +12 -13
- package/src/storage.ts +21 -0
- package/src/testing/subjects/meetings-namespace-harness.ts +159 -0
- package/src/testing/subjects/meetings-namespace.test.ts +93 -0
- package/src/types.ts +1 -2
- package/src/wearables/cli.ts +2 -1
- package/src/wearables/index.ts +1 -0
- package/src/wearables/mcp-tools.ts +143 -0
- package/src/wearables/registry.test.ts +11 -0
- package/src/wearables/registry.ts +13 -0
- package/src/wearables/service.test.ts +62 -1
- package/src/wearables/service.ts +30 -2
- package/src/wearables/transcript-search-namespace-isolation.test.ts +98 -0
- package/dist/chunk-6ANPNUVL.js.map +0 -1
- package/dist/chunk-6NZ7U4R4.js.map +0 -1
- package/dist/chunk-7S5OMI76.js.map +0 -1
- package/dist/chunk-ACGGTPU4.js.map +0 -1
- package/dist/chunk-DM5A4ZFL.js.map +0 -1
- package/dist/chunk-EQYBHXRR.js.map +0 -1
- package/dist/chunk-IC2TCZJ2.js.map +0 -1
- package/dist/chunk-MONWCSVL.js.map +0 -1
- package/dist/chunk-N6M7MRVN.js.map +0 -1
- package/dist/chunk-QMGIQUBG.js.map +0 -1
- package/dist/chunk-QTO2FGBT.js.map +0 -1
- package/dist/chunk-SQEDDWAM.js.map +0 -1
- package/dist/chunk-W3XT32W7.js.map +0 -1
- /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
- /package/dist/{chunk-GF22HWS2.js.map → chunk-2PHX3I5C.js.map} +0 -0
- /package/dist/{chunk-JFDEYXF7.js.map → chunk-3NAHZQU3.js.map} +0 -0
- /package/dist/{chunk-XBRPEGXF.js.map → chunk-3UOSVTFR.js.map} +0 -0
- /package/dist/{chunk-TTKWTT72.js.map → chunk-3ZW6ZQO6.js.map} +0 -0
- /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
- /package/dist/{chunk-LMHDFOVV.js.map → chunk-5CPI2QWG.js.map} +0 -0
- /package/dist/{chunk-P72WZMWQ.js.map → chunk-5FZQ74K5.js.map} +0 -0
- /package/dist/{chunk-BRT7KEXU.js.map → chunk-6TWXQ4E2.js.map} +0 -0
- /package/dist/{chunk-CV6ZYXK3.js.map → chunk-7I7AYIUQ.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-JN57HZCD.js.map → chunk-ATEAL5EI.js.map} +0 -0
- /package/dist/{chunk-4PYLBXX6.js.map → chunk-AYAROCWD.js.map} +0 -0
- /package/dist/{chunk-NB2P47N7.js.map → chunk-CUUY5CC4.js.map} +0 -0
- /package/dist/{chunk-KL5Y6PEV.js.map → chunk-E3ZMFFQZ.js.map} +0 -0
- /package/dist/{chunk-55FDQYPS.js.map → chunk-ES5IHQ6M.js.map} +0 -0
- /package/dist/{chunk-N47ZWQBO.js.map → chunk-G2NSSQES.js.map} +0 -0
- /package/dist/{chunk-GSWPU5CR.js.map → chunk-GD3QOBMB.js.map} +0 -0
- /package/dist/{chunk-SACPRJ7F.js.map → chunk-JNIJBXVK.js.map} +0 -0
- /package/dist/{chunk-DGLON3AL.js.map → chunk-KNRG6ZGJ.js.map} +0 -0
- /package/dist/{chunk-GEUDRENF.js.map → chunk-MMTG3SCJ.js.map} +0 -0
- /package/dist/{chunk-WZFAPHIK.js.map → chunk-MU7ZHSZE.js.map} +0 -0
- /package/dist/{chunk-W4FBGYGK.js.map → chunk-NRV4Y3PM.js.map} +0 -0
- /package/dist/{chunk-KQ2XCQYN.js.map → chunk-P2BLHUL2.js.map} +0 -0
- /package/dist/{chunk-6L775BUB.js.map → chunk-QE63OHLB.js.map} +0 -0
- /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
- /package/dist/{chunk-6RYWNBSA.js.map → chunk-SVZGA2MK.js.map} +0 -0
- /package/dist/{chunk-OCMLGND7.js.map → chunk-WWZ35GWW.js.map} +0 -0
- /package/dist/{chunk-A5N6ISRK.js.map → chunk-ZN4UZWMT.js.map} +0 -0
- /package/dist/{chunk-7M6APKJL.js.map → chunk-ZYU7ZFLT.js.map} +0 -0
- /package/dist/{first-start-migration-LEXL3XYL.js.map → first-start-migration-BKNGOFED.js.map} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import type { ActivitySnapshot } from "../activity/types.js";
|
|
5
|
+
import type { FusionConversationInput } from "../wearables/fusion/types.js";
|
|
6
|
+
import {
|
|
7
|
+
ActivityWearablesMeetingsDaySource,
|
|
8
|
+
buildAudioWindows,
|
|
9
|
+
deriveAppSpans,
|
|
10
|
+
type MeetingsActivityReader,
|
|
11
|
+
type MeetingsWearableReader,
|
|
12
|
+
} from "./day-source.js";
|
|
13
|
+
import { MeetingsInputError } from "./errors.js";
|
|
14
|
+
import { detectMeetings } from "./detect.js";
|
|
15
|
+
import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
|
|
16
|
+
import type { MeetingsConfig } from "./types.js";
|
|
17
|
+
|
|
18
|
+
const DATE = "2026-03-10";
|
|
19
|
+
|
|
20
|
+
function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
|
|
21
|
+
return {
|
|
22
|
+
...DEFAULT_MEETINGS_CONFIG,
|
|
23
|
+
appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns],
|
|
24
|
+
enabled: true,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function snap(overrides: Partial<ActivitySnapshot> = {}): ActivitySnapshot {
|
|
30
|
+
return {
|
|
31
|
+
machine: "workstation-a",
|
|
32
|
+
capturedAtUtc: "2026-03-10T14:00:00.000Z",
|
|
33
|
+
app: "Chrome",
|
|
34
|
+
windowTitle: "",
|
|
35
|
+
text: "",
|
|
36
|
+
textSource: "ax",
|
|
37
|
+
contentHash: Math.random().toString(36).slice(2),
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function conv(overrides: Partial<FusionConversationInput> = {}): FusionConversationInput {
|
|
43
|
+
return {
|
|
44
|
+
source: "desktop",
|
|
45
|
+
conversationId: "c1",
|
|
46
|
+
startIso: "2026-03-10T14:00:00.000Z",
|
|
47
|
+
segments: [],
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("deriveAppSpans groups consecutive matching snapshots and breaks on a non-match", () => {
|
|
53
|
+
const spans = deriveAppSpans(
|
|
54
|
+
[
|
|
55
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
|
|
56
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:01:00.000Z" }),
|
|
57
|
+
snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:02:00.000Z" }), // breaks the run
|
|
58
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:03:00.000Z" }),
|
|
59
|
+
],
|
|
60
|
+
config().appPatterns,
|
|
61
|
+
);
|
|
62
|
+
assert.equal(spans.length, 2, "the non-matching snapshot splits the run into two spans");
|
|
63
|
+
assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:02:00.000Z" }, "the run ends at the next (non-matching) snapshot that bounds it");
|
|
64
|
+
assert.deepEqual(spans[1], { app: "Zoom", startUtc: "2026-03-10T14:03:00.000Z", endUtc: "2026-03-10T14:04:00.000Z" }, "a lone trailing run gets a finite end at the machine's cadence, not a zero-length point");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("deriveAppSpans matches a meeting URL in the browser url, not just the app name", () => {
|
|
68
|
+
const spans = deriveAppSpans(
|
|
69
|
+
[
|
|
70
|
+
snap({ app: "Chrome", browserUrl: "https://meet.google.com/abc-defg-hij", capturedAtUtc: "2026-03-10T09:00:00.000Z" }),
|
|
71
|
+
snap({ app: "Chrome", browserUrl: "https://meet.google.com/abc-defg-hij", capturedAtUtc: "2026-03-10T09:05:00.000Z" }),
|
|
72
|
+
],
|
|
73
|
+
config().appPatterns,
|
|
74
|
+
);
|
|
75
|
+
assert.equal(spans.length, 1);
|
|
76
|
+
assert.equal(spans[0]?.startUtc, "2026-03-10T09:00:00.000Z");
|
|
77
|
+
assert.equal(spans[0]?.endUtc, "2026-03-10T09:05:00.000Z");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("deriveAppSpans returns nothing when there are no patterns", () => {
|
|
81
|
+
assert.deepEqual(deriveAppSpans([snap({ app: "Zoom" })], []), []);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("buildAudioWindows counts distinct non-wearer speakers and derives the end", () => {
|
|
85
|
+
const windows = buildAudioWindows([
|
|
86
|
+
conv({
|
|
87
|
+
source: "limitless",
|
|
88
|
+
startIso: "2026-03-10T09:00:00.000Z",
|
|
89
|
+
segments: [
|
|
90
|
+
{ speaker: "Me (you)", isSelf: true, text: "hi", startIso: "2026-03-10T09:00:00.000Z" },
|
|
91
|
+
{ speaker: "Jane", isSelf: false, text: "hello", startIso: "2026-03-10T09:01:00.000Z", endIso: "2026-03-10T09:01:30.000Z" },
|
|
92
|
+
{ speaker: "Bob", isSelf: false, text: "hey", startIso: "2026-03-10T09:02:00.000Z" },
|
|
93
|
+
{ speaker: "Jane", isSelf: false, text: "again", startIso: "2026-03-10T09:03:00.000Z" },
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
assert.equal(windows.length, 1);
|
|
98
|
+
assert.equal(windows[0]?.source, "limitless");
|
|
99
|
+
assert.equal(windows[0]?.distinctNonWearerSpeakers, 2, "Jane + Bob, deduped; the wearer excluded");
|
|
100
|
+
assert.equal(windows[0]?.startUtc, "2026-03-10T09:00:00.000Z");
|
|
101
|
+
assert.equal(windows[0]?.endUtc, "2026-03-10T09:03:00.000Z", "latest segment instant when no conversation endIso");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("buildAudioWindows prefers an explicit conversation endIso", () => {
|
|
105
|
+
const windows = buildAudioWindows([
|
|
106
|
+
conv({ startIso: "2026-03-10T09:00:00.000Z", endIso: "2026-03-10T10:00:00.000Z", segments: [] }),
|
|
107
|
+
]);
|
|
108
|
+
assert.equal(windows[0]?.endUtc, "2026-03-10T10:00:00.000Z");
|
|
109
|
+
assert.equal(windows[0]?.distinctNonWearerSpeakers, 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("loadDayData assembles detection + activity for the requested day, keyed to that day", async () => {
|
|
113
|
+
const activity: MeetingsActivityReader = {
|
|
114
|
+
listSnapshotsForDay: (_machine, startUtc, endUtc) => {
|
|
115
|
+
assert.ok(startUtc < endUtc, "half-open UTC window");
|
|
116
|
+
return [
|
|
117
|
+
snap({ app: "Zoom", windowTitle: "Standup", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
|
|
118
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:05:00.000Z" }),
|
|
119
|
+
];
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const wearables: MeetingsWearableReader = { readDayBodies: async () => [] };
|
|
123
|
+
const source = new ActivityWearablesMeetingsDaySource({ activity, wearables, config: config(), timezone: "UTC" });
|
|
124
|
+
const data = await source.loadDayData(DATE);
|
|
125
|
+
assert.equal(data.detection.date, DATE, "detection is keyed to the requested day (builder asserts this)");
|
|
126
|
+
assert.equal(data.detection.appSpans.length, 1);
|
|
127
|
+
assert.equal(data.detection.appSpans[0]?.app, "Zoom");
|
|
128
|
+
assert.deepEqual(data.detection.audioWindows, [], "no wearable transcripts → no audio windows");
|
|
129
|
+
assert.equal(data.activity?.length, 2);
|
|
130
|
+
assert.equal(data.activity?.[0]?.title, "Standup");
|
|
131
|
+
assert.equal(data.conversations.length, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("loadDayData rejects a malformed day with a clean input error", async () => {
|
|
135
|
+
const source = new ActivityWearablesMeetingsDaySource({
|
|
136
|
+
activity: { listSnapshotsForDay: () => [] },
|
|
137
|
+
wearables: { readDayBodies: async () => [] },
|
|
138
|
+
config: config(),
|
|
139
|
+
timezone: "UTC",
|
|
140
|
+
});
|
|
141
|
+
await assert.rejects(() => Promise.resolve(source.loadDayData("2026-13-40")), MeetingsInputError);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("deriveAppSpans gives a lone matching snapshot a non-zero span so app+audio detection is not missed (issue #1900)", () => {
|
|
145
|
+
const spans = deriveAppSpans(
|
|
146
|
+
[
|
|
147
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }), // single match
|
|
148
|
+
snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:30:00.000Z" }), // ends + bounds it
|
|
149
|
+
],
|
|
150
|
+
config().appPatterns,
|
|
151
|
+
);
|
|
152
|
+
assert.equal(spans.length, 1);
|
|
153
|
+
assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z" });
|
|
154
|
+
assert.ok(
|
|
155
|
+
Date.parse(spans[0]!.endUtc) > Date.parse(spans[0]!.startUtc),
|
|
156
|
+
"the span is non-zero-length, so detect's isFinitePair keeps it",
|
|
157
|
+
);
|
|
158
|
+
// With a real span the overlapping audio window now classifies as app+audio;
|
|
159
|
+
// the old zero-length span was dropped and this returned nothing.
|
|
160
|
+
const detected = detectMeetings({
|
|
161
|
+
date: DATE,
|
|
162
|
+
appSpans: spans,
|
|
163
|
+
audioWindows: [
|
|
164
|
+
{ source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z", distinctNonWearerSpeakers: 1 },
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
assert.equal(detected.length, 1);
|
|
168
|
+
assert.equal(detected[0]?.detectionSource, "app+audio");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("deriveAppSpans derives runs per capture machine so another machine cannot split a call (issue #1900)", () => {
|
|
172
|
+
// Laptop is in Zoom continuously; a desktop ticks Chrome (non-match) in between.
|
|
173
|
+
const spans = deriveAppSpans(
|
|
174
|
+
[
|
|
175
|
+
snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
|
|
176
|
+
snap({ machine: "desktop", app: "Chrome", capturedAtUtc: "2026-03-10T14:01:00.000Z" }),
|
|
177
|
+
snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:02:00.000Z" }),
|
|
178
|
+
snap({ machine: "desktop", app: "Chrome", capturedAtUtc: "2026-03-10T14:03:00.000Z" }),
|
|
179
|
+
snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:04:00.000Z" }),
|
|
180
|
+
],
|
|
181
|
+
config().appPatterns,
|
|
182
|
+
);
|
|
183
|
+
assert.equal(spans.length, 1, "the desktop's Chrome ticks must not fragment the laptop's Zoom run");
|
|
184
|
+
assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:04:00.000Z" });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("loadDayData converts the transcript timezone so audio lines up with UTC screen snapshots (issue #1900)", async () => {
|
|
188
|
+
const body = [
|
|
189
|
+
"# desktop transcript — 2026-03-10",
|
|
190
|
+
"",
|
|
191
|
+
"## 09:00–09:30 · Standup (conversation c1)",
|
|
192
|
+
"",
|
|
193
|
+
"**Me (you)** [09:00]: morning",
|
|
194
|
+
"**Jane** [09:05]: hi",
|
|
195
|
+
"**Bob** [09:10]: hey",
|
|
196
|
+
"",
|
|
197
|
+
].join("\n");
|
|
198
|
+
const activity: MeetingsActivityReader = {
|
|
199
|
+
listSnapshotsForDay: () => [
|
|
200
|
+
snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
|
|
201
|
+
snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:35:00.000Z" }),
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
const wearables: MeetingsWearableReader = {
|
|
205
|
+
readDayBodies: async () => [{ source: "desktop", body, timezone: "America/Chicago" }],
|
|
206
|
+
};
|
|
207
|
+
const source = new ActivityWearablesMeetingsDaySource({
|
|
208
|
+
activity,
|
|
209
|
+
wearables,
|
|
210
|
+
config: config(),
|
|
211
|
+
timezone: "America/Chicago",
|
|
212
|
+
});
|
|
213
|
+
const data = await source.loadDayData(DATE);
|
|
214
|
+
// 09:00 America/Chicago (CDT) is 14:00Z, not 09:00Z: the window now overlaps
|
|
215
|
+
// the 14:00Z Zoom snapshot; before the fix it drifted to 09:00Z and missed it.
|
|
216
|
+
assert.equal(data.detection.audioWindows.length, 1);
|
|
217
|
+
assert.equal(data.detection.audioWindows[0]?.startUtc, "2026-03-10T14:00:00.000Z");
|
|
218
|
+
assert.equal(data.detection.audioWindows[0]?.endUtc, "2026-03-10T14:30:00.000Z");
|
|
219
|
+
assert.equal(data.conversations[0]?.startIso, "2026-03-10T14:00:00.000Z");
|
|
220
|
+
const detected = detectMeetings(data.detection);
|
|
221
|
+
assert.equal(detected.length, 1);
|
|
222
|
+
assert.equal(detected[0]?.detectionSource, "app+audio");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("deriveAppSpans gives a LONE TRAILING matching snapshot a finite span so end-of-window app+audio is not missed (issue #1900)", () => {
|
|
226
|
+
// A single Zoom tick at the end of the machine's snapshots, with NO later
|
|
227
|
+
// non-matching tick to bound it. Before the fix this pushed endUtc==startUtc,
|
|
228
|
+
// a zero-length point detect's isFinitePair dropped — so overlapping audio
|
|
229
|
+
// never paired and no meeting surfaced.
|
|
230
|
+
const spans = deriveAppSpans(
|
|
231
|
+
[snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" })],
|
|
232
|
+
config().appPatterns,
|
|
233
|
+
);
|
|
234
|
+
assert.equal(spans.length, 1);
|
|
235
|
+
assert.equal(spans[0]?.startUtc, "2026-03-10T14:00:00.000Z");
|
|
236
|
+
assert.ok(
|
|
237
|
+
Date.parse(spans[0]!.endUtc) > Date.parse(spans[0]!.startUtc),
|
|
238
|
+
"the trailing run gets a non-zero end so detect's isFinitePair keeps it",
|
|
239
|
+
);
|
|
240
|
+
const detected = detectMeetings(
|
|
241
|
+
{
|
|
242
|
+
date: DATE,
|
|
243
|
+
appSpans: spans,
|
|
244
|
+
audioWindows: [
|
|
245
|
+
{ source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z", distinctNonWearerSpeakers: 1 },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
config(),
|
|
249
|
+
);
|
|
250
|
+
assert.equal(detected.length, 1, "the finite trailing span now pairs with the overlapping audio window");
|
|
251
|
+
assert.equal(detected[0]?.detectionSource, "app+audio");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("buildAudioWindows marks provider sources + carries the title so a titled provider conversation with no app span is detected (issue #1900)", () => {
|
|
255
|
+
const windows = buildAudioWindows([
|
|
256
|
+
conv({
|
|
257
|
+
source: "granola",
|
|
258
|
+
conversationId: "g1",
|
|
259
|
+
title: "Weekly sync",
|
|
260
|
+
startIso: "2026-03-10T16:00:00.000Z",
|
|
261
|
+
endIso: "2026-03-10T16:30:00.000Z",
|
|
262
|
+
segments: [{ speaker: "Jane", isSelf: false, text: "hi", startIso: "2026-03-10T16:05:00.000Z" }],
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
assert.equal(windows.length, 1);
|
|
266
|
+
assert.equal(windows[0]?.providerMeeting, true, "a known provider source is marked so detect's provider branch fires");
|
|
267
|
+
assert.equal(windows[0]?.title, "Weekly sync", "the provider-supplied title is carried onto the window");
|
|
268
|
+
// No app spans and only one non-wearer speaker: the provider branch is the
|
|
269
|
+
// ONLY path to a meeting. Before the fix the window carried neither
|
|
270
|
+
// providerMeeting nor title, so nothing was detected.
|
|
271
|
+
const detected = detectMeetings({ date: DATE, appSpans: [], audioWindows: windows });
|
|
272
|
+
assert.equal(detected.length, 1);
|
|
273
|
+
assert.equal(detected[0]?.detectionSource, "provider");
|
|
274
|
+
assert.equal(detected[0]?.title, "Weekly sync");
|
|
275
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete meetings day source (issue #1900) — the wiring slice that feeds the
|
|
3
|
+
* builder from the ESTABLISHED stores rather than fixtures:
|
|
4
|
+
* - screen activity snapshots (the activity SQLite store), and
|
|
5
|
+
* - wearable day transcripts (the wearable day-store), reconstructed into the
|
|
6
|
+
* shared fusion conversation shape via `reconstructFusionInputs`.
|
|
7
|
+
*
|
|
8
|
+
* All timestamps stay UTC and every day read is a half-open [start, end)
|
|
9
|
+
* window resolved by `activityDayWindow(date, timezone)`, so the local day the
|
|
10
|
+
* caller asked for maps to one deterministic UTC window shared by both signals.
|
|
11
|
+
*
|
|
12
|
+
* Fidelity caveat (inherited, documented): a wearable day transcript is stored
|
|
13
|
+
* RENDERED at minute precision, so `reconstructFusionInputs` rebuilds each clock
|
|
14
|
+
* as `${date}THH:MM:00Z` — the same reconstruction the fusion subsystem uses.
|
|
15
|
+
* App spans come from true-UTC activity instants; the two frames coincide when
|
|
16
|
+
* the activity timezone is UTC (the default). Under a non-UTC timezone the
|
|
17
|
+
* app+audio overlap carries that reconstruction offset, exactly as wearable
|
|
18
|
+
* fusion does; audio-only detection is unaffected. Provider-meeting boundaries
|
|
19
|
+
* (Granola/Fireflies explicit titles) are a connector concern not present in the
|
|
20
|
+
* reconstructed day transcript, so audio windows never fabricate a provider flag.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { activityDayWindow, isValidActivityDate, timezoneOffsetIso } from "../activity/digest.js";
|
|
24
|
+
import type { ActivitySnapshot } from "../activity/types.js";
|
|
25
|
+
import { bodyIsEscaped, parseDayTranscript } from "../wearables/day-store.js";
|
|
26
|
+
import { reconstructFusionInputs } from "../wearables/fusion/index.js";
|
|
27
|
+
import type { FusionConversationInput, FusionSegmentInput } from "../wearables/fusion/types.js";
|
|
28
|
+
import type { MeetingDayData, MeetingsDaySource } from "./build.js";
|
|
29
|
+
import { MeetingsInputError } from "./errors.js";
|
|
30
|
+
import type {
|
|
31
|
+
MeetingActivitySnapshot,
|
|
32
|
+
MeetingAppSpan,
|
|
33
|
+
MeetingAudioWindow,
|
|
34
|
+
MeetingsConfig,
|
|
35
|
+
} from "./types.js";
|
|
36
|
+
|
|
37
|
+
/** Reads the day's screen-activity snapshots. Satisfied by `ActivityStore`. */
|
|
38
|
+
export interface MeetingsActivityReader {
|
|
39
|
+
/** Snapshots whose capture instant is in the half-open [start, end) window. */
|
|
40
|
+
listSnapshotsForDay(
|
|
41
|
+
machine: string | null,
|
|
42
|
+
startUtcInclusive: string,
|
|
43
|
+
endUtcExclusive: string,
|
|
44
|
+
): ActivitySnapshot[] | Promise<ActivitySnapshot[]>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One stored wearable day transcript body, ready for reconstruction. */
|
|
48
|
+
export interface WearableDayBody {
|
|
49
|
+
source: string;
|
|
50
|
+
body: string;
|
|
51
|
+
escaped?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* IANA timezone the body's `HH:MM` clocks were rendered in (from the
|
|
54
|
+
* transcript frontmatter). Empty when the transcript stored no timezone —
|
|
55
|
+
* the clocks are then treated as UTC, matching the fusion identity guard.
|
|
56
|
+
*/
|
|
57
|
+
timezone?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Reads the day's rendered wearable transcript bodies (one per source). */
|
|
61
|
+
export interface MeetingsWearableReader {
|
|
62
|
+
readDayBodies(date: string): Promise<WearableDayBody[]>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Narrow slice of `StorageManager` the wearable reader needs. */
|
|
66
|
+
export interface WearableDayTranscriptStore {
|
|
67
|
+
listWearableTranscriptDays(
|
|
68
|
+
sourceId?: string,
|
|
69
|
+
): Promise<Array<{ source: string; date: string }>>;
|
|
70
|
+
readWearableDayTranscript(sourceId: string, date: string): Promise<string | null>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a wearable reader over a storage manager: discover which sources stored
|
|
75
|
+
* a transcript for the day (store-driven, not config-driven, so a day is read
|
|
76
|
+
* exactly from what exists on disk), then read + parse each body.
|
|
77
|
+
*/
|
|
78
|
+
export function storageWearableDayReader(store: WearableDayTranscriptStore): MeetingsWearableReader {
|
|
79
|
+
return {
|
|
80
|
+
async readDayBodies(date: string): Promise<WearableDayBody[]> {
|
|
81
|
+
const days = await store.listWearableTranscriptDays();
|
|
82
|
+
const sources = [...new Set(days.filter((d) => d.date === date).map((d) => d.source))].sort();
|
|
83
|
+
const bodies: WearableDayBody[] = [];
|
|
84
|
+
for (const source of sources) {
|
|
85
|
+
const raw = await store.readWearableDayTranscript(source, date);
|
|
86
|
+
if (raw === null) continue;
|
|
87
|
+
const parsed = parseDayTranscript(raw);
|
|
88
|
+
bodies.push({
|
|
89
|
+
source,
|
|
90
|
+
body: parsed?.body ?? raw,
|
|
91
|
+
escaped: bodyIsEscaped(parsed?.meta),
|
|
92
|
+
timezone: parsed?.meta.timezone ?? "",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return bodies;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Map an activity snapshot to the redaction-safe screen snapshot fusion sees. */
|
|
101
|
+
function toMeetingActivitySnapshot(snapshot: ActivitySnapshot): MeetingActivitySnapshot {
|
|
102
|
+
const out: MeetingActivitySnapshot = { tsUtc: snapshot.capturedAtUtc, app: snapshot.app };
|
|
103
|
+
if (snapshot.windowTitle.length > 0) out.title = snapshot.windowTitle;
|
|
104
|
+
if (snapshot.browserUrl !== undefined && snapshot.browserUrl.length > 0) out.url = snapshot.browserUrl;
|
|
105
|
+
if (snapshot.text.length > 0) out.text = snapshot.text;
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fallback dwell for a lone trailing meeting-app tick on a machine that
|
|
111
|
+
* captured no other snapshot to estimate a cadence from. Five minutes keeps
|
|
112
|
+
* the inferred span above the default `minOverlapMinutes`, so a single
|
|
113
|
+
* end-of-window tick with overlapping audio still pairs.
|
|
114
|
+
*/
|
|
115
|
+
const LONE_TRAILING_SPAN_MS = 5 * 60_000;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Estimate a machine's capture cadence as the median positive gap between its
|
|
119
|
+
* consecutive (time-sorted) snapshots; falls back to {@link LONE_TRAILING_SPAN_MS}
|
|
120
|
+
* when there is no gap to observe (a single snapshot on the machine).
|
|
121
|
+
*/
|
|
122
|
+
function estimateSamplingIntervalMs(sorted: readonly ActivitySnapshot[]): number {
|
|
123
|
+
const gaps: number[] = [];
|
|
124
|
+
let prevMs = Number.NaN;
|
|
125
|
+
for (const snapshot of sorted) {
|
|
126
|
+
const curMs = Date.parse(snapshot.capturedAtUtc);
|
|
127
|
+
if (Number.isFinite(prevMs) && Number.isFinite(curMs) && curMs > prevMs) gaps.push(curMs - prevMs);
|
|
128
|
+
prevMs = curMs;
|
|
129
|
+
}
|
|
130
|
+
if (gaps.length === 0) return LONE_TRAILING_SPAN_MS;
|
|
131
|
+
gaps.sort((a, b) => a - b);
|
|
132
|
+
const mid = Math.floor(gaps.length / 2);
|
|
133
|
+
const median = gaps.length % 2 === 0 ? (gaps[mid - 1]! + gaps[mid]!) / 2 : gaps[mid]!;
|
|
134
|
+
return Math.round(median);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Derive meeting-app foreground spans from the day's snapshots. A span is a
|
|
139
|
+
* maximal run of consecutive (time-ordered) snapshots on ONE capture machine
|
|
140
|
+
* whose app/title/url matches any configured pattern; the next non-matching
|
|
141
|
+
* snapshot ends the run AND bounds its end (so a lone matching snapshot still
|
|
142
|
+
* yields a non-zero span). A trailing run at the END of a machine's snapshots
|
|
143
|
+
* has no following non-matching tick to bound it, so a lone trailing tick is
|
|
144
|
+
* extended by the machine's sampling cadence rather than collapsing to a
|
|
145
|
+
* zero-length point the detector's `isFinitePair` rejects. Brief app switches
|
|
146
|
+
* during a call surface as adjacent spans and the detector's `mergeGapMinutes`
|
|
147
|
+
* re-merges them.
|
|
148
|
+
*/
|
|
149
|
+
export function deriveAppSpans(
|
|
150
|
+
snapshots: readonly ActivitySnapshot[],
|
|
151
|
+
appPatterns: readonly string[],
|
|
152
|
+
): MeetingAppSpan[] {
|
|
153
|
+
const patterns = appPatterns.map((p) => p.toLowerCase()).filter((p) => p.length > 0);
|
|
154
|
+
if (patterns.length === 0) return [];
|
|
155
|
+
const byCaptureTime = (a: ActivitySnapshot, b: ActivitySnapshot): number => {
|
|
156
|
+
const at = Date.parse(a.capturedAtUtc);
|
|
157
|
+
const bt = Date.parse(b.capturedAtUtc);
|
|
158
|
+
if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return at < bt ? -1 : 1;
|
|
159
|
+
return a.capturedAtUtc < b.capturedAtUtc ? -1 : a.capturedAtUtc > b.capturedAtUtc ? 1 : 0;
|
|
160
|
+
};
|
|
161
|
+
// Group by capture machine: a global time sort otherwise lets a non-matching
|
|
162
|
+
// snapshot from ANOTHER machine (a desktop's Chrome tick) close a run on THIS
|
|
163
|
+
// machine (a laptop in Zoom), splitting one call into fragments. Runs are
|
|
164
|
+
// derived per-machine and merged into one span list.
|
|
165
|
+
const byMachine = new Map<string, ActivitySnapshot[]>();
|
|
166
|
+
for (const snapshot of snapshots) {
|
|
167
|
+
const list = byMachine.get(snapshot.machine);
|
|
168
|
+
if (list === undefined) byMachine.set(snapshot.machine, [snapshot]);
|
|
169
|
+
else list.push(snapshot);
|
|
170
|
+
}
|
|
171
|
+
const spans: MeetingAppSpan[] = [];
|
|
172
|
+
for (const machineSnaps of byMachine.values()) {
|
|
173
|
+
const sorted = [...machineSnaps].sort(byCaptureTime);
|
|
174
|
+
const intervalMs = estimateSamplingIntervalMs(sorted);
|
|
175
|
+
let run: MeetingAppSpan | null = null;
|
|
176
|
+
for (const snapshot of sorted) {
|
|
177
|
+
const hay = `${snapshot.app}\n${snapshot.windowTitle}\n${snapshot.browserUrl ?? ""}`.toLowerCase();
|
|
178
|
+
if (patterns.some((p) => hay.includes(p))) {
|
|
179
|
+
if (run === null) {
|
|
180
|
+
run = { app: snapshot.app, startUtc: snapshot.capturedAtUtc, endUtc: snapshot.capturedAtUtc };
|
|
181
|
+
} else {
|
|
182
|
+
run.endUtc = snapshot.capturedAtUtc;
|
|
183
|
+
}
|
|
184
|
+
} else if (run !== null) {
|
|
185
|
+
// End at this next (non-matching) snapshot rather than the last matching
|
|
186
|
+
// one: a single matching snapshot then yields a real [start, next) span
|
|
187
|
+
// instead of a zero-length point the detector's isFinitePair rejects.
|
|
188
|
+
run.endUtc = snapshot.capturedAtUtc;
|
|
189
|
+
spans.push(run);
|
|
190
|
+
run = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (run !== null) {
|
|
194
|
+
// End-of-array trailing run: with no following non-matching snapshot to
|
|
195
|
+
// bound it, a lone matching tick collapses to a zero-length point that
|
|
196
|
+
// detect's isFinitePair drops. Infer the missing next tick at the
|
|
197
|
+
// machine's sampling cadence so an end-of-window Zoom/Teams tick still
|
|
198
|
+
// yields a finite span that pairs with overlapping audio.
|
|
199
|
+
if (run.endUtc === run.startUtc) {
|
|
200
|
+
const startMs = Date.parse(run.startUtc);
|
|
201
|
+
if (Number.isFinite(startMs)) run.endUtc = new Date(startMs + intervalMs).toISOString();
|
|
202
|
+
}
|
|
203
|
+
spans.push(run);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
spans.sort((a, b) =>
|
|
207
|
+
a.startUtc < b.startUtc ? -1 : a.startUtc > b.startUtc ? 1
|
|
208
|
+
: a.app < b.app ? -1 : a.app > b.app ? 1
|
|
209
|
+
: a.endUtc < b.endUtc ? -1 : a.endUtc > b.endUtc ? 1 : 0,
|
|
210
|
+
);
|
|
211
|
+
return spans;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convert an instant the transcript renderer wrote as `...Z` — whose `HH:MM`
|
|
216
|
+
* clock is really local to `timezone`, but which reconstruct labelled UTC —
|
|
217
|
+
* into the true UTC instant. An empty/UTC zone or an unresolvable offset
|
|
218
|
+
* returns the input unchanged, so a transcript with no stored zone keeps UTC
|
|
219
|
+
* semantics (matching the fusion identity guard).
|
|
220
|
+
*/
|
|
221
|
+
function wallClockZToUtc(iso: string, timezone: string): string {
|
|
222
|
+
if (timezone === "" || timezone === "UTC") return iso;
|
|
223
|
+
const assumedMs = Date.parse(iso);
|
|
224
|
+
if (!Number.isFinite(assumedMs)) return iso;
|
|
225
|
+
let offset: string;
|
|
226
|
+
try {
|
|
227
|
+
offset = timezoneOffsetIso(new Date(assumedMs), timezone);
|
|
228
|
+
} catch {
|
|
229
|
+
return iso;
|
|
230
|
+
}
|
|
231
|
+
if (offset === "+00:00") return iso;
|
|
232
|
+
const wall = iso.endsWith("Z") ? iso.slice(0, -1) : iso;
|
|
233
|
+
const trueMs = Date.parse(`${wall}${offset}`);
|
|
234
|
+
return Number.isFinite(trueMs) ? new Date(trueMs).toISOString() : iso;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Re-anchor reconstructed conversations from their rendered (assumed-UTC) clocks
|
|
239
|
+
* to true UTC using each source's transcript timezone, so audio windows line up
|
|
240
|
+
* with the true-UTC screen snapshots instead of drifting by the zone offset
|
|
241
|
+
* (a 09:00 America/Chicago call must land at 14:00Z, not 09:00Z).
|
|
242
|
+
*/
|
|
243
|
+
function shiftFusionInputsToUtc(
|
|
244
|
+
conversations: readonly FusionConversationInput[],
|
|
245
|
+
tzBySource: ReadonlyMap<string, string>,
|
|
246
|
+
): FusionConversationInput[] {
|
|
247
|
+
return conversations.map((conversation) => {
|
|
248
|
+
const tz = tzBySource.get(conversation.source) ?? "";
|
|
249
|
+
if (tz === "" || tz === "UTC") return conversation;
|
|
250
|
+
const shifted: FusionConversationInput = {
|
|
251
|
+
...conversation,
|
|
252
|
+
startIso: wallClockZToUtc(conversation.startIso, tz),
|
|
253
|
+
segments: conversation.segments.map((segment) => {
|
|
254
|
+
const out: FusionSegmentInput = { ...segment };
|
|
255
|
+
if (segment.startIso !== undefined) out.startIso = wallClockZToUtc(segment.startIso, tz);
|
|
256
|
+
if (segment.endIso !== undefined) out.endIso = wallClockZToUtc(segment.endIso, tz);
|
|
257
|
+
return out;
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
if (conversation.endIso !== undefined) shifted.endIso = wallClockZToUtc(conversation.endIso, tz);
|
|
261
|
+
return shifted;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Cloud meeting-provider sources that supply explicit meeting boundaries and
|
|
267
|
+
* titles (Granola/Fireflies): such a conversation is a meeting on its own, so
|
|
268
|
+
* its audio window is marked `providerMeeting` to fire detect's provider branch
|
|
269
|
+
* even with no matching app span.
|
|
270
|
+
*/
|
|
271
|
+
const PROVIDER_MEETING_SOURCES: Record<string, true> = { granola: true, fireflies: true };
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Derive audio windows from reconstructed conversations. Each conversation is
|
|
275
|
+
* one window on its source; `distinctNonWearerSpeakers` counts distinct
|
|
276
|
+
* non-self speaker labels (the audio-only detection rule), and the end is the
|
|
277
|
+
* conversation end when known, else the latest segment instant, else the start.
|
|
278
|
+
*/
|
|
279
|
+
export function buildAudioWindows(
|
|
280
|
+
conversations: readonly FusionConversationInput[],
|
|
281
|
+
): MeetingAudioWindow[] {
|
|
282
|
+
return conversations.map((conversation) => {
|
|
283
|
+
const speakers = new Set<string>();
|
|
284
|
+
let maxEndMs = Date.parse(conversation.startIso);
|
|
285
|
+
for (const segment of conversation.segments) {
|
|
286
|
+
if (!segment.isSelf && segment.speaker.length > 0) speakers.add(segment.speaker);
|
|
287
|
+
for (const iso of [segment.endIso, segment.startIso]) {
|
|
288
|
+
if (iso === undefined) continue;
|
|
289
|
+
const ms = Date.parse(iso);
|
|
290
|
+
if (Number.isFinite(ms) && (!Number.isFinite(maxEndMs) || ms > maxEndMs)) maxEndMs = ms;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const endUtc =
|
|
294
|
+
conversation.endIso ??
|
|
295
|
+
(Number.isFinite(maxEndMs) ? new Date(maxEndMs).toISOString() : conversation.startIso);
|
|
296
|
+
const audioWindow: MeetingAudioWindow = {
|
|
297
|
+
source: conversation.source,
|
|
298
|
+
startUtc: conversation.startIso,
|
|
299
|
+
endUtc,
|
|
300
|
+
distinctNonWearerSpeakers: speakers.size,
|
|
301
|
+
};
|
|
302
|
+
// Carry the provider-supplied title so app+audio/audio/provider candidates
|
|
303
|
+
// can label the meeting; mark known cloud meeting providers so a titled
|
|
304
|
+
// provider conversation with no matching app span still fires detect's
|
|
305
|
+
// provider branch (a meeting on its own).
|
|
306
|
+
if (conversation.title !== undefined && conversation.title.length > 0) {
|
|
307
|
+
audioWindow.title = conversation.title;
|
|
308
|
+
}
|
|
309
|
+
if (PROVIDER_MEETING_SOURCES[conversation.source]) audioWindow.providerMeeting = true;
|
|
310
|
+
return audioWindow;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Reads a day's real activity + wearable signals and assembles the
|
|
316
|
+
* {@link MeetingDayData} the builder consumes. Injected readers keep this
|
|
317
|
+
* testable against fixtures and free of direct filesystem access.
|
|
318
|
+
*/
|
|
319
|
+
export class ActivityWearablesMeetingsDaySource implements MeetingsDaySource {
|
|
320
|
+
constructor(
|
|
321
|
+
private readonly deps: {
|
|
322
|
+
/**
|
|
323
|
+
* Screen-activity reader. Optional (issue #2123): only the machine-owner
|
|
324
|
+
* (default) namespace consumes machine-scoped activity; every other caller
|
|
325
|
+
* namespace runs audio-only, so it is constructed with no activity reader
|
|
326
|
+
* and the load degrades to zero screen snapshots.
|
|
327
|
+
*/
|
|
328
|
+
activity?: MeetingsActivityReader;
|
|
329
|
+
wearables: MeetingsWearableReader;
|
|
330
|
+
config: MeetingsConfig;
|
|
331
|
+
/** IANA timezone resolving the local day → UTC window (activity's zone). */
|
|
332
|
+
timezone: string;
|
|
333
|
+
},
|
|
334
|
+
) {}
|
|
335
|
+
|
|
336
|
+
async loadDayData(date: string): Promise<MeetingDayData> {
|
|
337
|
+
if (!isValidActivityDate(date)) {
|
|
338
|
+
throw new MeetingsInputError(`invalid day '${date}' — expected a real YYYY-MM-DD`);
|
|
339
|
+
}
|
|
340
|
+
const { startUtc, endUtc } = activityDayWindow(date, this.deps.timezone);
|
|
341
|
+
const snapshots = this.deps.activity
|
|
342
|
+
? await this.deps.activity.listSnapshotsForDay(null, startUtc, endUtc)
|
|
343
|
+
: [];
|
|
344
|
+
const activity = snapshots.map(toMeetingActivitySnapshot);
|
|
345
|
+
const appSpans = deriveAppSpans(snapshots, this.deps.config.appPatterns);
|
|
346
|
+
const bodies = await this.deps.wearables.readDayBodies(date);
|
|
347
|
+
const tzBySource = new Map(bodies.map((b) => [b.source, b.timezone ?? ""] as const));
|
|
348
|
+
const conversations = shiftFusionInputsToUtc(reconstructFusionInputs(date, bodies), tzBySource);
|
|
349
|
+
const audioWindows = buildAudioWindows(conversations);
|
|
350
|
+
return { detection: { date, appSpans, audioWindows }, conversations, activity };
|
|
351
|
+
}
|
|
352
|
+
}
|