Repository navigation
Timeline notes: time-anchored notes in place of the transcript's free-text user notes #488
Description
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 Contextsection 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.pycarriesuser_notesover by hand. - Two devices overwrite each other silently. A
PUTreplaces 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_msalready uses. Live utterances run on the decoded-segment sum, whichrecording_public.pynotes 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 Notessection of generated notes.
Planned PR series:
- B1: the table, API, migration (each non-blank
user_notesbecomes 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-notesare removed, andtranscript.user_notesleaves the recording payload.- MCP
get_meeting_notesreturnstimeline_notes(kind, time, body, quote, author) in place ofuser_notes. append_meeting_notesadds 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:
- Do you want a compatibility period for
/user-notes? For example,GET/PUTcould 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? - Is the MCP payload change acceptable as a hard cutover, or should
get_meeting_notesalso return the olduser_notesstring for a release? - 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?
- 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
feat(notes)!: timeline notes replace the transcript's free-text user …
fix(notes): save live notes once the owner signs back in
fix(notes): build the note buttons on the Button primitives
feat(notes): add notes from the live transcript, and show them inline (…
thebeline commentedon Oct 22, 2026
B3 plan, before we cut anything. Four stacked PRs:
TranscriptView.tsxand add one line renderer that the live and finished views share. No behaviour change.Three questions for you:
noteOutbox.ts. Shall we take it? We'd do it before B3b.— nullcat (Claudette)
Valtora commentedon Oct 22, 2026
Answers to the B3 plan:
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.
refactor(transcript): split TranscriptView and share one line renderer (
thebeline commentedon Oct 22, 2026
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)
feat(notes): a ⋯ menu on each transcript line, with "Note on this lin…
feat(notes): a note on text selected in the transcript (#616)
feat(notes): fold a line's saved notes into a count (#649)
feat(notes): highlight a selection note's words, found again after a …
Valtora commentedon Oct 23, 2026
Two things from the #659 review for a later pass, yours if you want them:
thebeline commentedon Oct 23, 2026
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)
fix(notes): find a one-word quote inside a word only from three lette…
thebeline commentedon Oct 24, 2026
Starting B5 as one PR. It no longer waits on B4 (drag to re-anchor), which follows on its own.
append_meeting_notestakes an optionalat_secondsand adds a point note at that moment. Without it, it adds a tail note, as it does now.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_staleinworker/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)