@remnic/core 9.19.0 → 9.20.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,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting record composition, serialization, and the on-disk store (#1900).
|
|
3
|
+
*
|
|
4
|
+
* One markdown file per meeting under `<memoryDir>/meetings/<YYYY-MM-DD>/<id>.md`
|
|
5
|
+
* with YAML frontmatter. Placement mirrors the wearables day-store rationale:
|
|
6
|
+
*
|
|
7
|
+
* - OUTSIDE the memory scan roots (facts/, procedures/, …) so meeting
|
|
8
|
+
* transcripts never surface as memories in recall or governance passes;
|
|
9
|
+
* - INSIDE the QMD collection root (the memory dir) so records are full-text
|
|
10
|
+
* searchable after the next index update — but never auto-recalled raw.
|
|
11
|
+
*
|
|
12
|
+
* Records rebuild idempotently on `contentHash`: an unchanged meeting re-writes
|
|
13
|
+
* nothing. The transcript body reuses the wearables day-store line grammar
|
|
14
|
+
* (`**Name** [HH:MM]: text`, escape-aware) so existing parsers apply. File IO is
|
|
15
|
+
* injected (like the fusion artifact store) so encrypted-at-rest + atomic-write
|
|
16
|
+
* semantics are inherited without this module touching the secure-store key.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { pathIsInside } from "../utils/path-containment.js";
|
|
23
|
+
import { escapeSegmentText, escapeSpeakerLabel, isValidTranscriptDate } from "../wearables/day-store.js";
|
|
24
|
+
import type { FusedSegment } from "../wearables/fusion/types.js";
|
|
25
|
+
import type { FusedMeeting, MeetingDetectionSource, MeetingRecord } from "./types.js";
|
|
26
|
+
|
|
27
|
+
export const MEETINGS_DIR_NAME = "meetings";
|
|
28
|
+
export const MEETING_KIND = "meeting" as const;
|
|
29
|
+
export const MEETING_FORMAT_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
/** `mtg-YYYY-MM-DD-<8 hex>` — the id shape derived by `meetingId`. */
|
|
32
|
+
const MEETING_ID_PATTERN = /^mtg-(\d{4}-\d{2}-\d{2})-[0-9a-f]{8}$/;
|
|
33
|
+
|
|
34
|
+
/** The detection-source label from a meeting id would be ambiguous, so validate
|
|
35
|
+
* an id AND confirm its embedded date matches the directory it lives under. */
|
|
36
|
+
export function meetingIdDate(id: string): string | null {
|
|
37
|
+
const match = MEETING_ID_PATTERN.exec(id);
|
|
38
|
+
return match === null ? null : match[1]!;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Base fields (identity + detection) a fused meeting is composed with. */
|
|
42
|
+
export interface MeetingRecordBase {
|
|
43
|
+
id: string;
|
|
44
|
+
date: string;
|
|
45
|
+
startUtc: string;
|
|
46
|
+
endUtc: string;
|
|
47
|
+
app?: string;
|
|
48
|
+
detectionSource: MeetingDetectionSource;
|
|
49
|
+
title?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Canonical semantic content hashed for idempotency (excludes contentHash). */
|
|
53
|
+
function canonicalContent(base: MeetingRecordBase, fused: FusedMeeting): string {
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
formatVersion: MEETING_FORMAT_VERSION,
|
|
56
|
+
id: base.id,
|
|
57
|
+
date: base.date,
|
|
58
|
+
startUtc: base.startUtc,
|
|
59
|
+
endUtc: base.endUtc,
|
|
60
|
+
app: base.app ?? null,
|
|
61
|
+
detectionSource: base.detectionSource,
|
|
62
|
+
title: base.title ?? null,
|
|
63
|
+
attendees: fused.attendees,
|
|
64
|
+
sources: fused.sources,
|
|
65
|
+
corroboratedBy: fused.corroboratedBy,
|
|
66
|
+
screenContext: fused.screenContext,
|
|
67
|
+
contextExcerpts: fused.contextExcerpts,
|
|
68
|
+
transcript: fused.transcript,
|
|
69
|
+
speakers: fused.speakers,
|
|
70
|
+
snapshotCount: fused.snapshotCount,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Compose a full meeting record (with contentHash) from its base + fused view. */
|
|
75
|
+
export function composeMeetingRecord(base: MeetingRecordBase, fused: FusedMeeting): MeetingRecord {
|
|
76
|
+
const contentHash = createHash("sha256").update(canonicalContent(base, fused), "utf-8").digest("hex");
|
|
77
|
+
return {
|
|
78
|
+
...base,
|
|
79
|
+
...fused,
|
|
80
|
+
contentHash,
|
|
81
|
+
formatVersion: MEETING_FORMAT_VERSION,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function formatClockUtc(iso: string): string {
|
|
86
|
+
const ms = Date.parse(iso);
|
|
87
|
+
if (Number.isNaN(ms)) return "--:--";
|
|
88
|
+
return new Date(ms).toISOString().slice(11, 16);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderTranscriptLine(segment: FusedSegment): string {
|
|
92
|
+
const clock = formatClockUtc(segment.startIso ?? "");
|
|
93
|
+
return `**${escapeSpeakerLabel(segment.speaker)}** [${clock}]: ${escapeSegmentText(segment.text)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function frontmatterList(key: string, values: readonly string[]): string[] {
|
|
97
|
+
if (values.length === 0) return [`${key}: []`];
|
|
98
|
+
const lines = [`${key}:`];
|
|
99
|
+
for (const value of values) lines.push(` - ${JSON.stringify(value)}`);
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Serialize a meeting record into its persisted markdown form. */
|
|
104
|
+
export function serializeMeetingRecord(record: MeetingRecord): string {
|
|
105
|
+
const lines: string[] = ["---"];
|
|
106
|
+
lines.push(`kind: ${MEETING_KIND}`);
|
|
107
|
+
lines.push(`formatVersion: ${record.formatVersion}`);
|
|
108
|
+
lines.push(`id: ${JSON.stringify(record.id)}`);
|
|
109
|
+
lines.push(`date: ${JSON.stringify(record.date)}`);
|
|
110
|
+
lines.push(`startUtc: ${JSON.stringify(record.startUtc)}`);
|
|
111
|
+
lines.push(`endUtc: ${JSON.stringify(record.endUtc)}`);
|
|
112
|
+
if (record.app !== undefined) lines.push(`app: ${JSON.stringify(record.app)}`);
|
|
113
|
+
lines.push(`detectionSource: ${JSON.stringify(record.detectionSource)}`);
|
|
114
|
+
if (record.title !== undefined) lines.push(`title: ${JSON.stringify(record.title)}`);
|
|
115
|
+
lines.push(...frontmatterList("attendees", record.attendees));
|
|
116
|
+
lines.push(...frontmatterList("sources", record.sources));
|
|
117
|
+
lines.push(...frontmatterList("corroboratedBy", record.corroboratedBy));
|
|
118
|
+
lines.push(`snapshotCount: ${record.snapshotCount}`);
|
|
119
|
+
lines.push(`contentHash: ${JSON.stringify(record.contentHash)}`);
|
|
120
|
+
lines.push("---");
|
|
121
|
+
lines.push("");
|
|
122
|
+
|
|
123
|
+
const heading = record.title !== undefined && record.title.length > 0 ? ` · ${record.title}` : "";
|
|
124
|
+
lines.push(`# Meeting ${record.id}${heading}`);
|
|
125
|
+
lines.push("");
|
|
126
|
+
|
|
127
|
+
lines.push("## Attendees");
|
|
128
|
+
lines.push("");
|
|
129
|
+
if (record.attendees.length === 0) {
|
|
130
|
+
lines.push("_None resolved._");
|
|
131
|
+
} else {
|
|
132
|
+
for (const attendee of record.attendees) lines.push(`- ${attendee}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("");
|
|
135
|
+
|
|
136
|
+
if (record.screenContext.length > 0 || record.contextExcerpts.length > 0) {
|
|
137
|
+
lines.push("## Screen context");
|
|
138
|
+
lines.push("");
|
|
139
|
+
for (const event of record.screenContext) {
|
|
140
|
+
lines.push(`- [${event.clock}] ${event.label}`);
|
|
141
|
+
}
|
|
142
|
+
if (record.contextExcerpts.length > 0) {
|
|
143
|
+
if (record.screenContext.length > 0) lines.push("");
|
|
144
|
+
lines.push("Notable excerpts:");
|
|
145
|
+
lines.push("");
|
|
146
|
+
for (const excerpt of record.contextExcerpts) lines.push(`> ${excerpt.replace(/\n/g, " ")}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push("## Transcript");
|
|
152
|
+
lines.push("");
|
|
153
|
+
if (record.transcript.length === 0) {
|
|
154
|
+
lines.push("_No transcript segments in this window._");
|
|
155
|
+
} else {
|
|
156
|
+
for (const segment of record.transcript) lines.push(renderTranscriptLine(segment));
|
|
157
|
+
}
|
|
158
|
+
lines.push("");
|
|
159
|
+
|
|
160
|
+
return `${lines.join("\n")}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A meeting record's frontmatter, parsed for listing + idempotency checks. */
|
|
164
|
+
export interface MeetingRecordSummary {
|
|
165
|
+
id: string;
|
|
166
|
+
date: string;
|
|
167
|
+
startUtc: string;
|
|
168
|
+
endUtc: string;
|
|
169
|
+
app?: string;
|
|
170
|
+
detectionSource: string;
|
|
171
|
+
title?: string;
|
|
172
|
+
attendees: string[];
|
|
173
|
+
sources: string[];
|
|
174
|
+
corroboratedBy: string[];
|
|
175
|
+
snapshotCount: number;
|
|
176
|
+
contentHash: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseScalar(value: string): string {
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
if (trimmed.startsWith('"')) {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(trimmed) as string;
|
|
184
|
+
} catch {
|
|
185
|
+
return trimmed;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return trimmed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Parse a meeting record's frontmatter. Returns null when the content is not a
|
|
192
|
+
* meeting record (wrong kind, missing frontmatter) so callers distinguish a
|
|
193
|
+
* non-record from a corrupt one. */
|
|
194
|
+
export function parseMeetingRecordSummary(raw: string): MeetingRecordSummary | null {
|
|
195
|
+
if (!raw.startsWith("---\n")) return null;
|
|
196
|
+
const closeIndex = raw.indexOf("\n---\n", 4);
|
|
197
|
+
if (closeIndex === -1) return null;
|
|
198
|
+
const frontmatter = raw.slice(4, closeIndex).split("\n");
|
|
199
|
+
|
|
200
|
+
const scalars: Record<string, string> = {};
|
|
201
|
+
const lists: Record<string, string[]> = {};
|
|
202
|
+
let currentList: string | null = null;
|
|
203
|
+
for (const line of frontmatter) {
|
|
204
|
+
const listItem = /^ {2}- (.*)$/.exec(line);
|
|
205
|
+
if (listItem !== null && currentList !== null) {
|
|
206
|
+
lists[currentList]!.push(parseScalar(listItem[1]!));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const kv = /^([A-Za-z][A-Za-z0-9]*):\s*(.*)$/.exec(line);
|
|
210
|
+
if (kv === null) continue;
|
|
211
|
+
const key = kv[1]!;
|
|
212
|
+
const value = kv[2]!;
|
|
213
|
+
if (value === "") {
|
|
214
|
+
currentList = key;
|
|
215
|
+
lists[key] = [];
|
|
216
|
+
} else if (value === "[]") {
|
|
217
|
+
currentList = null;
|
|
218
|
+
lists[key] = [];
|
|
219
|
+
} else {
|
|
220
|
+
currentList = null;
|
|
221
|
+
scalars[key] = value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (scalars.kind !== undefined && parseScalar(scalars.kind) !== MEETING_KIND) return null;
|
|
226
|
+
const id = scalars.id !== undefined ? parseScalar(scalars.id) : undefined;
|
|
227
|
+
const contentHash = scalars.contentHash !== undefined ? parseScalar(scalars.contentHash) : undefined;
|
|
228
|
+
if (id === undefined || contentHash === undefined) return null;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
date: scalars.date !== undefined ? parseScalar(scalars.date) : "",
|
|
233
|
+
startUtc: scalars.startUtc !== undefined ? parseScalar(scalars.startUtc) : "",
|
|
234
|
+
endUtc: scalars.endUtc !== undefined ? parseScalar(scalars.endUtc) : "",
|
|
235
|
+
...(scalars.app !== undefined ? { app: parseScalar(scalars.app) } : {}),
|
|
236
|
+
detectionSource: scalars.detectionSource !== undefined ? parseScalar(scalars.detectionSource) : "",
|
|
237
|
+
...(scalars.title !== undefined ? { title: parseScalar(scalars.title) } : {}),
|
|
238
|
+
attendees: lists.attendees ?? [],
|
|
239
|
+
sources: lists.sources ?? [],
|
|
240
|
+
corroboratedBy: lists.corroboratedBy ?? [],
|
|
241
|
+
snapshotCount: scalars.snapshotCount !== undefined ? Number(scalars.snapshotCount) || 0 : 0,
|
|
242
|
+
contentHash,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Encrypted-at-rest + atomic file IO the meeting record store needs. Satisfied
|
|
248
|
+
* by StorageManager (which owns the secure-store key + atomic write path) and
|
|
249
|
+
* injected so this module performs NO direct fs — records inherit the same
|
|
250
|
+
* encrypted-at-rest + atomic-write semantics as memories. Same shape as the
|
|
251
|
+
* fusion artifact store's IO port (kept per-module, not shared, so each store's
|
|
252
|
+
* boundary stays explicit).
|
|
253
|
+
*/
|
|
254
|
+
export interface MeetingRecordFileIo {
|
|
255
|
+
writeFile(filePath: string, content: string): Promise<void>;
|
|
256
|
+
readFile(filePath: string): Promise<string>;
|
|
257
|
+
readDir(dirPath: string): Promise<string[]>;
|
|
258
|
+
deleteFile(filePath: string): Promise<void>;
|
|
259
|
+
realpath(filePath: string): Promise<string>;
|
|
260
|
+
lstat(filePath: string): Promise<{ isSymbolicLink: boolean }>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Result of an idempotent record save. */
|
|
264
|
+
export interface MeetingSaveResult {
|
|
265
|
+
path: string;
|
|
266
|
+
/** False when the on-disk contentHash already matched (nothing rewritten). */
|
|
267
|
+
written: boolean;
|
|
268
|
+
contentHash: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Owns the meeting record files under `<memoryDir>/meetings/<date>/<id>.md`.
|
|
273
|
+
* Path construction, date/id validation, and symlink containment live here; the
|
|
274
|
+
* bytes flow through the injected secure IO. Containment is anchored to the real
|
|
275
|
+
* memory-dir root and mirrors the fusion artifact store (AGENTS.md pattern #3).
|
|
276
|
+
*/
|
|
277
|
+
export class MeetingRecordStore {
|
|
278
|
+
private readonly meetingsDir: string;
|
|
279
|
+
|
|
280
|
+
constructor(
|
|
281
|
+
private readonly memoryDir: string,
|
|
282
|
+
private readonly io: MeetingRecordFileIo,
|
|
283
|
+
) {
|
|
284
|
+
this.meetingsDir = path.join(memoryDir, MEETINGS_DIR_NAME);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
recordPath(date: string, id: string): string {
|
|
288
|
+
if (!isValidTranscriptDate(date)) {
|
|
289
|
+
throw new Error(`invalid meeting date '${String(date)}' — expected YYYY-MM-DD`);
|
|
290
|
+
}
|
|
291
|
+
const idDate = meetingIdDate(id);
|
|
292
|
+
if (idDate === null) {
|
|
293
|
+
throw new Error(`invalid meeting id '${String(id)}' — expected mtg-YYYY-MM-DD-<hash>`);
|
|
294
|
+
}
|
|
295
|
+
if (idDate !== date) {
|
|
296
|
+
throw new Error(`meeting id '${id}' does not belong to date '${date}'`);
|
|
297
|
+
}
|
|
298
|
+
return path.join(this.meetingsDir, date, `${id}.md`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Idempotently persist a record: skip the write when contentHash is unchanged. */
|
|
302
|
+
async saveMeetingRecord(record: MeetingRecord): Promise<MeetingSaveResult> {
|
|
303
|
+
const filePath = this.recordPath(record.date, record.id);
|
|
304
|
+
await this.assertPathContained(filePath);
|
|
305
|
+
const existing = await this.readRaw(filePath);
|
|
306
|
+
if (existing !== null) {
|
|
307
|
+
const summary = parseMeetingRecordSummary(existing);
|
|
308
|
+
if (summary !== null && summary.contentHash === record.contentHash) {
|
|
309
|
+
return { path: filePath, written: false, contentHash: record.contentHash };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
await this.io.writeFile(filePath, serializeMeetingRecord(record));
|
|
313
|
+
return { path: filePath, written: true, contentHash: record.contentHash };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Read a stored record's raw markdown; null when absent. */
|
|
317
|
+
async readMeetingRecord(date: string, id: string): Promise<string | null> {
|
|
318
|
+
const filePath = this.recordPath(date, id);
|
|
319
|
+
await this.assertPathContained(filePath);
|
|
320
|
+
return this.readRaw(filePath);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Read + parse every stored record for a day exactly ONCE, sorted by start
|
|
325
|
+
* time (ascending, id tiebreak). The single day-read path both `listMeetingIds`
|
|
326
|
+
* and `listMeetingSummaries` derive from — no record is read twice.
|
|
327
|
+
*/
|
|
328
|
+
private async readDaySummaries(date: string): Promise<MeetingRecordSummary[]> {
|
|
329
|
+
if (!isValidTranscriptDate(date)) {
|
|
330
|
+
throw new Error(`invalid meeting date '${String(date)}' — expected YYYY-MM-DD`);
|
|
331
|
+
}
|
|
332
|
+
const dayDir = path.join(this.meetingsDir, date);
|
|
333
|
+
await this.assertPathContained(dayDir);
|
|
334
|
+
let entries: string[];
|
|
335
|
+
try {
|
|
336
|
+
entries = await this.io.readDir(dayDir);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
const summaries: MeetingRecordSummary[] = [];
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (!entry.endsWith(".md")) continue;
|
|
344
|
+
const id = entry.slice(0, -3);
|
|
345
|
+
if (meetingIdDate(id) !== date) continue;
|
|
346
|
+
const raw = await this.readMeetingRecord(date, id);
|
|
347
|
+
if (raw === null) continue;
|
|
348
|
+
const summary = parseMeetingRecordSummary(raw);
|
|
349
|
+
if (summary !== null) summaries.push(summary);
|
|
350
|
+
}
|
|
351
|
+
summaries.sort((a, b) => (a.startUtc < b.startUtc ? -1 : a.startUtc > b.startUtc ? 1 : SORT_STR(a.id, b.id)));
|
|
352
|
+
return summaries;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** List meeting ids stored for a day, sorted by start time (ascending). */
|
|
356
|
+
async listMeetingIds(date: string): Promise<string[]> {
|
|
357
|
+
return (await this.readDaySummaries(date)).map((summary) => summary.id);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Read parsed summaries for a day, sorted by start time. */
|
|
361
|
+
async listMeetingSummaries(date: string): Promise<MeetingRecordSummary[]> {
|
|
362
|
+
return this.readDaySummaries(date);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** List dates that have at least one stored meeting record, newest first. */
|
|
366
|
+
async listMeetingDates(): Promise<string[]> {
|
|
367
|
+
await this.assertPathContained(this.meetingsDir);
|
|
368
|
+
let entries: string[];
|
|
369
|
+
try {
|
|
370
|
+
entries = await this.io.readDir(this.meetingsDir);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
const dates = entries.filter((entry) => isValidTranscriptDate(entry));
|
|
376
|
+
dates.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
|
|
377
|
+
return dates;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Remove a stored record. Idempotent: a no-op when already absent. */
|
|
381
|
+
async deleteMeetingRecord(date: string, id: string): Promise<void> {
|
|
382
|
+
const filePath = this.recordPath(date, id);
|
|
383
|
+
await this.assertPathContained(filePath);
|
|
384
|
+
try {
|
|
385
|
+
await this.io.deleteFile(filePath);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async readRaw(filePath: string): Promise<string | null> {
|
|
393
|
+
try {
|
|
394
|
+
return await this.io.readFile(filePath);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async assertPathContained(targetPath: string): Promise<void> {
|
|
402
|
+
let rootReal: string;
|
|
403
|
+
try {
|
|
404
|
+
rootReal = await this.io.realpath(this.memoryDir);
|
|
405
|
+
} catch {
|
|
406
|
+
rootReal = path.resolve(this.memoryDir);
|
|
407
|
+
}
|
|
408
|
+
await this.assertNoEscape(rootReal, this.meetingsDir);
|
|
409
|
+
await this.assertNoEscape(rootReal, path.dirname(targetPath));
|
|
410
|
+
await this.assertNoEscape(rootReal, targetPath);
|
|
411
|
+
// Reject a symlinked meetings root OR a symlinked intermediate day directory
|
|
412
|
+
// outright — even when the link resolves inside the memory dir (aliasing /
|
|
413
|
+
// tampering) — before any read/write (AGENTS.md pattern #3).
|
|
414
|
+
await this.assertNotSymlink(this.meetingsDir, "directory");
|
|
415
|
+
const dayDir = this.dayDirWithin(targetPath);
|
|
416
|
+
if (dayDir !== null) await this.assertNotSymlink(dayDir, "directory");
|
|
417
|
+
// Reject a symlinked LEAF record file too: lstat'ing only the root + day dir
|
|
418
|
+
// let a pre-existing `<id>.md` symlink (e.g. into facts/) pass realpath
|
|
419
|
+
// containment, so the read/write would FOLLOW the link and leak raw
|
|
420
|
+
// transcript into the recall corpus. Refuse the link before any IO.
|
|
421
|
+
if (targetPath.endsWith(".md")) await this.assertNotSymlink(targetPath, "record file");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The `<meetings>/<date>` directory on the path to `targetPath`, or null when
|
|
425
|
+
* `targetPath` is the meetings root itself. */
|
|
426
|
+
private dayDirWithin(targetPath: string): string | null {
|
|
427
|
+
const rel = path.relative(this.meetingsDir, targetPath);
|
|
428
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
429
|
+
const first = rel.split(path.sep)[0];
|
|
430
|
+
if (first === undefined || first.length === 0) return null;
|
|
431
|
+
return path.join(this.meetingsDir, first);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private async assertNotSymlink(target: string, kind: "directory" | "record file"): Promise<void> {
|
|
435
|
+
let stat: { isSymbolicLink: boolean };
|
|
436
|
+
try {
|
|
437
|
+
stat = await this.io.lstat(target);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
if (stat.isSymbolicLink) {
|
|
443
|
+
const label = path.relative(this.memoryDir, target) || MEETINGS_DIR_NAME;
|
|
444
|
+
throw new Error(
|
|
445
|
+
`meetings ${kind} '${label}' is a symbolic link — refusing to follow it even when it resolves inside the memory dir (AGENTS.md pattern #3)`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private async assertNoEscape(rootReal: string, candidate: string): Promise<void> {
|
|
451
|
+
let candidateReal: string;
|
|
452
|
+
try {
|
|
453
|
+
candidateReal = await this.io.realpath(candidate);
|
|
454
|
+
} catch {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (!pathIsInside(rootReal, candidateReal)) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
"meeting record path resolves outside the memory dir — refusing to follow a symlink/traversal (AGENTS.md pattern #3)",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const SORT_STR = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
package/src/meetings/types.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* timestamps are UTC ISO-8601; windows are half-open [startUtc, endUtc).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type {
|
|
12
|
+
FusedSegment,
|
|
13
|
+
FusedSpeaker,
|
|
14
|
+
FusionConversationInput,
|
|
15
|
+
} from "../wearables/fusion/types.js";
|
|
16
|
+
|
|
11
17
|
/** A contiguous meeting-app foreground span (derived from activity in a later slice). */
|
|
12
18
|
export interface MeetingAppSpan {
|
|
13
19
|
/** Meeting app label (e.g. "Zoom", "Google Meet"). */
|
|
@@ -74,3 +80,108 @@ export interface MeetingsDetectionConfig {
|
|
|
74
80
|
/** Merge candidates within this gap of each other (minutes). */
|
|
75
81
|
mergeGapMinutes: number;
|
|
76
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One screen-activity snapshot fed into meeting fusion (assembled from the
|
|
86
|
+
* activity store by a later wiring slice; injected directly in tests). Text is
|
|
87
|
+
* already post-redaction — meeting fusion never sees raw capture.
|
|
88
|
+
*/
|
|
89
|
+
export interface MeetingActivitySnapshot {
|
|
90
|
+
/** ISO 8601 UTC instant the snapshot was captured. */
|
|
91
|
+
tsUtc: string;
|
|
92
|
+
/** Foreground application label (e.g. "Preview", "Chrome", "Zoom"). */
|
|
93
|
+
app: string;
|
|
94
|
+
/** Window title, when known. */
|
|
95
|
+
title?: string;
|
|
96
|
+
/** Browser URL, when known. */
|
|
97
|
+
url?: string;
|
|
98
|
+
/** Extracted on-screen text excerpt, when known. */
|
|
99
|
+
text?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One entry in a meeting's screen-context timeline (an other-app dwell). */
|
|
103
|
+
export interface MeetingScreenContextEvent {
|
|
104
|
+
/** ISO 8601 UTC instant the dwell began (clamped to the meeting window). */
|
|
105
|
+
tsUtc: string;
|
|
106
|
+
/** `HH:MM` (UTC) render used on the timeline line. */
|
|
107
|
+
clock: string;
|
|
108
|
+
/** App label. */
|
|
109
|
+
app: string;
|
|
110
|
+
/** Compact label, e.g. `Preview: Q3-roadmap.pdf` or `Chrome: github.com/x`. */
|
|
111
|
+
label: string;
|
|
112
|
+
/** Foreground dwell within the meeting window, in whole seconds. */
|
|
113
|
+
dwellSeconds: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** LLM meeting-summary mode: off (episode only), review (all to review queue),
|
|
117
|
+
* or smart (trust-gated). */
|
|
118
|
+
export type MeetingSummaryMode = "off" | "review" | "smart";
|
|
119
|
+
|
|
120
|
+
/** Detection-plus-fusion configuration for meeting building. */
|
|
121
|
+
export interface MeetingsConfig extends MeetingsDetectionConfig {
|
|
122
|
+
/** Master gate for the meetings subsystem. */
|
|
123
|
+
enabled: boolean;
|
|
124
|
+
/** Min foreground dwell (seconds) for an other-app span to enter context. */
|
|
125
|
+
contextDwellSeconds: number;
|
|
126
|
+
/** Cap on total deduped screen-context excerpt characters. */
|
|
127
|
+
maxContextChars: number;
|
|
128
|
+
/** LLM summary/facts mode: off | review | smart (default smart). */
|
|
129
|
+
summaryMode: MeetingSummaryMode;
|
|
130
|
+
/** Provenance trust prior for meeting-derived facts (0..1). */
|
|
131
|
+
sourceTrust: number;
|
|
132
|
+
/** Trust at/above which a smart-mode fact is auto-approved to active. */
|
|
133
|
+
autoApproveTrust: number;
|
|
134
|
+
/** Trust at/above which a smart-mode fact is queued for review. */
|
|
135
|
+
reviewTrust: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Per-meeting build input (day conversations + activity are clipped here). */
|
|
139
|
+
export interface MeetingBuildInput {
|
|
140
|
+
meeting: DetectedMeeting;
|
|
141
|
+
/**
|
|
142
|
+
* Fusion conversation inputs across ALL wearable sources for the day. Only
|
|
143
|
+
* segments overlapping the meeting window are used; passing whole-day inputs
|
|
144
|
+
* is fine (they are clipped in `fuseMeeting`).
|
|
145
|
+
*/
|
|
146
|
+
conversations: FusionConversationInput[];
|
|
147
|
+
/** Day screen-activity snapshots (filtered to the window in `fuseMeeting`). */
|
|
148
|
+
activity?: MeetingActivitySnapshot[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The fused, screen-aware view of a meeting produced by `fuseMeeting`. */
|
|
152
|
+
export interface FusedMeeting {
|
|
153
|
+
/** Distinct resolved non-wearer attendee labels, sorted. */
|
|
154
|
+
attendees: string[];
|
|
155
|
+
/** Contributing transcript sources, sorted + de-duplicated. */
|
|
156
|
+
sources: string[];
|
|
157
|
+
/** Sources that corroborated a higher-trust pick (alternatives), sorted. */
|
|
158
|
+
corroboratedBy: string[];
|
|
159
|
+
/** Screen-context timeline (other-app dwell >= contextDwellSeconds). */
|
|
160
|
+
screenContext: MeetingScreenContextEvent[];
|
|
161
|
+
/** Deduped notable on-screen text excerpts (capped by maxContextChars). */
|
|
162
|
+
contextExcerpts: string[];
|
|
163
|
+
/** Fused, reconciled transcript segments in chronological order. */
|
|
164
|
+
transcript: FusedSegment[];
|
|
165
|
+
/** Reconciled speakers (deduped across sources). */
|
|
166
|
+
speakers: FusedSpeaker[];
|
|
167
|
+
/** Count of activity snapshots inside the meeting window. */
|
|
168
|
+
snapshotCount: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A persisted meeting record. Frontmatter + fused body serialize to
|
|
173
|
+
* `<memoryDir>/meetings/<date>/<id>.md`; rebuilt idempotently on contentHash.
|
|
174
|
+
*/
|
|
175
|
+
export interface MeetingRecord extends FusedMeeting {
|
|
176
|
+
id: string;
|
|
177
|
+
date: string;
|
|
178
|
+
startUtc: string;
|
|
179
|
+
endUtc: string;
|
|
180
|
+
app?: string;
|
|
181
|
+
detectionSource: MeetingDetectionSource;
|
|
182
|
+
title?: string;
|
|
183
|
+
/** SHA-256 over the record's canonical semantic content (idempotency key). */
|
|
184
|
+
contentHash: string;
|
|
185
|
+
/** Serializer format version (bump invalidates older files). */
|
|
186
|
+
formatVersion: number;
|
|
187
|
+
}
|
|
@@ -601,6 +601,20 @@ export function isArtifactMemoryPath(filePath: string): boolean {
|
|
|
601
601
|
return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(filePath);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Meeting records (issue #1900) live at
|
|
606
|
+
* `<root>/meetings/<YYYY-MM-DD>/mtg-<YYYY-MM-DD>-<hash>.md` — inside the QMD
|
|
607
|
+
* collection root (full-text searchable) but NEVER surfaced through generic
|
|
608
|
+
* recall, exactly like `artifacts/`. The predicate matches the FULL record
|
|
609
|
+
* shape (anchored at the filename) rather than any `meetings` path segment, so
|
|
610
|
+
* a namespaced deployment whose non-default namespace is literally named
|
|
611
|
+
* `meetings` (`.../namespaces/meetings/facts/…`) keeps its ordinary memories in
|
|
612
|
+
* recall — only true meeting records are excluded.
|
|
613
|
+
*/
|
|
614
|
+
export function isMeetingRecordPath(filePath: string): boolean {
|
|
615
|
+
return /(?:^|[\\/])meetings[\\/]\d{4}-\d{2}-\d{2}[\\/]mtg-\d{4}-\d{2}-\d{2}-[0-9a-f]{8}\.md$/i.test(filePath);
|
|
616
|
+
}
|
|
617
|
+
|
|
604
618
|
/** Exactly `activity/<YYYY-MM-DD>.md`, at the top level of a memory root. */
|
|
605
619
|
const ACTIVITY_DIGEST_TOPLEVEL = /^activity[\\/]\d{4}-\d{2}-\d{2}\.md$/i;
|
|
606
620
|
/** The digest FILE shape anywhere in a path (root-unaware best-effort). */
|
|
@@ -627,11 +641,17 @@ export function isActivityDigestPath(filePath: string, memoryRoot?: string): boo
|
|
|
627
641
|
|
|
628
642
|
/**
|
|
629
643
|
* Paths that dedicated surfaces own and generic recall must never inject:
|
|
630
|
-
* artifacts
|
|
631
|
-
*
|
|
644
|
+
* artifacts, activity digests (issue #1899), and meeting records (issue #1900).
|
|
645
|
+
* One predicate shared by every recall filter site so the exclusion cannot drift.
|
|
646
|
+
* Explicit search paths (memory_search, activity search) do not apply this
|
|
647
|
+
* filter, so those surfaces still read them.
|
|
632
648
|
*/
|
|
633
649
|
export function isGenericRecallExcludedPath(filePath: string, memoryRoot?: string): boolean {
|
|
634
|
-
return
|
|
650
|
+
return (
|
|
651
|
+
isArtifactMemoryPath(filePath) ||
|
|
652
|
+
isActivityDigestPath(filePath, memoryRoot) ||
|
|
653
|
+
isMeetingRecordPath(filePath)
|
|
654
|
+
);
|
|
635
655
|
}
|
|
636
656
|
|
|
637
657
|
export function buildCompressionGuidelinesMarkdown(
|