Skip to content

Meetings

Meetings turns a call into a live, searchable transcript and, the moment it ends, a summary and coaching notes, all as ordinary pages your other pages already link to and embed. There’s no separate meetings product to learn: a meeting is a page in meetings/, its transcript is a node tree, and its summary is just more nodes at the top.

Calling the start flow (or clicking Record on an attendee-bearing event on the Calendar agenda) creates the meeting page: a title like “Meeting, Rachel Kim, Jun 30”, an Attendees: [[...]] bullet linking every real CRM page among the event’s attendees, and a root Transcript node everything else appends under. It also stamps the source calendar event with a back-link to the page and appends a ((Meeting, ...)) embed bullet to each attendee’s CRM page, so the meeting shows up right on their profile.

Starting is idempotent: recording the same calendar event twice adopts the existing page (flipping its status back to recording) rather than creating a duplicate, and starting a meeting that’s already recording is a no-op that hands back the same page.

Every meeting streams two audio channels, kept separate on purpose: channel 0 is you (microphone), channel 1 is them (system audio, everyone else on the call). Per-channel separation is the diarization, there’s no speaker-identification model in the loop.

  • Speech-to-text: real transcription rides Deepgram’s live streaming API in multichannel mode. Under the deterministic fake used in tests, each newline-delimited line sent on a channel becomes one final transcript segment for that channel, stamped with a monotonic two-second clock.
  • On macOS, system audio (channel 1) comes from a small Swift helper built on ScreenCaptureKit’s audio-only capture. Electron has no way to tap system output itself, so the desktop shell spawns this helper per meeting and pipes its stdout, raw 16 kHz mono 16-bit PCM, over the shell’s WebSocket connection to the server. Your microphone (channel 0) streams from a hidden browser window the desktop shell opens for the duration of the call; it exists purely to hold a getUserMedia capture and forward it over its own WebSocket. If the audio transport ever drops, the desktop shell tears down both the helper and the hidden window rather than leaving system-audio capture running silently.
  • Transport: both channels arrive at the server over ws /meetings/audio?meeting=<pageId>&channel=0|1, one connection per channel, binary frames carrying audio.

Every finalized segment commits as one node under the Transcript parent, formatted [hh:mm:ss] me|them: <text>, and echoes live into any open window on the page’s own topic, so a second tab (or a teammate with access) watches the transcript build in real time. The outliner renders each transcript node with a me/them speaker chip, and the page itself shows a status banner with a Stop recording button for as long as the meeting is live.

Stopping the recording closes the audio stream (this works even if the live session is gone, a restarted server can still end a meeting from what’s already in Postgres), flips the page’s status to done, and, in the same transaction, fires a meeting-ended event carrying the meeting id, page, title, source calendar event, attendee names, and duration.

That event does two things:

  1. Any agent definition listening for meeting-ended runs against the finished page. Triage stamps the event silently, it’s not something that shows up as a notification on its own.
  2. The summarizer agent runs automatically, writing a Summary sub-tree at the top of the page. Under the deterministic fake, the summary is built extractively: consecutive same-speaker segments group into turns, and the first sentence of each turn (capped at five) becomes a bullet prefixed me: or them:. A real model gets the full transcript and a prose instruction to produce the same shape. Either way, re-ending a meeting replaces the existing Summary node rather than stacking a second one.

The seeded agents/meeting-coach definition also listens for meeting-ended and files proposal-only coaching notes on the finished meeting page, advice for you, nothing is ever sent anywhere. Each note cites a specific moment from the transcript (the last transcript line at the time the event fired) and the playbook principle it maps to, grounded by reading a linked playbook page, the seeded playbooks/Sales Playbook by default. Point the agent definition at a different playbook (hiring, support, whatever you coach against) and the same loop follows it, no code change required.

A meeting page is a normal outline page, so it participates in the rest of Subspace like any other: it’s linked from its source calendar event, embedded under every attendee’s CRM page, findable through search, and taggable and linkable like anything else you write.