Skip to content

Timeline notes: time-anchored notes in place of the transcript's free-text user notes #488

Open

Description

@thebeline

What problem would this solve?
A recording's own notes are one free-text column, transcripts.user_notes, edited through GET/PUT /transcripts/{id}/user-notes. That causes three problems:

  • Notes have no time. A note typed at minute 40 reaches the model in the # User Notes Context section as context for the whole meeting, so it cannot tell which part of the meeting the note is about.
  • A reprocess has to copy the notes across. The transcript row is deleted and rebuilt, so recordings/helpers.py carries user_notes over by hand.
  • Two devices overwrite each other silently. A PUT replaces the whole text and carries no revision, so the last save wins and nobody is told.

The solution you'd like
Timeline notes. Each note is a row of its own (timeline_notes), owned by the recording rather than the transcript, so it survives a reprocess by construction. Each row has a revision, and an edit or delete names the revision it was made against. A stale write gets a 409 that carries the current note. Each write may also carry a client-chosen token that the note keeps, and a delete leaves a tombstone with its token, so a client whose response was lost is told what became of its write, including whether the note was deleted by its own write or another device's.

  • Four kinds, with the shape enforced by a check constraint and the API schemas:
    • point: a moment.
    • selection: quoted words over a time range.
    • head: before the whole transcript, with no time.
    • tail: after the whole transcript, with no time.
  • The clock is the recording's audio timeline: milliseconds into the captured audio with paused time left out, the clock that utterance start_ms already uses. Live utterances run on the decoded-segment sum, which recording_public.py notes runs 2-3% high against the final master. B1 writes no timed notes from the UI, so B2 measures that drift before it adds live anchors, and remaps them at finalize if needed.
  • One placement rule puts a note after the transcript line it falls in. Every reader uses the same rule: notes generation, automatic meeting intelligence, Meeting Edge, chat, speaker inference and the ## User Notes section of generated notes.

Planned PR series:

  • B1: the table, API, migration (each non-blank user_notes becomes one head note), placement, prompts, MCP and backup format 3. The existing notes box edits the head note, with conflict handling. No new UI surface.
  • B2: a live entry box that writes point notes during a recording, shown inline in the live transcript.
  • B3: notes on selected transcript text, with a highlight that follows the text across a reprocess.
  • B4: drag a point note to a new place, with a keyboard path as well.
  • B5: a "My notes" tab that lists, jumps to, edits and deletes notes, plus attachments on a note, notes in exports, and an MCP tool for timed notes.

This is a breaking change for API and MCP clients:

  • GET/PUT /transcripts/{id}/user-notes are removed, and transcript.user_notes leaves the recording payload.
  • MCP get_meeting_notes returns timeline_notes (kind, time, body, quote, author) in place of user_notes.
  • append_meeting_notes adds one tail note per call and returns that note. Today it appends to the text.

Alternatives you've considered

  • A JSON list of notes beside the column: keeps one row per recording, so the lost-update problem stays, and there are two sources of truth.
  • Anchoring notes to utterance ids: the final pass rebuilds every utterance, so the ids would dangle.
  • Wall-clock times: a pause would shift every later note.
  • Server-sent events for the second device: there is no broadcast channel, and the detail poll already carries the notes.

Additional context
Questions for you:

  1. Do you want a compatibility period for /user-notes? For example, GET/PUT could keep working for one release, mapped onto the head note, before they are removed. Once the frontend moves to the new endpoints, nothing in the repository calls them. Do you know of anything outside it that does, such as scripts or a companion app?
  2. Is the MCP payload change acceptable as a hard cutover, or should get_meeting_notes also return the old user_notes string for a release?
  3. B1 is one PR because dropping the column moves every reader at once. It is about 3,400 lines outside tests and docs. The only clean split is table and API first, with the column read-only, then the prompts and the column drop, which leaves two sources of truth in between. Would you rather review it split?
  4. Is bumping the backup format from 2 to 3 fine? Older archives convert on restore, the same way the migration converts existing notes.

A draft PR for B1 will follow and will link here.

— nullcat (Claudette)

Activity

  1. thebeline commented on Oct 22, 2026

    @thebeline
    ContributorAuthor

    B3 plan, before we cut anything. Four stacked PRs:

    • B3a: split TranscriptView.tsx and add one line renderer that the live and finished views share. No behaviour change.
    • B3b: a per-line ⋯ menu you can use from the keyboard, whose first action is "Note on this line".
    • B3c: notes on free text selection, across lines.
    • B3d: highlights that follow their text across a reprocess. Each one is found at read time from the note's stored time span and quote, and is never written back, so it needs no schema or pipeline change. If the text can't be found, the note shows as unanchored rather than landing somewhere silently.

    Three questions for you:

    1. Four PRs, or B3a on its own and then one PR for the rest?
    2. Clicking a line's text opens the editor, so a drag-select inside a line ends in that click and loses the selection. We'd keep click-to-edit but skip it when text is selected, and add "Edit text" to the ⋯ menu. Does that work, or would you rather editing moved to double-click or the menu only?
    3. Unsaved live notes are lost when the tab reloads or crashes #537 is our outbox from feat(notes): add notes from the live transcript, and show them inline #517, and B3b extends the same noteOutbox.ts. Shall we take it? We'd do it before B3b.

    — nullcat (Claudette)

  2. Valtora commented on Oct 22, 2026

    @Valtora
    Owner

    Answers to the B3 plan:

    1. Four PRs is fine, as long as each one leaves main releasable on its own.
    2. Agreed, keep click to edit and skip it when the click ends a selection. Edit text in the menu also gives the editor its first keyboard path.
    3. Yes, please take Unsaved live notes are lost when the tab reloads or crashes #537 before B3b.

    B3a is reviewed on #542. I filed #550 to memoise the transcript line, best done before B3b adds per-line props, and #551 for the toolbar's focus ring and button names.

  3. thebeline commented on Oct 22, 2026

    @thebeline
    ContributorAuthor

    Thanks, that settles all three. We'll take #550 and #551, then #537, then B3b, each leaving main releasable on its own. The memo for #550 was already in progress from the B3a review, so it comes first.

    — nullcat (Claudette)

  4. Valtora commented on Oct 23, 2026

    @Valtora
    Owner

    Two things from the #659 review for a later pass, yours if you want them:

    • A drag from the middle of one speaker's line into an overlapping line of another speaker marks unchanged words as changed, on partly wrong words. The quote is saved in time order (feat(notes): a note on text selected in the transcript #616) but the resolver orders lines by their start.
    • One-word quotes match inside longer words, so "a" lands on "and" as an exact match once the original "a" is gone. Preferring whole words for one-word quotes, or a minimum length, would likely cover most of the 1.7% wrong marks.
  5. thebeline commented on Oct 23, 2026

    @thebeline
    ContributorAuthor

    Ours, both, in one small PR: resolve lines in the same time order #616 quotes them in, and keep one-word part-word matches to fragments of a few letters or more, so "a" never lands inside "and".

    — nullcat (Claudette)

  6. thebeline commented on Oct 24, 2026

    @thebeline
    ContributorAuthor

    Starting B5 as one PR. It no longer waits on B4 (drag to re-anchor), which follows on its own.

    • My notes: a list of every note on a processed meeting, in timeline order: notes on the whole meeting first and last, the rest by time. A click scrolls the transcript to the note, and each note can be edited or deleted there, with the same revision checks as on its line. Today a finished meeting shows its head and tail notes nowhere. On desktop the side column's top panel becomes tabbed, Speakers | My notes, so the list stays next to the transcript while you jump through it, and Captures can be a third tab later. On a phone, My notes is a tab in the row after Notes. Nothing changes while recording, since the live view already shows every note.
    • Exports: the transcript section carries your notes where they fall, as the transcript view does. A tick in the export dialog leaves them out when you're sharing a transcript.
    • MCP: append_meeting_notes takes an optional at_seconds and adds a point note at that moment. Without it, it adds a tail note, as it does now.
    • No migration, and no change to the backup format.

    Attachments are out. #488 listed attachments on a note, but your Captures plan on #653 covers the case that matters, an image at a moment, so we'll leave the attachment model to you rather than add a second one. One thing for that design: every ready document feeds notes generation today (_flag_notes_stale in worker/tasks/documents.py), so captures stored as documents would reach the notes without the per-capture switch.

    If you'd rather have My notes as a fifth main tab than in the side column, say so before we get there.

    — nullcat (Claudette)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions