Meeting → RAG ingestion — design spec

Status: Planned
Author: Christophe
Date: 2026-07-21
Repo: north-os (apps/worker-ingestion-knowledge, packages/knowledge, packages/embeddings, packages/db knowledge + meeting schema, packages/agent-runtime meeting tools)
Related: WS-12 of 2026-07-21-meeting-production-readiness · review docs/reviews/2026-07-21-meeting-parity-recording-rag-state.md (Part 3) · depends on WS-1 (non-stub provider) + WS-4 (durable transcribed-event).

Problem

Meetings are absent from the RAG. The lifecycle stops at a Claude summary: source.meeting.transcribed has one consumer (the summary consumer). "What happened" search is direct SQL only — search_matter_meetings filters a substring over the derived label, not the transcript (meeting-access.ts:107-118); the code says "transcript full-text search is a follow-up" (meeting-access.ts:78). Every other piece exists: a voyage-law-2 embedder, a pgvector chunk store, hybrid retrieval, and an ingestion worker. The missing piece is a single consumer that turns a transcribed meeting into embedded chunks.

Design (fits the existing ingestion architecture)

  1. Trigger. A second consumer of source.meeting.transcribed in apps/worker-ingestion-knowledge, next to the document-changed consumer. Requires WS-4's at-least-once delivery — do not build durable ingestion on a fire-and-forget event. (Revised 2026-07-22 — see Revision below: no second consumer on the shared queue; a dedicated ingest queue fed by a ledger sweep.)
  2. Types. Extend doc_type with meeting_transcript and chunk_kind with transcript_turn (knowledge.ts:57-79), so a meeting is a first-class knowledge_sources row (matter_id, scope: "matter", source_uri = meeting id).
  3. Chunking (speaker/timestamp-aware). Window the diarized segments (speakerLabel, startMs, endMs, text) into speaker-labeled chunks — the summarizer's SPEAKER [mm:ss]: text rendering (summarize.ts:64-84) is a ready template. Each chunk carries meeting_id, matter_id, speaker_label, start_ms/end_ms, organization_id, source.
  4. Embed + upsert. voyage-law-2 (embeddings/src/embed.ts) into knowledge_chunks.embedding, reusing the ingest/commit pipeline.
  5. Tenant/RLS. knowledge_chunks/knowledge_sources are organization_id-RLS-isolated exactly like meeting/meeting_transcript; embeddings inherit the same wall. The audio is encrypted at rest, but the derived segment text is already stored plaintext and read by the summarizer — so meeting embeddings are no less protected than the existing summary path.
  6. Retrieval. Surface meeting chunks through the existing hybrid (vector+lexical) retrieval, or upgrade search_matter_meetings to a semantic mode — replacing the label-substring filter with an ANN scan, closing the follow-up TODO.

Decisions to lock before build

Acceptance

A transcribed matter meeting produces speaker/time-scoped chunks in knowledge_chunks under the correct org; a semantic query ("what did the counterparty say about the indemnity cap?") returns the right turns with click-to-source (meeting id + timestamp); purging the meeting removes its chunks (revised — see Revision: the audio purge deliberately keeps the transcript and therefore the chunks; the hard-delete lockstep ships as a tested seam wired when deleteMeeting lands); and no stub-transcribed meeting is ever ingested.

Revision — 2026-07-22 (as implemented, NOS-201)

The implementation plan was adversarially verified against main (two review rounds plus a final ack round) and refined the design bullets above. The original text is kept for the record; this section is what shipped.