@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,498 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { composeMeetingRecord, type MeetingRecordBase } from "./store.js";
|
|
8
|
+
import {
|
|
9
|
+
composeMeetingEpisodeContent,
|
|
10
|
+
generateMeetingEpisodes,
|
|
11
|
+
generateMeetingSummaryFacts,
|
|
12
|
+
meetingDayTag,
|
|
13
|
+
meetingSourceLabel,
|
|
14
|
+
writeMeetingEpisodeMemory,
|
|
15
|
+
createMeetingMemoryWriter,
|
|
16
|
+
createMeetingMemoryGenerator,
|
|
17
|
+
type MeetingFactCandidate,
|
|
18
|
+
type MeetingSummaryExtractor,
|
|
19
|
+
type MeetingSummaryJudge,
|
|
20
|
+
} from "./memory-gen.js";
|
|
21
|
+
import type { MeetingMemoryWriter } from "./memory-generator.js";
|
|
22
|
+
import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
|
|
23
|
+
import type { FusedMeeting, MeetingRecord, MeetingsConfig } from "./types.js";
|
|
24
|
+
import type { FusedSegment } from "../wearables/fusion/types.js";
|
|
25
|
+
import type { SealedMemoryEnvelope } from "../write-envelope.js";
|
|
26
|
+
import { StorageManager, type MemoryWriteResult } from "../storage.js";
|
|
27
|
+
|
|
28
|
+
const DATE = "2026-03-10";
|
|
29
|
+
|
|
30
|
+
interface WriteCall {
|
|
31
|
+
envelope: SealedMemoryEnvelope;
|
|
32
|
+
extras: { status?: string; memoryKind?: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** In-memory writer modelling the sealed write path + source-scoped dedup. */
|
|
36
|
+
class FakeWriter implements MeetingMemoryWriter {
|
|
37
|
+
writes: WriteCall[] = [];
|
|
38
|
+
retired: string[] = [];
|
|
39
|
+
private bySource = new Map<string, Set<string>>();
|
|
40
|
+
|
|
41
|
+
async writeSealedMemory(
|
|
42
|
+
envelope: SealedMemoryEnvelope,
|
|
43
|
+
extras: { status?: string; memoryKind?: string; contentHashSource?: string },
|
|
44
|
+
): Promise<MemoryWriteResult> {
|
|
45
|
+
this.writes.push({ envelope, extras });
|
|
46
|
+
if (extras.contentHashSource !== undefined) {
|
|
47
|
+
const set = this.bySource.get(envelope.source) ?? new Set<string>();
|
|
48
|
+
set.add(extras.contentHashSource);
|
|
49
|
+
this.bySource.set(envelope.source, set);
|
|
50
|
+
}
|
|
51
|
+
return { id: `m${this.writes.length}`, path: `moments/m${this.writes.length}.md` } as unknown as MemoryWriteResult;
|
|
52
|
+
}
|
|
53
|
+
async hasMemoryFromSource(source: string, content: string): Promise<boolean> {
|
|
54
|
+
return this.bySource.get(source)?.has(content) ?? false;
|
|
55
|
+
}
|
|
56
|
+
async retireMemoriesFromSource(source: string): Promise<number> {
|
|
57
|
+
this.retired.push(source);
|
|
58
|
+
const count = this.bySource.get(source)?.size ?? 0;
|
|
59
|
+
this.bySource.delete(source);
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fused(overrides: Partial<FusedMeeting> = {}): FusedMeeting {
|
|
65
|
+
return {
|
|
66
|
+
attendees: ["Jane", "Sam"],
|
|
67
|
+
sources: ["desktop", "pendant"],
|
|
68
|
+
corroboratedBy: ["pendant"],
|
|
69
|
+
screenContext: [],
|
|
70
|
+
contextExcerpts: [],
|
|
71
|
+
transcript: [],
|
|
72
|
+
speakers: [],
|
|
73
|
+
snapshotCount: 0,
|
|
74
|
+
...overrides,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function record(base: Partial<MeetingRecordBase> = {}, f: Partial<FusedMeeting> = {}): MeetingRecord {
|
|
79
|
+
return composeMeetingRecord(
|
|
80
|
+
{
|
|
81
|
+
id: "mtg-2026-03-10-abcdef01",
|
|
82
|
+
date: DATE,
|
|
83
|
+
startUtc: "2026-03-10T14:00:00.000Z",
|
|
84
|
+
endUtc: "2026-03-10T14:30:00.000Z",
|
|
85
|
+
app: "Zoom",
|
|
86
|
+
detectionSource: "app+audio",
|
|
87
|
+
...base,
|
|
88
|
+
},
|
|
89
|
+
fused(f),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test("episode content is deterministic and includes span, attendees, sources, app", () => {
|
|
94
|
+
const content = composeMeetingEpisodeContent(record());
|
|
95
|
+
assert.match(content, /Meeting mtg-2026-03-10-abcdef01 — 2026-03-10 14:00–14:30 UTC \(Zoom\)/);
|
|
96
|
+
assert.match(content, /Attendees: Jane, Sam\./);
|
|
97
|
+
assert.match(content, /Transcript sources: desktop, pendant\./);
|
|
98
|
+
// Same input → identical content (idempotency anchor).
|
|
99
|
+
assert.equal(composeMeetingEpisodeContent(record()), content);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("writeMeetingEpisodeMemory writes an active episode with meeting provenance", async () => {
|
|
103
|
+
const writer = new FakeWriter();
|
|
104
|
+
const wrote = await writeMeetingEpisodeMemory(record(), writer);
|
|
105
|
+
assert.equal(wrote, true);
|
|
106
|
+
assert.equal(writer.writes.length, 1);
|
|
107
|
+
const { envelope, extras } = writer.writes[0]!;
|
|
108
|
+
assert.equal(extras.status, "active");
|
|
109
|
+
assert.equal(extras.memoryKind, "episode");
|
|
110
|
+
assert.equal(envelope.category, "moment");
|
|
111
|
+
assert.equal(envelope.source, meetingSourceLabel("mtg-2026-03-10-abcdef01"));
|
|
112
|
+
assert.equal(envelope.validAt, "2026-03-10T14:00:00.000Z");
|
|
113
|
+
assert.ok(envelope.tags.includes("meeting"));
|
|
114
|
+
assert.ok(envelope.tags.includes(meetingDayTag(DATE)));
|
|
115
|
+
// Byte-preserved camelCase provenance (composer canonicalizes structuredAttributes
|
|
116
|
+
// keys to lowercase; rawStructuredAttributes keeps the original casing).
|
|
117
|
+
assert.equal(envelope.rawStructuredAttributes?.meetingId, "mtg-2026-03-10-abcdef01");
|
|
118
|
+
assert.equal(envelope.rawStructuredAttributes?.meetingDate, DATE);
|
|
119
|
+
assert.equal(envelope.rawStructuredAttributes?.meetingApp, "Zoom");
|
|
120
|
+
assert.equal(envelope.rawStructuredAttributes?.transcriptSources, "desktop,pendant");
|
|
121
|
+
// Canonical (lowercased) form is what fingerprints/consumers query.
|
|
122
|
+
assert.equal(envelope.structuredAttributes?.meetingid, "mtg-2026-03-10-abcdef01");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("episode write is idempotent — an identical episode is skipped", async () => {
|
|
126
|
+
const writer = new FakeWriter();
|
|
127
|
+
assert.equal(await writeMeetingEpisodeMemory(record(), writer), true);
|
|
128
|
+
assert.equal(await writeMeetingEpisodeMemory(record(), writer), false);
|
|
129
|
+
assert.equal(writer.writes.length, 1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("an audio-only meeting omits the meetingApp attribute and the app suffix", async () => {
|
|
133
|
+
const writer = new FakeWriter();
|
|
134
|
+
const audioOnly = record({ app: undefined, detectionSource: "audio" });
|
|
135
|
+
await writeMeetingEpisodeMemory(audioOnly, writer);
|
|
136
|
+
const { envelope } = writer.writes[0]!;
|
|
137
|
+
assert.equal(envelope.structuredAttributes?.meetingApp, undefined);
|
|
138
|
+
assert.doesNotMatch(composeMeetingEpisodeContent(audioOnly), /\(Zoom\)/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("generateMeetingEpisodes writes one per record and is idempotent on re-run", async () => {
|
|
142
|
+
const writer = new FakeWriter();
|
|
143
|
+
const records = [
|
|
144
|
+
record({ id: "mtg-2026-03-10-00000001", startUtc: "2026-03-10T09:00:00.000Z" }),
|
|
145
|
+
record({ id: "mtg-2026-03-10-00000002", startUtc: "2026-03-10T15:00:00.000Z" }),
|
|
146
|
+
];
|
|
147
|
+
const first = await generateMeetingEpisodes(records, writer);
|
|
148
|
+
assert.deepEqual(first, { written: 2, skipped: 0 });
|
|
149
|
+
const second = await generateMeetingEpisodes(records, writer);
|
|
150
|
+
assert.deepEqual(second, { written: 0, skipped: 2 });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function segment(text: string): FusedSegment {
|
|
154
|
+
return {
|
|
155
|
+
speaker: "Jane",
|
|
156
|
+
isSelf: false,
|
|
157
|
+
text,
|
|
158
|
+
startIso: "2026-03-10T14:05:00.000Z",
|
|
159
|
+
confidence: 0.9,
|
|
160
|
+
provenance: { source: "desktop", conversationId: "d1", sourceTrust: 0.9, reason: "only-source", alternatives: [] },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function meetingConfig(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
|
|
165
|
+
return { ...DEFAULT_MEETINGS_CONFIG, appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns], enabled: true, ...overrides };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function spyExtractor(summary: string, candidates: MeetingFactCandidate[]): MeetingSummaryExtractor & { calls: number } {
|
|
169
|
+
return {
|
|
170
|
+
calls: 0,
|
|
171
|
+
async extract() {
|
|
172
|
+
this.calls++;
|
|
173
|
+
return { summary, candidates };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const TRANSCRIPT_RECORD = (): MeetingRecord =>
|
|
179
|
+
record({}, { transcript: [segment("we shipped it")], corroboratedBy: [] });
|
|
180
|
+
|
|
181
|
+
test("summaryMode off never invokes the extractor and writes no facts", async () => {
|
|
182
|
+
const writer = new FakeWriter();
|
|
183
|
+
const extractor = spyExtractor("s", [{ content: "we decided to ship", category: "decision", confidence: 0.9 }]);
|
|
184
|
+
const res = await generateMeetingSummaryFacts(TRANSCRIPT_RECORD(), meetingConfig({ summaryMode: "off" }), { extractor, writer });
|
|
185
|
+
assert.equal(res.llmInvoked, false);
|
|
186
|
+
assert.equal(extractor.calls, 0, "the LLM extractor must NOT be called in off mode");
|
|
187
|
+
assert.equal(writer.writes.length, 0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("summaryMode smart routes a high-trust decision to active with meeting provenance", async () => {
|
|
191
|
+
const writer = new FakeWriter();
|
|
192
|
+
const extractor = spyExtractor("Meeting summary.", [
|
|
193
|
+
{ content: "Team decided to ship v2 on Friday", category: "decision", confidence: 0.9 },
|
|
194
|
+
{ content: "Sam will send the recap by EOD", category: "commitment", confidence: 0.9 },
|
|
195
|
+
]);
|
|
196
|
+
const res = await generateMeetingSummaryFacts(TRANSCRIPT_RECORD(), meetingConfig({ summaryMode: "smart" }), { extractor, writer });
|
|
197
|
+
assert.equal(res.llmInvoked, true);
|
|
198
|
+
// 0.9 * 0.85 = 0.765 >= autoApprove 0.7 -> both active; plus the summary fact.
|
|
199
|
+
assert.equal(res.active, 2);
|
|
200
|
+
assert.equal(res.dropped, 0);
|
|
201
|
+
assert.equal(res.summaryWritten, true);
|
|
202
|
+
const decision = writer.writes.find((w) => w.envelope.category === "decision");
|
|
203
|
+
assert.ok(decision, "a decision fact was written");
|
|
204
|
+
assert.equal(decision!.extras.status, "active");
|
|
205
|
+
assert.equal(decision!.envelope.rawStructuredAttributes?.meetingId, "mtg-2026-03-10-abcdef01");
|
|
206
|
+
assert.ok(writer.writes.some((w) => w.envelope.category === "commitment"), "a commitment fact was written");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("summaryMode smart drops a low-trust candidate but corroboration promotes it to review", async () => {
|
|
210
|
+
// conf 0.5 * sourceTrust 0.85 = 0.425 < reviewTrust 0.45 -> drop.
|
|
211
|
+
const lowConf: MeetingFactCandidate[] = [{ content: "maybe we will refactor later", category: "fact", confidence: 0.5 }];
|
|
212
|
+
const w1 = new FakeWriter();
|
|
213
|
+
const r1 = await generateMeetingSummaryFacts(
|
|
214
|
+
record({}, { transcript: [segment("x")], corroboratedBy: [] }),
|
|
215
|
+
meetingConfig({ summaryMode: "smart" }),
|
|
216
|
+
{ extractor: spyExtractor("", lowConf), writer: w1 },
|
|
217
|
+
);
|
|
218
|
+
assert.equal(r1.dropped, 1);
|
|
219
|
+
assert.equal(r1.active + r1.review, 0);
|
|
220
|
+
// With corroboration (+0.15 -> 0.575) it clears reviewTrust but not autoApprove -> review.
|
|
221
|
+
const w2 = new FakeWriter();
|
|
222
|
+
const r2 = await generateMeetingSummaryFacts(
|
|
223
|
+
record({}, { transcript: [segment("x")], corroboratedBy: ["pendant"] }),
|
|
224
|
+
meetingConfig({ summaryMode: "smart" }),
|
|
225
|
+
{ extractor: spyExtractor("", lowConf), writer: w2 },
|
|
226
|
+
);
|
|
227
|
+
assert.equal(r2.review, 1);
|
|
228
|
+
assert.equal(r2.dropped, 0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("summaryMode review queues every candidate as pending_review regardless of trust", async () => {
|
|
232
|
+
const writer = new FakeWriter();
|
|
233
|
+
const extractor = spyExtractor("summary", [{ content: "high trust decision here", category: "decision", confidence: 0.99 }]);
|
|
234
|
+
const res = await generateMeetingSummaryFacts(TRANSCRIPT_RECORD(), meetingConfig({ summaryMode: "review" }), { extractor, writer });
|
|
235
|
+
assert.equal(res.active, 0);
|
|
236
|
+
assert.equal(res.review, 1);
|
|
237
|
+
const fact = writer.writes.find((w) => w.envelope.category === "decision");
|
|
238
|
+
assert.equal(fact!.extras.status, "pending_review");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a judge reject drops the candidate even at high trust", async () => {
|
|
242
|
+
const writer = new FakeWriter();
|
|
243
|
+
const extractor = spyExtractor("", [{ content: "decision that the judge rejects", category: "decision", confidence: 0.99 }]);
|
|
244
|
+
const res = await generateMeetingSummaryFacts(TRANSCRIPT_RECORD(), meetingConfig({ summaryMode: "smart" }), {
|
|
245
|
+
extractor,
|
|
246
|
+
writer,
|
|
247
|
+
judge: { async judge() { return ["reject"]; } },
|
|
248
|
+
});
|
|
249
|
+
assert.equal(res.dropped, 1);
|
|
250
|
+
assert.equal(res.active, 0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("no transcript (no audio) short-circuits without invoking the extractor", async () => {
|
|
254
|
+
const writer = new FakeWriter();
|
|
255
|
+
const extractor = spyExtractor("s", [{ content: "x y z decision", category: "decision", confidence: 0.9 }]);
|
|
256
|
+
const res = await generateMeetingSummaryFacts(record({}, { transcript: [] }), meetingConfig({ summaryMode: "smart" }), { extractor, writer });
|
|
257
|
+
assert.equal(res.llmInvoked, false);
|
|
258
|
+
assert.equal(extractor.calls, 0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("finding 3 — smart-mode summary is trust-gated, not force-active for a low-trust meeting", async () => {
|
|
262
|
+
const writer = new FakeWriter();
|
|
263
|
+
// sourceTrust 0.5 → summary trust 0.8 * 0.5 = 0.4 < reviewTrust 0.45 → drop.
|
|
264
|
+
// The judge also rejects the only candidate. Before the fix the summary text
|
|
265
|
+
// was written ACTIVE whenever not already hashed, bypassing the gate.
|
|
266
|
+
const extractor = spyExtractor("Everyone agreed to ship on Friday.", [
|
|
267
|
+
{ content: "we will ship on friday", category: "decision", confidence: 0.9 },
|
|
268
|
+
]);
|
|
269
|
+
const res = await generateMeetingSummaryFacts(
|
|
270
|
+
TRANSCRIPT_RECORD(),
|
|
271
|
+
meetingConfig({ summaryMode: "smart", sourceTrust: 0.5 }),
|
|
272
|
+
{ extractor, writer, judge: { async judge() { return ["reject"]; } } },
|
|
273
|
+
);
|
|
274
|
+
assert.equal(res.active, 0, "no candidate auto-actives");
|
|
275
|
+
assert.equal(res.summaryWritten, false, "the summary must be gated, not force-active");
|
|
276
|
+
assert.ok(res.dropped >= 1, "the below-review-bar summary is dropped");
|
|
277
|
+
assert.equal(writer.writes.length, 0, "nothing was written when everything is below the bar");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("finding 1 — episode idempotency holds against a real StorageManager (source+content, not fact-only hash)", async () => {
|
|
281
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-meeting-mem-"));
|
|
282
|
+
try {
|
|
283
|
+
const storage = new StorageManager(dir);
|
|
284
|
+
const writer = createMeetingMemoryWriter(storage);
|
|
285
|
+
const rec = record();
|
|
286
|
+
assert.equal(await writeMeetingEpisodeMemory(rec, writer), true, "first write lands the episode");
|
|
287
|
+
// The episode is a `moment`, absent from the fact-only hash index, so a
|
|
288
|
+
// rebuild must recognize it via the source+content probe and skip.
|
|
289
|
+
assert.equal(await writeMeetingEpisodeMemory(rec, writer), false, "unchanged rebuild skips");
|
|
290
|
+
const memories = await storage.readAllMemories();
|
|
291
|
+
const episodes = memories.filter((m) => m.frontmatter.source === meetingSourceLabel(rec.id));
|
|
292
|
+
assert.equal(episodes.length, 1, "exactly one episode after two runs — no duplicate");
|
|
293
|
+
} finally {
|
|
294
|
+
rmSync(dir, { recursive: true, force: true });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("finding 2 — smart-mode facts are idempotent on rebuild against a real StorageManager", async () => {
|
|
299
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-meeting-mem-"));
|
|
300
|
+
try {
|
|
301
|
+
const storage = new StorageManager(dir);
|
|
302
|
+
const writer = createMeetingMemoryWriter(storage);
|
|
303
|
+
const extractor = spyExtractor("Team shipped v2 on Friday.", [
|
|
304
|
+
{ content: "Team decided to ship v2 on Friday", category: "decision", confidence: 0.9 },
|
|
305
|
+
{ content: "Sam will send the recap by EOD", category: "commitment", confidence: 0.9 },
|
|
306
|
+
]);
|
|
307
|
+
const cfg = meetingConfig({ summaryMode: "smart" });
|
|
308
|
+
const rec = TRANSCRIPT_RECORD();
|
|
309
|
+
const first = await generateMeetingSummaryFacts(rec, cfg, { extractor, writer });
|
|
310
|
+
assert.equal(first.active, 2, "both high-trust candidates auto-active on the first pass");
|
|
311
|
+
assert.equal(first.skipped, 0);
|
|
312
|
+
const afterFirst = (await storage.readAllMemories()).filter(
|
|
313
|
+
(m) => m.frontmatter.source === meetingSourceLabel(rec.id),
|
|
314
|
+
).length;
|
|
315
|
+
// Rebuild/retry with identical extraction: every claim already persisted for
|
|
316
|
+
// this meeting → all skipped, nothing duplicated.
|
|
317
|
+
const second = await generateMeetingSummaryFacts(rec, cfg, { extractor, writer });
|
|
318
|
+
assert.equal(second.active, 0, "no new active facts on the second pass");
|
|
319
|
+
assert.ok(second.skipped >= 2, "duplicate candidates are skipped, not re-written");
|
|
320
|
+
const afterSecond = (await storage.readAllMemories()).filter(
|
|
321
|
+
(m) => m.frontmatter.source === meetingSourceLabel(rec.id),
|
|
322
|
+
).length;
|
|
323
|
+
assert.equal(afterSecond, afterFirst, "no meeting-derived memory was duplicated on rebuild");
|
|
324
|
+
} finally {
|
|
325
|
+
rmSync(dir, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("finding 1 — two production builds of an unchanged day write the episode once (generator + writer + StorageManager)", async () => {
|
|
330
|
+
const dir = mkdtempSync(path.join(tmpdir(), "remnic-meeting-mem-"));
|
|
331
|
+
try {
|
|
332
|
+
const storage = new StorageManager(dir);
|
|
333
|
+
const writer = createMeetingMemoryWriter(storage);
|
|
334
|
+
const cfg = meetingConfig({ summaryMode: "off" });
|
|
335
|
+
const generator = createMeetingMemoryGenerator(writer, cfg);
|
|
336
|
+
const rec = record();
|
|
337
|
+
// Both builds hand the record as BUILT (not unchanged) so the writer-level
|
|
338
|
+
// source+content dedup is what must hold end to end — proving the episode is
|
|
339
|
+
// idempotent through createMeetingMemoryGenerator + createMeetingMemoryWriter
|
|
340
|
+
// even if the seam's unchanged-skip did not fire.
|
|
341
|
+
const first = await generator.onRecordsBuilt({ built: [rec], removedIds: [], unchangedIds: [], updatedIds: [] });
|
|
342
|
+
assert.deepEqual(first.episodes, { written: 1, skipped: 0 });
|
|
343
|
+
const second = await generator.onRecordsBuilt({ built: [rec], removedIds: [], unchangedIds: [], updatedIds: [] });
|
|
344
|
+
assert.deepEqual(second.episodes, { written: 0, skipped: 1 }, "the unchanged-day rebuild writes no duplicate episode");
|
|
345
|
+
const episodes = (await storage.readAllMemories()).filter(
|
|
346
|
+
(m) => m.frontmatter.source === meetingSourceLabel(rec.id),
|
|
347
|
+
);
|
|
348
|
+
assert.equal(episodes.length, 1, "exactly one episode persisted after two production builds");
|
|
349
|
+
} finally {
|
|
350
|
+
rmSync(dir, { recursive: true, force: true });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("finding 2 — the generator skips an unchanged id WHOSE memories exist: zero extractor calls, nothing new written", async () => {
|
|
355
|
+
const writer = new FakeWriter();
|
|
356
|
+
const extractor = spyExtractor("summary", [{ content: "we decided to ship", category: "decision", confidence: 0.9 }]);
|
|
357
|
+
const cfg = meetingConfig({ summaryMode: "smart" });
|
|
358
|
+
const generator = createMeetingMemoryGenerator(writer, cfg, { extractor });
|
|
359
|
+
const rec = TRANSCRIPT_RECORD();
|
|
360
|
+
// Seed: a prior build generated this record's episode + facts.
|
|
361
|
+
await generator.onRecordsBuilt({ built: [rec], removedIds: [], unchangedIds: [], updatedIds: [] });
|
|
362
|
+
const extractorCallsAfterSeed = extractor.calls;
|
|
363
|
+
const writesAfterSeed = writer.writes.length;
|
|
364
|
+
assert.ok(writesAfterSeed > 0, "the seed build wrote memories");
|
|
365
|
+
// Rebuild: the id is unchanged AND its memories already exist → the generator
|
|
366
|
+
// must skip it: no episode re-write, and crucially no LLM extractor call.
|
|
367
|
+
const outcome = await generator.onRecordsBuilt({
|
|
368
|
+
built: [rec],
|
|
369
|
+
removedIds: [],
|
|
370
|
+
unchangedIds: [rec.id],
|
|
371
|
+
updatedIds: [],
|
|
372
|
+
});
|
|
373
|
+
assert.equal(extractor.calls, extractorCallsAfterSeed, "the LLM extractor never re-runs on an idempotent rebuild");
|
|
374
|
+
assert.equal(writer.writes.length, writesAfterSeed, "no new memory is written for an unchanged record");
|
|
375
|
+
assert.equal(outcome.episodes, undefined, "no episode aggregate when everything was skipped");
|
|
376
|
+
assert.equal(outcome.facts, undefined, "no fact aggregate when everything was skipped");
|
|
377
|
+
assert.equal(outcome.reindexNeeded, false, "a no-op rebuild needs no reindex");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("finding 2b — an unchanged id whose memories are MISSING is regenerated (retryable after a prior generation throw)", async () => {
|
|
381
|
+
const writer = new FakeWriter();
|
|
382
|
+
const extractor = spyExtractor("summary", [{ content: "we decided to ship", category: "decision", confidence: 0.9 }]);
|
|
383
|
+
const cfg = meetingConfig({ summaryMode: "smart" });
|
|
384
|
+
const generator = createMeetingMemoryGenerator(writer, cfg, { extractor });
|
|
385
|
+
const rec = TRANSCRIPT_RECORD();
|
|
386
|
+
// Build A persisted the record but its generation threw AFTER the record save,
|
|
387
|
+
// so NO episode/summary memory exists for meeting:<id>. Model that state as a
|
|
388
|
+
// writer that holds nothing for the source.
|
|
389
|
+
assert.equal(writer.writes.length, 0, "no memories exist yet (generation threw before writing)");
|
|
390
|
+
const source = meetingSourceLabel(rec.id);
|
|
391
|
+
assert.equal(await writer.hasMemoryFromSource(source, composeMeetingEpisodeContent(rec)), false);
|
|
392
|
+
// Build B: the store reports the record UNCHANGED (contentHash identical). A plain
|
|
393
|
+
// skip would strand the meeting with zero memories forever; the generator must
|
|
394
|
+
// instead regenerate because the expected episode is absent.
|
|
395
|
+
const outcome = await generator.onRecordsBuilt({
|
|
396
|
+
built: [rec],
|
|
397
|
+
removedIds: [],
|
|
398
|
+
unchangedIds: [rec.id],
|
|
399
|
+
updatedIds: [],
|
|
400
|
+
});
|
|
401
|
+
assert.deepEqual(outcome.episodes, { written: 1, skipped: 0 }, "the missing episode is regenerated on the retry build");
|
|
402
|
+
assert.equal(extractor.calls, 1, "the summary/facts layer runs on the retry too");
|
|
403
|
+
assert.equal(await writer.hasMemoryFromSource(source, composeMeetingEpisodeContent(rec)), true, "the episode now resolves");
|
|
404
|
+
// Build C: memories now present → unchanged rebuild skips (idempotent, no LLM).
|
|
405
|
+
const again = await generator.onRecordsBuilt({
|
|
406
|
+
built: [rec],
|
|
407
|
+
removedIds: [],
|
|
408
|
+
unchangedIds: [rec.id],
|
|
409
|
+
updatedIds: [],
|
|
410
|
+
});
|
|
411
|
+
assert.equal(extractor.calls, 1, "with memories present, the extractor never re-runs");
|
|
412
|
+
assert.equal(again.episodes, undefined, "with the episode present, an unchanged rebuild skips generation");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("finding 3 — the generator refreshes an updated id: retract then regenerate, no duplicate", async () => {
|
|
416
|
+
const writer = new FakeWriter();
|
|
417
|
+
const extractor = spyExtractor("summary", [{ content: "we decided to ship", category: "decision", confidence: 0.9 }]);
|
|
418
|
+
const cfg = meetingConfig({ summaryMode: "smart" });
|
|
419
|
+
const generator = createMeetingMemoryGenerator(writer, cfg, { extractor });
|
|
420
|
+
const rec = TRANSCRIPT_RECORD();
|
|
421
|
+
await generator.onRecordsBuilt({ built: [rec], removedIds: [], unchangedIds: [], updatedIds: [] });
|
|
422
|
+
const writesAfterFirst = writer.writes.length;
|
|
423
|
+
assert.ok(writesAfterFirst > 0, "the first build wrote memories");
|
|
424
|
+
const source = meetingSourceLabel(rec.id);
|
|
425
|
+
|
|
426
|
+
// Same id, changed content: the record is signalled as updated. The generator
|
|
427
|
+
// must retract the prior memories for the source, then regenerate.
|
|
428
|
+
const changed = record({ app: "Meet" }, { transcript: [segment("we shipped it")], corroboratedBy: [] });
|
|
429
|
+
const outcome = await generator.onRecordsBuilt({
|
|
430
|
+
built: [changed],
|
|
431
|
+
removedIds: [],
|
|
432
|
+
unchangedIds: [],
|
|
433
|
+
updatedIds: [changed.id],
|
|
434
|
+
});
|
|
435
|
+
assert.equal(writer.retired.filter((s) => s === source).length, 1, "the updated meeting's prior memories were retracted once");
|
|
436
|
+
assert.equal(extractor.calls, 2, "the updated record is re-extracted");
|
|
437
|
+
assert.deepEqual(outcome.episodes, { written: 1, skipped: 0 }, "a fresh episode is regenerated after the retract");
|
|
438
|
+
// After retract, only the refreshed copy resolves for the source — no stale duplicate.
|
|
439
|
+
const stale = composeMeetingEpisodeContent(rec);
|
|
440
|
+
const fresh = composeMeetingEpisodeContent(changed);
|
|
441
|
+
assert.notEqual(stale, fresh, "the changed record renders a different episode");
|
|
442
|
+
assert.equal(await writer.hasMemoryFromSource(source, stale), false, "the stale episode no longer resolves");
|
|
443
|
+
assert.equal(await writer.hasMemoryFromSource(source, fresh), true, "the refreshed episode resolves");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("finding 4 — a throwing durability judge degrades gracefully, queueing candidates instead of failing", async () => {
|
|
447
|
+
const writer = new FakeWriter();
|
|
448
|
+
const extractor = spyExtractor("We shipped v2.", [
|
|
449
|
+
{ content: "Team decided to ship v2 on Friday", category: "decision", confidence: 0.9 },
|
|
450
|
+
]);
|
|
451
|
+
const judge: MeetingSummaryJudge = {
|
|
452
|
+
async judge() {
|
|
453
|
+
throw new Error("judge provider timed out");
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
// review mode routes every survivor to the review queue via the bands — with a
|
|
457
|
+
// throwing judge the pass must still COMPLETE (no exception) and queue the
|
|
458
|
+
// candidate rather than aborting the whole day build.
|
|
459
|
+
const cfg = meetingConfig({ summaryMode: "review" });
|
|
460
|
+
const rec = TRANSCRIPT_RECORD();
|
|
461
|
+
const result = await generateMeetingSummaryFacts(rec, cfg, { extractor, judge, writer });
|
|
462
|
+
assert.equal(result.llmInvoked, true, "the extractor still ran");
|
|
463
|
+
assert.equal(result.review, 1, "the candidate is queued for review despite the judge throwing");
|
|
464
|
+
assert.ok(
|
|
465
|
+
result.warnings.some((w) => w.includes("judge unavailable")),
|
|
466
|
+
"a degradation warning records the judge failure",
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("finding 5 — a tombstone-blocked fact write is counted as blocked, not active/written", async () => {
|
|
471
|
+
const extractor = spyExtractor("We shipped v2.", [
|
|
472
|
+
{ content: "Team decided to ship v2 on Friday", category: "fact", confidence: 0.95 },
|
|
473
|
+
]);
|
|
474
|
+
// Writer whose sealed writes always report the #1579 tombstone block (the
|
|
475
|
+
// chokepoint downgraded the fact to pending_review — no active copy).
|
|
476
|
+
const blocked = new Set<string>();
|
|
477
|
+
const writer: MeetingMemoryWriter = {
|
|
478
|
+
async writeSealedMemory(envelope, extras) {
|
|
479
|
+
if (extras.contentHashSource !== undefined) {
|
|
480
|
+
blocked.add(`${envelope.source}\u0000${extras.contentHashSource}`);
|
|
481
|
+
}
|
|
482
|
+
return { id: "blocked-1", tombstoneBlocked: true, blockedBy: "tomb-1" };
|
|
483
|
+
},
|
|
484
|
+
async hasMemoryFromSource(source, content) {
|
|
485
|
+
return blocked.has(`${source}\u0000${content}`);
|
|
486
|
+
},
|
|
487
|
+
async retireMemoriesFromSource() {
|
|
488
|
+
return 0;
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
const cfg = meetingConfig({ summaryMode: "smart" });
|
|
492
|
+
const rec = TRANSCRIPT_RECORD();
|
|
493
|
+
const result = await generateMeetingSummaryFacts(rec, cfg, { extractor, writer });
|
|
494
|
+
assert.equal(result.active, 0, "a tombstone-blocked write must NOT be reported active");
|
|
495
|
+
assert.equal(result.review, 0, "a tombstone-blocked write must NOT be reported review");
|
|
496
|
+
assert.ok(result.tombstoneBlocked >= 1, "the blocked write is counted as tombstone-blocked");
|
|
497
|
+
assert.equal(result.summaryWritten, false, "a blocked summary is not reported as written");
|
|
498
|
+
});
|