@remnic/core 9.19.0 → 9.21.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 +8 -8
- package/dist/access-admin-ops-surface.js +8 -8
- package/dist/access-boundary.d.ts +8 -8
- package/dist/access-boundary.js +8 -8
- package/dist/access-cli.js +32 -32
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +12 -12
- package/dist/access-identity-continuity-surface.d.ts +6 -6
- package/dist/access-identity-continuity-surface.js +8 -8
- package/dist/access-lcm-surface.d.ts +8 -8
- package/dist/access-lcm-surface.js +8 -8
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +11 -11
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-BsawJa-L.d.ts → access-namespace-preflight-tmwQgCT6.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +8 -8
- package/dist/access-observe-write-surface.d.ts +8 -8
- package/dist/access-observe-write-surface.js +8 -8
- package/dist/access-operations-batch.js +9 -9
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-concurrency.d.ts +8 -8
- package/dist/access-recall-concurrency.js +8 -8
- package/dist/access-recall-response.d.ts +8 -8
- package/dist/access-recall-response.js +8 -8
- package/dist/access-recall-surface.d.ts +8 -8
- package/dist/access-recall-surface.js +8 -8
- package/dist/{access-service-DLuVnV6a.d.ts → access-service-BqUii7LY.d.ts} +6 -6
- package/dist/access-service.d.ts +8 -8
- package/dist/access-service.js +8 -8
- package/dist/access-surface-catalog.d.ts +8 -8
- 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 +1 -1
- package/dist/{auto-sync-IH3G25MT.js → auto-sync-UPTUVE4X.js} +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -6
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +4 -4
- 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-O6P8694A.d.ts → catalog-iWvWz61I.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +5 -5
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-YUWFASAK.js → chunk-275ENJDK.js} +2 -2
- package/dist/{chunk-EZFVVEQG.js → chunk-3ZUY2VQF.js} +2 -2
- package/dist/{chunk-HRREDJSL.js → chunk-4F6AANKS.js} +3 -3
- package/dist/{chunk-U7D5HWNG.js → chunk-4PYLBXX6.js} +2 -2
- package/dist/{chunk-3LZSS77C.js → chunk-53WDAG7X.js} +2 -2
- package/dist/{chunk-QEEMLGCO.js → chunk-5IXPWEQB.js} +5 -5
- package/dist/{chunk-VSVX777Y.js → chunk-5JQLAQUZ.js} +2 -2
- package/dist/{chunk-DR65EEXG.js → chunk-62QM5MKY.js} +7 -7
- package/dist/{chunk-WYL5ODHJ.js → chunk-6NZ7U4R4.js} +1 -1
- package/dist/chunk-6NZ7U4R4.js.map +1 -0
- package/dist/{chunk-OFQUY5NZ.js → chunk-7M6APKJL.js} +331 -5
- package/dist/chunk-7M6APKJL.js.map +1 -0
- package/dist/{chunk-OOJQ6ID5.js → chunk-AT3TDN6O.js} +1017 -296
- package/dist/chunk-AT3TDN6O.js.map +1 -0
- package/dist/{chunk-DQCUBRTZ.js → chunk-B7C3TF5H.js} +2 -2
- package/dist/{chunk-RPOOJPMB.js → chunk-EILX7SQR.js} +3 -3
- package/dist/{chunk-D5H3B36B.js → chunk-EXU5IQ3S.js} +2 -2
- package/dist/{chunk-KUF7OIFK.js → chunk-EZKKHVFY.js} +2 -2
- package/dist/{chunk-S5HCKQ7Q.js → chunk-GEUDRENF.js} +2 -2
- package/dist/{chunk-X7GOH7Y6.js → chunk-HJEEAO4O.js} +2 -2
- package/dist/{chunk-FL7MNSH3.js → chunk-HPOY2PHQ.js} +3 -3
- package/dist/{chunk-Q55XM4SU.js → chunk-JFDEYXF7.js} +2 -2
- package/dist/{chunk-UVSDFM7R.js → chunk-JN3IUA2Q.js} +2 -2
- package/dist/{chunk-PLRDZ2OO.js → chunk-JWMYVQAM.js} +2 -2
- package/dist/{chunk-KOFVOVJG.js → chunk-KHRKEELC.js} +17 -17
- package/dist/{chunk-Y4JHE2ML.js → chunk-LI4ZOFHW.js} +2 -2
- package/dist/{chunk-MLW3J7IC.js → chunk-LMHDFOVV.js} +3 -3
- package/dist/{chunk-X6E5WAGX.js → chunk-N7EVMKP6.js} +6 -6
- package/dist/{chunk-HCNG42P3.js → chunk-Q3FWRDCP.js} +2 -2
- package/dist/{chunk-5MFHOX5T.js → chunk-QO2KODU6.js} +2 -2
- package/dist/{chunk-W3DJOT22.js → chunk-TCZWBJBO.js} +2 -2
- package/dist/{chunk-B3ABVQ36.js → chunk-UHVXE24Y.js} +2 -1
- package/dist/{chunk-UX3ZXN34.js → chunk-UV72DEZD.js} +2 -2
- package/dist/{chunk-OZSL2QZC.js → chunk-V5TSUGRB.js} +5 -5
- package/dist/{chunk-CH3BRJ3P.js → chunk-X5QS5C2O.js} +2 -2
- package/dist/{cli-0dQHR6e6.d.ts → cli-DPEhq0Cp.d.ts} +5 -5
- package/dist/cli.d.ts +10 -10
- package/dist/cli.js +25 -25
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +4 -4
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +4 -4
- 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 +4 -4
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/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 +468 -88
- package/dist/index.js +66 -36
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +8 -8
- 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 +5 -5
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +4 -4
- 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 +9 -9
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +3 -3
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-B8ickLkd.d.ts → orchestrator-DB93S48I.d.ts} +5 -5
- package/dist/orchestrator.d.ts +6 -6
- package/dist/orchestrator.js +32 -32
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-B669unGm.d.ts → pipeline-DmGc-H66.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-D0PVXmea.d.ts → qmd-cJT0crkF.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-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 +2 -2
- 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-BOp3gSzp.d.ts → scope-profiles-KG2nODX7.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-C7SZNzUJ.d.ts → semantic-consolidation-BaZlg0D8.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +4 -4
- 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-CJ_Fia4H.d.ts → storage-BKhHHnRo.d.ts} +2 -221
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +3 -3
- 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-GeWGXq9E.d.ts → types-GBQZuT7l.d.ts} +395 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +4 -4
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/config.ts +3 -3
- package/src/meetings/build.test.ts +646 -0
- package/src/meetings/build.ts +365 -0
- package/src/meetings/cli.test.ts +276 -0
- package/src/meetings/cli.ts +242 -0
- package/src/meetings/config.test.ts +72 -0
- package/src/meetings/config.ts +183 -0
- package/src/meetings/errors.ts +13 -0
- package/src/meetings/fuse.test.ts +208 -0
- package/src/meetings/fuse.ts +297 -0
- package/src/meetings/index.ts +11 -1
- package/src/meetings/memory-generator.ts +116 -0
- package/src/meetings/recall-exclusion.test.ts +88 -0
- package/src/meetings/store.test.ts +232 -0
- package/src/meetings/store.ts +465 -0
- package/src/meetings/types.ts +111 -0
- package/src/orchestration/orchestrator-helpers.ts +23 -3
- package/src/orchestration/recall-search-pipeline.test.ts +182 -0
- package/src/orchestration/recall-search-pipeline.ts +36 -15
- package/src/types.ts +6 -0
- package/dist/chunk-OFQUY5NZ.js.map +0 -1
- package/dist/chunk-OOJQ6ID5.js.map +0 -1
- package/dist/chunk-WYL5ODHJ.js.map +0 -1
- /package/dist/{auto-sync-IH3G25MT.js.map → auto-sync-UPTUVE4X.js.map} +0 -0
- /package/dist/{chunk-YUWFASAK.js.map → chunk-275ENJDK.js.map} +0 -0
- /package/dist/{chunk-EZFVVEQG.js.map → chunk-3ZUY2VQF.js.map} +0 -0
- /package/dist/{chunk-HRREDJSL.js.map → chunk-4F6AANKS.js.map} +0 -0
- /package/dist/{chunk-U7D5HWNG.js.map → chunk-4PYLBXX6.js.map} +0 -0
- /package/dist/{chunk-3LZSS77C.js.map → chunk-53WDAG7X.js.map} +0 -0
- /package/dist/{chunk-QEEMLGCO.js.map → chunk-5IXPWEQB.js.map} +0 -0
- /package/dist/{chunk-VSVX777Y.js.map → chunk-5JQLAQUZ.js.map} +0 -0
- /package/dist/{chunk-DR65EEXG.js.map → chunk-62QM5MKY.js.map} +0 -0
- /package/dist/{chunk-DQCUBRTZ.js.map → chunk-B7C3TF5H.js.map} +0 -0
- /package/dist/{chunk-RPOOJPMB.js.map → chunk-EILX7SQR.js.map} +0 -0
- /package/dist/{chunk-D5H3B36B.js.map → chunk-EXU5IQ3S.js.map} +0 -0
- /package/dist/{chunk-KUF7OIFK.js.map → chunk-EZKKHVFY.js.map} +0 -0
- /package/dist/{chunk-S5HCKQ7Q.js.map → chunk-GEUDRENF.js.map} +0 -0
- /package/dist/{chunk-X7GOH7Y6.js.map → chunk-HJEEAO4O.js.map} +0 -0
- /package/dist/{chunk-FL7MNSH3.js.map → chunk-HPOY2PHQ.js.map} +0 -0
- /package/dist/{chunk-Q55XM4SU.js.map → chunk-JFDEYXF7.js.map} +0 -0
- /package/dist/{chunk-UVSDFM7R.js.map → chunk-JN3IUA2Q.js.map} +0 -0
- /package/dist/{chunk-PLRDZ2OO.js.map → chunk-JWMYVQAM.js.map} +0 -0
- /package/dist/{chunk-KOFVOVJG.js.map → chunk-KHRKEELC.js.map} +0 -0
- /package/dist/{chunk-Y4JHE2ML.js.map → chunk-LI4ZOFHW.js.map} +0 -0
- /package/dist/{chunk-MLW3J7IC.js.map → chunk-LMHDFOVV.js.map} +0 -0
- /package/dist/{chunk-X6E5WAGX.js.map → chunk-N7EVMKP6.js.map} +0 -0
- /package/dist/{chunk-HCNG42P3.js.map → chunk-Q3FWRDCP.js.map} +0 -0
- /package/dist/{chunk-5MFHOX5T.js.map → chunk-QO2KODU6.js.map} +0 -0
- /package/dist/{chunk-W3DJOT22.js.map → chunk-TCZWBJBO.js.map} +0 -0
- /package/dist/{chunk-B3ABVQ36.js.map → chunk-UHVXE24Y.js.map} +0 -0
- /package/dist/{chunk-UX3ZXN34.js.map → chunk-UV72DEZD.js.map} +0 -0
- /package/dist/{chunk-OZSL2QZC.js.map → chunk-V5TSUGRB.js.map} +0 -0
- /package/dist/{chunk-CH3BRJ3P.js.map → chunk-X5QS5C2O.js.map} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
|
|
5
|
+
import { fuseMeeting } from "./fuse.js";
|
|
6
|
+
import type {
|
|
7
|
+
DetectedMeeting,
|
|
8
|
+
MeetingActivitySnapshot,
|
|
9
|
+
MeetingsConfig,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import type { FusionConversationInput, FusionSegmentInput } from "../wearables/fusion/types.js";
|
|
12
|
+
|
|
13
|
+
const DATE = "2026-03-10";
|
|
14
|
+
const WINDOW_START = "2026-03-10T14:00:00.000Z";
|
|
15
|
+
const WINDOW_END = "2026-03-10T14:30:00.000Z";
|
|
16
|
+
|
|
17
|
+
function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
|
|
18
|
+
return { ...DEFAULT_MEETINGS_CONFIG, appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns], ...overrides };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function meeting(overrides: Partial<DetectedMeeting> = {}): DetectedMeeting {
|
|
22
|
+
return {
|
|
23
|
+
id: "mtg-2026-03-10-abcdef01",
|
|
24
|
+
date: DATE,
|
|
25
|
+
startUtc: WINDOW_START,
|
|
26
|
+
endUtc: WINDOW_END,
|
|
27
|
+
app: "Zoom",
|
|
28
|
+
detectionSource: "app+audio",
|
|
29
|
+
sources: ["desktop"],
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function seg(
|
|
35
|
+
speaker: string,
|
|
36
|
+
isSelf: boolean,
|
|
37
|
+
text: string,
|
|
38
|
+
startIso: string,
|
|
39
|
+
endIso?: string,
|
|
40
|
+
): FusionSegmentInput {
|
|
41
|
+
return { speaker, isSelf, text, startIso, ...(endIso !== undefined ? { endIso } : {}) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function conv(
|
|
45
|
+
source: string,
|
|
46
|
+
id: string,
|
|
47
|
+
startIso: string,
|
|
48
|
+
endIso: string,
|
|
49
|
+
segments: FusionSegmentInput[],
|
|
50
|
+
): FusionConversationInput {
|
|
51
|
+
return { source, conversationId: id, startIso, endIso, segments };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function snap(overrides: Partial<MeetingActivitySnapshot> & { tsUtc: string; app: string }): MeetingActivitySnapshot {
|
|
55
|
+
return { ...overrides };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("fusion prefers the higher-trust source in overlap regions and records corroboratedBy without duplicating text", () => {
|
|
59
|
+
const fused = fuseMeeting(
|
|
60
|
+
{
|
|
61
|
+
meeting: meeting(),
|
|
62
|
+
conversations: [
|
|
63
|
+
conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:07:00.000Z", [
|
|
64
|
+
seg("Jane", false, "hello everyone", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:04.000Z"),
|
|
65
|
+
seg("Me (you)", true, "lets begin", "2026-03-10T14:06:00.000Z"),
|
|
66
|
+
]),
|
|
67
|
+
conv("pendant", "p1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:10.000Z", [
|
|
68
|
+
seg("Jane", false, "hello everyone", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:04.000Z"),
|
|
69
|
+
]),
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
config(),
|
|
73
|
+
{ sourceTrust: { desktop: 0.9, pendant: 0.6 } },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Same utterance recorded by two sources → ONE fused segment (no dup).
|
|
77
|
+
const helloSegments = fused.transcript.filter((s) => s.text === "hello everyone");
|
|
78
|
+
assert.equal(helloSegments.length, 1);
|
|
79
|
+
// Higher-trust source won the overlapping region.
|
|
80
|
+
assert.equal(helloSegments[0]?.provenance.source, "desktop");
|
|
81
|
+
assert.equal(helloSegments[0]?.provenance.reason, "higher-trust");
|
|
82
|
+
// The out-voted source is recorded as corroboration.
|
|
83
|
+
assert.deepEqual(fused.corroboratedBy, ["pendant"]);
|
|
84
|
+
assert.deepEqual(fused.sources, ["desktop", "pendant"]);
|
|
85
|
+
// Jane merged + wearer utterance = two segments total; wearer is not an attendee.
|
|
86
|
+
assert.equal(fused.transcript.length, 2);
|
|
87
|
+
assert.deepEqual(fused.attendees, ["Jane"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("screen context includes a >=20s dwell and omits a 5s alt-tab; meeting-app text feeds excerpts", () => {
|
|
91
|
+
const fused = fuseMeeting(
|
|
92
|
+
{
|
|
93
|
+
meeting: meeting(),
|
|
94
|
+
conversations: [
|
|
95
|
+
conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:12:00.000Z", [
|
|
96
|
+
seg("Jane", false, "agenda item one", "2026-03-10T14:10:00.000Z"),
|
|
97
|
+
]),
|
|
98
|
+
],
|
|
99
|
+
activity: [
|
|
100
|
+
snap({ tsUtc: "2026-03-10T14:05:00.000Z", app: "Preview", title: "Q3-roadmap.pdf", text: "Q3 roadmap draft" }),
|
|
101
|
+
snap({ tsUtc: "2026-03-10T14:05:25.000Z", app: "Notes", title: "scratch" }), // Preview dwell 25s → include; Notes starts
|
|
102
|
+
snap({ tsUtc: "2026-03-10T14:05:30.000Z", app: "Preview", title: "Q3-roadmap.pdf" }), // Notes dwell 5s → exclude
|
|
103
|
+
snap({ tsUtc: "2026-03-10T14:10:00.000Z", app: "Zoom", text: "Jane: agenda item one" }), // meeting-app → excerpt
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
config(),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assert.equal(fused.snapshotCount, 4);
|
|
110
|
+
const apps = fused.screenContext.map((e) => e.app);
|
|
111
|
+
assert.ok(apps.includes("Preview"), "Preview dwell should be in the timeline");
|
|
112
|
+
assert.ok(!apps.includes("Notes"), "5s alt-tab must be excluded");
|
|
113
|
+
const preview = fused.screenContext.find((e) => e.app === "Preview");
|
|
114
|
+
assert.equal(preview?.label, "Preview: Q3-roadmap.pdf");
|
|
115
|
+
assert.ok(preview !== undefined && preview.dwellSeconds >= 20);
|
|
116
|
+
assert.ok(fused.contextExcerpts.includes("Jane: agenda item one"));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("context excerpts respect maxContextChars", () => {
|
|
120
|
+
const fused = fuseMeeting(
|
|
121
|
+
{
|
|
122
|
+
meeting: meeting(),
|
|
123
|
+
conversations: [],
|
|
124
|
+
activity: [
|
|
125
|
+
snap({ tsUtc: "2026-03-10T14:01:00.000Z", app: "Zoom", text: "aaaa" }),
|
|
126
|
+
snap({ tsUtc: "2026-03-10T14:02:00.000Z", app: "Zoom", text: "bbbb" }),
|
|
127
|
+
snap({ tsUtc: "2026-03-10T14:03:00.000Z", app: "Zoom", text: "cccc" }),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
config({ maxContextChars: 8 }),
|
|
131
|
+
);
|
|
132
|
+
// 4-char excerpts, cap 8 → exactly two fit.
|
|
133
|
+
assert.deepEqual(fused.contextExcerpts, ["aaaa", "bbbb"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("degradation — no activity yields an empty screen context but a full transcript", () => {
|
|
137
|
+
const fused = fuseMeeting(
|
|
138
|
+
{
|
|
139
|
+
meeting: meeting({ detectionSource: "audio", app: undefined }),
|
|
140
|
+
conversations: [
|
|
141
|
+
conv("limitless", "l1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:20:00.000Z", [
|
|
142
|
+
seg("Jane", false, "quarterly numbers", "2026-03-10T14:05:00.000Z"),
|
|
143
|
+
]),
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
config(),
|
|
147
|
+
);
|
|
148
|
+
assert.equal(fused.snapshotCount, 0);
|
|
149
|
+
assert.deepEqual(fused.screenContext, []);
|
|
150
|
+
assert.deepEqual(fused.contextExcerpts, []);
|
|
151
|
+
assert.equal(fused.transcript.length, 1);
|
|
152
|
+
assert.deepEqual(fused.sources, ["limitless"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("degradation — no audio yields an empty transcript without crashing", () => {
|
|
156
|
+
const fused = fuseMeeting(
|
|
157
|
+
{ meeting: meeting(), conversations: [] },
|
|
158
|
+
config(),
|
|
159
|
+
);
|
|
160
|
+
assert.deepEqual(fused.transcript, []);
|
|
161
|
+
assert.deepEqual(fused.sources, []);
|
|
162
|
+
assert.deepEqual(fused.corroboratedBy, []);
|
|
163
|
+
assert.deepEqual(fused.attendees, []);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("segments outside the half-open window are clipped out", () => {
|
|
167
|
+
const fused = fuseMeeting(
|
|
168
|
+
{
|
|
169
|
+
meeting: meeting(),
|
|
170
|
+
conversations: [
|
|
171
|
+
conv("desktop", "d1", "2026-03-10T13:50:00.000Z", "2026-03-10T14:35:00.000Z", [
|
|
172
|
+
seg("Jane", false, "before start", "2026-03-10T13:59:59.000Z"),
|
|
173
|
+
seg("Jane", false, "in window", "2026-03-10T14:05:00.000Z"),
|
|
174
|
+
seg("Jane", false, "at end boundary", WINDOW_END), // half-open: end excluded
|
|
175
|
+
]),
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
config(),
|
|
179
|
+
);
|
|
180
|
+
const texts = fused.transcript.map((s) => s.text);
|
|
181
|
+
assert.deepEqual(texts, ["in window"]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("throws on an invalid meeting window", () => {
|
|
185
|
+
assert.throws(
|
|
186
|
+
() => fuseMeeting({ meeting: meeting({ endUtc: WINDOW_START }), conversations: [] }, config()),
|
|
187
|
+
/invalid window/,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a lone trailing other-app snapshot is not inflated to the meeting end", () => {
|
|
192
|
+
const fused = fuseMeeting(
|
|
193
|
+
{
|
|
194
|
+
meeting: meeting(),
|
|
195
|
+
conversations: [
|
|
196
|
+
conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:12:00.000Z", [
|
|
197
|
+
seg("Jane", false, "agenda", "2026-03-10T14:06:00.000Z"),
|
|
198
|
+
]),
|
|
199
|
+
],
|
|
200
|
+
// A single Notes snapshot 1 min before the window end. With no later
|
|
201
|
+
// snapshot, the trailing run must NOT count dwell up to the meeting end.
|
|
202
|
+
activity: [snap({ tsUtc: "2026-03-10T14:29:00.000Z", app: "Notes", title: "scratch" })],
|
|
203
|
+
},
|
|
204
|
+
config(),
|
|
205
|
+
);
|
|
206
|
+
assert.equal(fused.snapshotCount, 1);
|
|
207
|
+
assert.deepEqual(fused.screenContext, [], "a lone trailing snapshot has zero observed dwell");
|
|
208
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting fusion (issue #1900, Phase 4 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* For a detected meeting window `[startUtc, endUtc)` (half-open, AGENTS.md §23),
|
|
5
|
+
* fuse the concurrent signals into a screen-aware `FusedMeeting`:
|
|
6
|
+
*
|
|
7
|
+
* 1. Transcript — clip every wearable source's segments to the window and run
|
|
8
|
+
* them through the SHARED wearables reconciliation (`fuseCluster`). Higher-
|
|
9
|
+
* `sourceTrust` text wins overlapping regions and the out-voted sources are
|
|
10
|
+
* recorded per segment; this module never re-implements alignment/dedup —
|
|
11
|
+
* the repo rule is "extend the shared fusion module, never fork it".
|
|
12
|
+
* 2. Screen context — other-app foreground dwell within the window, kept only
|
|
13
|
+
* when it lasts `contextDwellSeconds`, plus deduped on-screen text excerpts
|
|
14
|
+
* capped at `maxContextChars`.
|
|
15
|
+
* 3. Attendees — distinct non-wearer speaker labels from the fused transcript
|
|
16
|
+
* (already resolved through the wearables speaker registry upstream).
|
|
17
|
+
*
|
|
18
|
+
* Pure and deterministic: same inputs ⇒ same `FusedMeeting`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { fuseCluster } from "../wearables/fusion/reconcile.js";
|
|
22
|
+
import type {
|
|
23
|
+
FusionConversationInput,
|
|
24
|
+
FusionSegmentInput,
|
|
25
|
+
} from "../wearables/fusion/types.js";
|
|
26
|
+
import type {
|
|
27
|
+
FusedMeeting,
|
|
28
|
+
MeetingActivitySnapshot,
|
|
29
|
+
MeetingBuildInput,
|
|
30
|
+
MeetingScreenContextEvent,
|
|
31
|
+
MeetingsConfig,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
/** Per-run tuning that mirrors the wearables fusion knobs. */
|
|
35
|
+
export interface MeetingFusionOptions {
|
|
36
|
+
/** Per-source trust priors handed to reconciliation. */
|
|
37
|
+
sourceTrust?: Record<string, number>;
|
|
38
|
+
/** Max drift (ms) for two segments to align across sources. */
|
|
39
|
+
windowToleranceMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SORT_STR = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
43
|
+
|
|
44
|
+
/** `HH:MM` in UTC for a parseable ISO instant (mirrors the day-store fallback). */
|
|
45
|
+
function formatClockUtc(ms: number): string {
|
|
46
|
+
return new Date(ms).toISOString().slice(11, 16);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Overlap of two half-open [start,end) windows in ms (0 when disjoint). */
|
|
50
|
+
function overlapMs(aStart: number, aEnd: number, bStart: number, bEnd: number): number {
|
|
51
|
+
return Math.max(0, Math.min(aEnd, bEnd) - Math.max(aStart, bStart));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a segment falls in the half-open window; null when it carries no
|
|
55
|
+
* parseable start (the caller decides via the conversation-level overlap). */
|
|
56
|
+
function segmentInWindow(
|
|
57
|
+
seg: FusionSegmentInput,
|
|
58
|
+
windowStartMs: number,
|
|
59
|
+
windowEndMs: number,
|
|
60
|
+
): boolean | null {
|
|
61
|
+
const startMs = seg.startIso === undefined ? NaN : Date.parse(seg.startIso);
|
|
62
|
+
if (Number.isNaN(startMs)) return null;
|
|
63
|
+
const endParsed = seg.endIso === undefined ? NaN : Date.parse(seg.endIso);
|
|
64
|
+
const endMs = Number.isNaN(endParsed) ? startMs : endParsed;
|
|
65
|
+
if (endMs === startMs) return startMs >= windowStartMs && startMs < windowEndMs;
|
|
66
|
+
return overlapMs(startMs, endMs, windowStartMs, windowEndMs) > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Clip one conversation to the window, keeping only in-window segments. */
|
|
70
|
+
function clipConversation(
|
|
71
|
+
conv: FusionConversationInput,
|
|
72
|
+
windowStartMs: number,
|
|
73
|
+
windowEndMs: number,
|
|
74
|
+
): FusionConversationInput | null {
|
|
75
|
+
const convStart = Date.parse(conv.startIso);
|
|
76
|
+
const convEnd = conv.endIso === undefined ? convStart : Date.parse(conv.endIso);
|
|
77
|
+
const convOverlaps =
|
|
78
|
+
Number.isFinite(convStart) &&
|
|
79
|
+
overlapMs(convStart, Number.isFinite(convEnd) ? convEnd : convStart, windowStartMs, windowEndMs) > 0;
|
|
80
|
+
|
|
81
|
+
const kept: FusionSegmentInput[] = [];
|
|
82
|
+
let earliestStart: number | undefined;
|
|
83
|
+
let earliestIso: string | undefined;
|
|
84
|
+
let latestEnd: number | undefined;
|
|
85
|
+
let latestIso: string | undefined;
|
|
86
|
+
for (const seg of conv.segments) {
|
|
87
|
+
const decision = segmentInWindow(seg, windowStartMs, windowEndMs);
|
|
88
|
+
// Untimed segments ride along only when the conversation itself overlaps.
|
|
89
|
+
if (decision === false || (decision === null && !convOverlaps)) continue;
|
|
90
|
+
kept.push(seg);
|
|
91
|
+
if (seg.startIso !== undefined) {
|
|
92
|
+
const s = Date.parse(seg.startIso);
|
|
93
|
+
if (Number.isFinite(s) && (earliestStart === undefined || s < earliestStart)) {
|
|
94
|
+
earliestStart = s;
|
|
95
|
+
earliestIso = seg.startIso;
|
|
96
|
+
}
|
|
97
|
+
const endIso = seg.endIso ?? seg.startIso;
|
|
98
|
+
const e = Date.parse(endIso);
|
|
99
|
+
if (Number.isFinite(e) && (latestEnd === undefined || e > latestEnd)) {
|
|
100
|
+
latestEnd = e;
|
|
101
|
+
latestIso = endIso;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (kept.length === 0) return null;
|
|
106
|
+
return {
|
|
107
|
+
source: conv.source,
|
|
108
|
+
conversationId: conv.conversationId,
|
|
109
|
+
...(conv.title !== undefined ? { title: conv.title } : {}),
|
|
110
|
+
...(conv.summary !== undefined ? { summary: conv.summary } : {}),
|
|
111
|
+
startIso: earliestIso ?? conv.startIso,
|
|
112
|
+
...(latestIso !== undefined ? { endIso: latestIso } : conv.endIso !== undefined ? { endIso: conv.endIso } : {}),
|
|
113
|
+
segments: kept,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Strip the URL scheme for a compact screen-context label. */
|
|
118
|
+
function shortenUrl(url: string): string {
|
|
119
|
+
return url.replace(/^[a-z]+:\/\//i, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function contextLabel(snap: MeetingActivitySnapshot): string {
|
|
123
|
+
if (snap.url !== undefined && snap.url.length > 0) return `${snap.app}: ${shortenUrl(snap.url)}`;
|
|
124
|
+
if (snap.title !== undefined && snap.title.length > 0) return `${snap.app}: ${snap.title}`;
|
|
125
|
+
return snap.app;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** True when a snapshot is the meeting app itself (captions/chat/participants),
|
|
129
|
+
* not other context. Matches the meeting's app or any configured app pattern. */
|
|
130
|
+
function isMeetingAppSnapshot(
|
|
131
|
+
snap: MeetingActivitySnapshot,
|
|
132
|
+
meetingApp: string | undefined,
|
|
133
|
+
appPatterns: readonly string[],
|
|
134
|
+
): boolean {
|
|
135
|
+
const hay = `${snap.app} ${snap.title ?? ""} ${snap.url ?? ""}`.toLowerCase();
|
|
136
|
+
if (meetingApp !== undefined && meetingApp.length > 0 && hay.includes(meetingApp.toLowerCase())) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
for (const pattern of appPatterns) {
|
|
140
|
+
if (pattern.length > 0 && hay.includes(pattern.toLowerCase())) return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const MEETING_APP_KEY = "\u0000meeting-app";
|
|
146
|
+
|
|
147
|
+
interface ScreenContextResult {
|
|
148
|
+
events: MeetingScreenContextEvent[];
|
|
149
|
+
excerpts: string[];
|
|
150
|
+
snapshotCount: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the screen-context timeline + excerpts from the day's activity clipped
|
|
155
|
+
* to the meeting window. Consecutive snapshots sharing a foreground key form a
|
|
156
|
+
* dwell run whose length is bounded by the NEXT foreground change (or the
|
|
157
|
+
* window end); other-app runs are emitted only when the dwell clears
|
|
158
|
+
* `contextDwellSeconds`, so a brief alt-tab is dropped and a lingered doc is
|
|
159
|
+
* kept. Meeting-app snapshots never enter the timeline but feed excerpts.
|
|
160
|
+
*/
|
|
161
|
+
function buildScreenContext(
|
|
162
|
+
activity: readonly MeetingActivitySnapshot[],
|
|
163
|
+
windowStartMs: number,
|
|
164
|
+
windowEndMs: number,
|
|
165
|
+
meetingApp: string | undefined,
|
|
166
|
+
config: MeetingsConfig,
|
|
167
|
+
): ScreenContextResult {
|
|
168
|
+
const win = activity
|
|
169
|
+
.map((snap) => ({ snap, ms: Date.parse(snap.tsUtc) }))
|
|
170
|
+
.filter((entry) => Number.isFinite(entry.ms) && entry.ms >= windowStartMs && entry.ms < windowEndMs)
|
|
171
|
+
.sort((a, b) => a.ms - b.ms || SORT_STR(a.snap.app, b.snap.app));
|
|
172
|
+
|
|
173
|
+
const keyOf = (snap: MeetingActivitySnapshot): string =>
|
|
174
|
+
isMeetingAppSnapshot(snap, meetingApp, config.appPatterns)
|
|
175
|
+
? MEETING_APP_KEY
|
|
176
|
+
: `${snap.app}\u0000${snap.url ?? snap.title ?? ""}`;
|
|
177
|
+
|
|
178
|
+
const events: MeetingScreenContextEvent[] = [];
|
|
179
|
+
const includedText: string[] = [];
|
|
180
|
+
|
|
181
|
+
let runStartIdx = 0;
|
|
182
|
+
while (runStartIdx < win.length) {
|
|
183
|
+
const key = keyOf(win[runStartIdx]!.snap);
|
|
184
|
+
let runEndIdx = runStartIdx + 1;
|
|
185
|
+
while (runEndIdx < win.length && keyOf(win[runEndIdx]!.snap) === key) runEndIdx++;
|
|
186
|
+
const runStartMs = win[runStartIdx]!.ms;
|
|
187
|
+
// A run's dwell ends at the NEXT foreground change (next different-key
|
|
188
|
+
// snapshot). The trailing run has no observed switch-away, so bound it by the
|
|
189
|
+
// LAST snapshot in the run — never inflate to the meeting end (a lone trailing
|
|
190
|
+
// alt-tab would otherwise look like it dwelled until the meeting ended).
|
|
191
|
+
const runEndMs = runEndIdx < win.length ? win[runEndIdx]!.ms : win[runEndIdx - 1]!.ms;
|
|
192
|
+
const dwellSeconds = Math.floor((runEndMs - runStartMs) / 1000);
|
|
193
|
+
const head = win[runStartIdx]!.snap;
|
|
194
|
+
|
|
195
|
+
const isMeetingApp = key === MEETING_APP_KEY;
|
|
196
|
+
if (isMeetingApp) {
|
|
197
|
+
// Captions / chat / participant text visible on the meeting app itself.
|
|
198
|
+
for (let i = runStartIdx; i < runEndIdx; i++) {
|
|
199
|
+
const text = win[i]!.snap.text;
|
|
200
|
+
if (text !== undefined && text.trim().length > 0) includedText.push(text.trim());
|
|
201
|
+
}
|
|
202
|
+
} else if (dwellSeconds >= config.contextDwellSeconds) {
|
|
203
|
+
events.push({
|
|
204
|
+
tsUtc: new Date(runStartMs).toISOString(),
|
|
205
|
+
clock: formatClockUtc(runStartMs),
|
|
206
|
+
app: head.app,
|
|
207
|
+
label: contextLabel(head),
|
|
208
|
+
dwellSeconds,
|
|
209
|
+
});
|
|
210
|
+
for (let i = runStartIdx; i < runEndIdx; i++) {
|
|
211
|
+
const text = win[i]!.snap.text;
|
|
212
|
+
if (text !== undefined && text.trim().length > 0) includedText.push(text.trim());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
runStartIdx = runEndIdx;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Dedup excerpts (first occurrence wins) and cap total characters.
|
|
219
|
+
const excerpts: string[] = [];
|
|
220
|
+
const seen = new Set<string>();
|
|
221
|
+
let used = 0;
|
|
222
|
+
for (const text of includedText) {
|
|
223
|
+
if (seen.has(text)) continue;
|
|
224
|
+
if (used + text.length > config.maxContextChars) break;
|
|
225
|
+
seen.add(text);
|
|
226
|
+
excerpts.push(text);
|
|
227
|
+
used += text.length;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { events, excerpts, snapshotCount: win.length };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Fuse one detected meeting's transcript + screen context into a FusedMeeting. */
|
|
234
|
+
export function fuseMeeting(
|
|
235
|
+
input: MeetingBuildInput,
|
|
236
|
+
config: MeetingsConfig,
|
|
237
|
+
options: MeetingFusionOptions = {},
|
|
238
|
+
): FusedMeeting {
|
|
239
|
+
const windowStartMs = Date.parse(input.meeting.startUtc);
|
|
240
|
+
const windowEndMs = Date.parse(input.meeting.endUtc);
|
|
241
|
+
if (!Number.isFinite(windowStartMs) || !Number.isFinite(windowEndMs) || windowEndMs <= windowStartMs) {
|
|
242
|
+
throw new RangeError(
|
|
243
|
+
`meetings: meeting ${input.meeting.id} has an invalid window [${input.meeting.startUtc}, ${input.meeting.endUtc}).`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const clipped: FusionConversationInput[] = [];
|
|
248
|
+
for (const conv of input.conversations) {
|
|
249
|
+
const clip = clipConversation(conv, windowStartMs, windowEndMs);
|
|
250
|
+
if (clip !== null) clipped.push(clip);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const fused =
|
|
254
|
+
clipped.length > 0
|
|
255
|
+
? fuseCluster(clipped, {
|
|
256
|
+
...(options.sourceTrust !== undefined ? { sourceTrust: options.sourceTrust } : {}),
|
|
257
|
+
...(options.windowToleranceMs !== undefined ? { windowToleranceMs: options.windowToleranceMs } : {}),
|
|
258
|
+
})
|
|
259
|
+
: { sources: [], speakers: [], segments: [], disagreements: [], startIso: input.meeting.startUtc };
|
|
260
|
+
|
|
261
|
+
const corroborated = new Set<string>();
|
|
262
|
+
for (const segment of fused.segments) {
|
|
263
|
+
for (const alt of segment.provenance.alternatives) {
|
|
264
|
+
// Suppress a source only as corroborator of the SAME segment it won
|
|
265
|
+
// (a winner is never its own alternative, so this is a guard). A source
|
|
266
|
+
// that WON one segment but lost another still corroborates the one it
|
|
267
|
+
// lost — deleting it globally erased legitimate multi-device corroboration
|
|
268
|
+
// and dropped the summary/fact trust boost.
|
|
269
|
+
if (alt.source === segment.provenance.source) continue;
|
|
270
|
+
corroborated.add(alt.source);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const attendees = new Set<string>();
|
|
275
|
+
for (const speaker of fused.speakers) {
|
|
276
|
+
if (!speaker.isSelf) attendees.add(speaker.label);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const screen = buildScreenContext(
|
|
280
|
+
input.activity ?? [],
|
|
281
|
+
windowStartMs,
|
|
282
|
+
windowEndMs,
|
|
283
|
+
input.meeting.app,
|
|
284
|
+
config,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
attendees: [...attendees].sort(SORT_STR),
|
|
289
|
+
sources: [...fused.sources].sort(SORT_STR),
|
|
290
|
+
corroboratedBy: [...corroborated].sort(SORT_STR),
|
|
291
|
+
screenContext: screen.events,
|
|
292
|
+
contextExcerpts: screen.excerpts,
|
|
293
|
+
transcript: fused.segments,
|
|
294
|
+
speakers: fused.speakers,
|
|
295
|
+
snapshotCount: screen.snapshotCount,
|
|
296
|
+
};
|
|
297
|
+
}
|
package/src/meetings/index.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public entry for the retrospective meeting-detection subsystem (issue #1900):
|
|
3
|
-
*
|
|
3
|
+
* the deterministic engine — detection, fusion, record store, day build, and the
|
|
4
|
+
* CLI over a day's already-ingested audio + app-span signals — plus the
|
|
5
|
+
* memory-generation SEAM the builder depends on. The concrete memory generator
|
|
6
|
+
* and the day-source/service/scheduler wiring ship in the surface slice.
|
|
4
7
|
* Re-exported from the package root (`src/index.ts`) so consumers import it from
|
|
5
8
|
* `@remnic/core`, matching the wearables/activity subsystems' surfacing.
|
|
6
9
|
*/
|
|
7
10
|
export * from "./types.js";
|
|
8
11
|
export * from "./detect.js";
|
|
12
|
+
export * from "./config.js";
|
|
13
|
+
export * from "./errors.js";
|
|
14
|
+
export * from "./fuse.js";
|
|
15
|
+
export * from "./store.js";
|
|
16
|
+
export * from "./memory-generator.js";
|
|
17
|
+
export * from "./build.js";
|
|
18
|
+
export * from "./cli.js";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting memory-generation SEAM (issue #1900 — engine/memory decouple).
|
|
3
|
+
*
|
|
4
|
+
* Engine-layer boundary between the deterministic meeting builder and the
|
|
5
|
+
* memory-generation layer. `MeetingsBuilder` (detect/fuse/store/build) depends
|
|
6
|
+
* ONLY on `MeetingMemoryGenerator` declared here — never on `memory-gen.ts` — so
|
|
7
|
+
* the deterministic engine carries zero dependency on episode/fact generation
|
|
8
|
+
* and ships in its own scope-budgeted PR. The concrete generator lives in
|
|
9
|
+
* `memory-gen.ts` behind `createMeetingMemoryGenerator`, which implements this
|
|
10
|
+
* interface by driving the existing episode/fact/retract logic.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { MemoryWriteResult } from "../index.js";
|
|
14
|
+
import type { ImportanceScore, MemoryStatus } from "../types.js";
|
|
15
|
+
import type { SealedMemoryEnvelope } from "../write-envelope.js";
|
|
16
|
+
import type { MeetingRecord } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Narrow writer interface satisfied by `StorageManager` — the same sealed
|
|
20
|
+
* write entry point the wearables generator uses, so meeting memories inherit
|
|
21
|
+
* encrypted-at-rest + atomic write + dedup without this module knowing the key.
|
|
22
|
+
*/
|
|
23
|
+
export interface MeetingMemoryWriter {
|
|
24
|
+
writeSealedMemory(
|
|
25
|
+
envelope: SealedMemoryEnvelope,
|
|
26
|
+
extras: {
|
|
27
|
+
importance?: ImportanceScore;
|
|
28
|
+
contentHashSource?: string;
|
|
29
|
+
status?: MemoryStatus;
|
|
30
|
+
memoryKind?: "episode" | "note" | "box" | "dream" | "procedural";
|
|
31
|
+
},
|
|
32
|
+
): Promise<MemoryWriteResult>;
|
|
33
|
+
/**
|
|
34
|
+
* True when an active/pending memory whose frontmatter `source` equals
|
|
35
|
+
* `source` already carries this exact content. Meeting memories are episodes
|
|
36
|
+
* (`moment`) and decisions/commitments (`decision`/`commitment`) — categories
|
|
37
|
+
* the storage fact-hash index does NOT cover — so idempotency is a
|
|
38
|
+
* source-scoped content scan, mirroring the wearables writer, NOT
|
|
39
|
+
* `hasFactContentHash` (fact-only; always false here → duplicates on rebuild).
|
|
40
|
+
*/
|
|
41
|
+
hasMemoryFromSource(source: string, content: string): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Retire (invalidate) every memory whose frontmatter `source` equals `source`.
|
|
44
|
+
* Returns the count retired. Used to reconcile away a removed meeting's
|
|
45
|
+
* episode/summary memories so recall never surfaces a meeting `show` can't load.
|
|
46
|
+
*/
|
|
47
|
+
retireMemoriesFromSource(source: string): Promise<number>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Result of generating episode memories for a day's meetings. */
|
|
51
|
+
export interface MeetingEpisodeGenResult {
|
|
52
|
+
/** Episodes newly written. */
|
|
53
|
+
written: number;
|
|
54
|
+
/** Episodes skipped because an identical one already existed. */
|
|
55
|
+
skipped: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Day-level aggregate of the trust-gated summary/fact layer across records. */
|
|
59
|
+
export interface MeetingsDayFactGenResult {
|
|
60
|
+
/** True when the LLM extractor ran for at least one record. */
|
|
61
|
+
llmInvoked: boolean;
|
|
62
|
+
/** Facts written active. */
|
|
63
|
+
active: number;
|
|
64
|
+
/** Facts queued for review (pending_review). */
|
|
65
|
+
review: number;
|
|
66
|
+
/** Candidates/summaries dropped below the trust bar or judge-rejected. */
|
|
67
|
+
dropped: number;
|
|
68
|
+
/** Candidates/summaries skipped because an identical memory already existed. */
|
|
69
|
+
skipped: number;
|
|
70
|
+
/** Count of records whose summary memory was written. */
|
|
71
|
+
summariesWritten: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Outcome the memory generator returns to the builder after a day's records are
|
|
76
|
+
* built and persisted. Counts feed the day-build summary; `reindexNeeded` folds
|
|
77
|
+
* the "reindex when new episodes/facts were written even if no record changed"
|
|
78
|
+
* rule so the builder never reaches into the memory layer to decide it.
|
|
79
|
+
*/
|
|
80
|
+
export interface MeetingMemoryOutcome {
|
|
81
|
+
/** Episode-memory counts, present only when records were built. */
|
|
82
|
+
episodes?: MeetingEpisodeGenResult;
|
|
83
|
+
/** Trust-gated summary/fact counts, present only when summary deps ran. */
|
|
84
|
+
facts?: MeetingsDayFactGenResult;
|
|
85
|
+
/** True when newly written memories require an isolated reindex. */
|
|
86
|
+
reindexNeeded: boolean;
|
|
87
|
+
/** Non-fatal warnings raised while generating memories. */
|
|
88
|
+
warnings: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Seam the deterministic builder depends on. Invoked once per day build after
|
|
93
|
+
* the record-store writes with, for the day:
|
|
94
|
+
* - `built` — every record composed this build (new, updated, unchanged).
|
|
95
|
+
* - `removedIds` — stale ids the rebuild reconciled away (retract memories).
|
|
96
|
+
* - `unchangedIds`— built ids whose stored `contentHash` was IDENTICAL this
|
|
97
|
+
* build (nothing rewritten). The generator SKIPS all
|
|
98
|
+
* episode/summary/fact work for these: an idempotent rebuild
|
|
99
|
+
* must not re-invoke the LLM or duplicate memories.
|
|
100
|
+
* - `updatedIds` — built ids that existed before AND were rewritten this build
|
|
101
|
+
* (same id, changed `contentHash`). The generator REFRESHES
|
|
102
|
+
* them: retract the stale episode/summary memories for that
|
|
103
|
+
* source, then regenerate from the new record.
|
|
104
|
+
* Ids not in `unchangedIds` or `updatedIds` are newly built — generated fresh
|
|
105
|
+
* with no retract. The three id sets are disjoint subsets of `built`. The
|
|
106
|
+
* implementation reports the counts + reindex signal the builder folds into its
|
|
107
|
+
* day summary.
|
|
108
|
+
*/
|
|
109
|
+
export interface MeetingMemoryGenerator {
|
|
110
|
+
onRecordsBuilt(input: {
|
|
111
|
+
built: readonly MeetingRecord[];
|
|
112
|
+
removedIds: readonly string[];
|
|
113
|
+
unchangedIds: readonly string[];
|
|
114
|
+
updatedIds: readonly string[];
|
|
115
|
+
}): Promise<MeetingMemoryOutcome>;
|
|
116
|
+
}
|