Skip to content

fix(notes): summarise a plain-text transcript with checklist action items (#2666) - #2815

Merged
Chadpiha merged 5 commits into
mainfrom
fix/plain-transcript-action-items
Oct 21, 2026
Merged

Chadpiha merged 5 commits into
mainfrom
fix/plain-transcript-action-items

Conversation

@greatcoat

@greatcoat greatcoat commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is part 2 of #2666. A note whose transcript was only plain text, with no speaker segments, was summarised as a personal note:

  • AI Summary got the generic note-cleanup wrapper. That wrapper has no action-item checklist rule and says "Do NOT list or guess participant names/roles".
  • Section-based templates (Detailed Notes and the rest) were told "There is no transcript". That also tells them to skip their meeting sections.

On #2666 this produced an action-item table with no owners. The main trigger was a summary run straight after a recording stopped, which read the recording store's leftover plain text; #2672 fixed that. This PR covers what remains: a transcript that is saved as plain text.

Behaviour

For a plain-text transcript:

  • Format. Action items are checklist lines in the normal format, - [ ] Action — Owner. A stated deadline goes inside the action: - [ ] Fix the build by Friday — Alice.
  • Only what was said. An owner or deadline appears only when the transcript or the user's notes state it.
    • Nothing is guessed.
    • There are no placeholders ("TBD", "Owner not specified").
    • There is no table, unless a custom template asks for one.
    • With nothing stated, the line is just - [ ] Send the revised proposal.
  • Mentions. The user and the note's invitees are its known people, so a stated owner who was invited becomes a mention.
  • No ## Meeting Context block. Without speaker labels the model can't tell who "I" is, and an invitee list invites guessed owners.
  • Sections that attribute by person (Stand-up's Updates by Person, User interview's quotes, 1:1) attribute a point only to someone the transcript or the notes name, and group the rest by topic. [name unclear] is never written as a speaker or an owner.
  • On a local model split into parts, each part is told its lines carry no speaker labels, so a part can't guess an owner that the merge would then take as stated.

What doesn't change:

  • Meetings with speaker segments and personal notes without a transcript compile byte for byte as on 2b4706f28. The check covered 142 compiles across every built-in and custom template shape and both summary and chat actions.
  • The run's input and content hash are unchanged in every case.

Where plain-text transcripts come from

  • A recording that captured no speaker segments. The stop path saves main's joined text instead (meetingTranscriptPersistence.ts).
  • Meeting notes recorded on v1.6.0–v1.6.5. Transcripts have been stored as segments since v1.6.6.
  • An upload whose provider returned no segments. The transcript is saved as the note's content and the transcript column stays empty (uploadNotes.ts), so the run treats that content as the plain transcript.

Granola imports and mobile sync store segments or nothing.

Changes

  • builtinActions.js adds two prompts that share one owner rule:
    • PLAIN_TRANSCRIPT_SYSTEM_PROMPT, for flat templates such as AI Summary.
    • PLAIN_TRANSCRIPT_INPUT_PREAMBLE, for section-based and standalone templates and for a first summary from a summary action.
    • The preamble also says never to guess who said something. It replaces the Action Items section's "You"/"Them" fallback with ending the line after the action. Sections that attribute by label or by person keep only named people and group the rest by topic, and [name unclear] never stands in for a speaker or an owner.
  • templatePrompts.js: compileTemplatePrompt and compileSummaryActionPrompt take a hasPlainTranscript option. Speaker segments win if both flags are set.
  • actionProcessingStore.ts forwards the flag, including on the merge pass of a long note split into parts on a local model. It appends the custom dictionary for plain transcripts, as it does for meetings. The part prompt (partNotesPrompt) has a variant for a transcript without speaker labels; the meeting part prompt is byte-identical.
  • PersonalNotesView.tsx (runNoteAction) sets the flag when a transcript has no segments and collects the user and the invitees as known people. An upload with an empty transcript column runs its content as the plain transcript, under ## Meeting Transcript rather than as the user's notes.
  • CLAUDE.md §19 documents the three cases.

Judgment calls (easy to veto)

  • The custom dictionary applies to plain transcripts. Speech-to-text misspells names here as it does in meetings. Like a meeting, a shorten or lengthen rewrite on such a note now gets the dictionary too.
  • An owner written in the user's own notes counts as stated.
  • AI Summary's wrapper is meeting-shaped for this case.
  • An upload's content counts as its transcript when the transcript column is empty, even if the user has since rewritten it into their own notes.
  • A custom template's own instructions still win. A user's template that explicitly asks for something, such as an action-item table or "Owner not specified", gets it. The new rules override only the built-in wrappers and the built-in Action Items section's "You"/"Them" fallback.
  • Known limitations:
    • A task the user's own notes assign to themselves ("I'll send the deck") gets no owner, because the model isn't given the user's name in this case.
    • Stand-up's "Updates by Person" section can only list people the transcript names; everything else is grouped by topic.

Tests

Each new test fails on 2b4706f28 for the reported reason and passes here.

  • test/helpers/templatePrompts.test.js
    • The plain AI Summary prompt: the checklist rule, owner and deadline only when stated, no placeholders, no personal-note rule.
    • The plain preamble for standalone templates and first summaries, with its rules.
    • Speaker segments win on both paths.
    • The Detailed Notes pin over the new preamble.
  • test/config/noteFidelityPrompt.test.js: the fidelity rule appears exactly once in the new compiles.
  • test/stores/actionProcessingChunked.test.js
    • The runner sends the plain prompt and the dictionary.
    • It saves a stated invitee owner as a mention and an owner-less line as written.
    • It covers a first summary from a summary action and the merge pass of a long note.
    • Meetings and personal notes are unchanged.
  • test/components/personalNotesUndoDiscard.test.js
    • runNoteAction flags a plain transcript, with the user and the invitee as known people and no Meeting Context.
    • A meeting keeps its Meeting Context, speaker lines and known people.
    • A note without a transcript is unchanged.
    • An upload whose transcript is its content runs as a plain transcript, with that content as the transcript rather than the user's notes.
  • Part prompts (actionProcessingChunked.test.js): a plain transcript's parts get the no-label prompt; a meeting's parts keep the label prompt.

Mutation checks. Each of these fails its new test:

  • removing the flag from the summary-action path;
  • emptying a meeting's known people;
  • dropping the invitees from the Meeting Context;
  • letting the plain preamble beat the meeting one;
  • giving a plain transcript's parts the meeting part prompt, or a meeting's parts the plain one;
  • removing the per-person and [name unclear] sentences from the preamble;
  • treating an upload's content as the user's notes.

Validation

  • npm test (Node 24.20): 8,320 tests, 17 failures.
    • All 17 are in metaActivation, noteCreateReconciliation, assistantNoteUndo and noteEditPersistence, where the local better-sqlite3 build targets Electron's Node ABI.
    • The same 17 fail on 2b4706f28 with the same dependencies.
  • npm run lint, npm run typecheck, npm run format:check: all pass. Lint's warnings are pre-existing, in other files.
  • Independent review:
    • Codex review (--scope branch --base 2b4706f28) found no actionable regressions.
    • A second reviewer found no defects. Its test gaps and prompt suggestions are fixed in f9e9793.
    • Both reviewers re-reviewed the repairs and found no new defects. The second reviewer's 19 mutations are all caught by the tests.
  • 2b0fd03 (review follow-up: part prompt, per-person sections, uploads): the 18 related test files pass, typecheck, lint and format:check are clean, and the builtinActions DB suite passes 41/41 under Electron's Node.

Not verified

  • Model behaviour. No real-model replay was run. Whether gpt-oss-120b now always writes the checklist, and never a table, is model behaviour that unit tests can't prove. Suggested check: replay the compiled plain-transcript prompt about 10 times through the production note chain, using the scripts/llm-canary.mjs probe pattern.
  • The real app. Not run. To try it in a dev build:
    1. Give a note a plain transcript and an invitee from the control panel's DevTools:
      window.electronAPI.updateNote(id, {
        transcript: "Okay. Alice, can you fix the build by Friday? Yes. Someone should send the revised proposal.",
        participants: JSON.stringify([{ email: "alice@example.com", displayName: "Alice Ng" }]),
      });
    2. Run Generate AI Summary.

Closes #2666 (part 1 landed in #2672).

🤖 Generated with Claude Code

greatcoat and others added 4 commits October 20, 2026 13:26
A transcript with no speaker segments was compiled as a personal note: no
checklist rule, "Do NOT list or guess participant names", and for sectioned
templates "There is no transcript". It now gets its own wrapper and preamble.
Action items are `- [ ] Action — Owner` lines with an owner or deadline only
when stated, never a placeholder, and never a table unless the instructions
ask for one. Meeting and personal-note compiles are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…note runner (#2666)

RunActionOptions gains hasPlainTranscript, which both compilers now receive,
including the merge pass of a long transcript run in parts on a local model.
A plain-text transcript is speech-to-text like a meeting's, so it also gets the
custom dictionary. isMeetingNote stays false for it, so parts still split plain
text as prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h its invitees for mentions (#2666)

runNoteAction now flags a transcript that has no speaker segments as
hasPlainTranscript instead of running it as a personal note. Its known people
are the user and the note's invitees, so an owner the transcript names becomes
a mention. It gets no Meeting Context: without speakers the model can't tell
who "I" is, and an invitee list invites guessed owners. Input and content hash
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The plain-transcript preamble also forbids guessing who said something, so
  templates that attribute by speaker (Stand-up, 1:1, User interview) don't
  guess a label. It now overrides only the Action Items section's "You" or
  "Them" fallback, not a custom template's own rules.
- Tests: a summary action on a plain note with no summary yet; a meeting
  with a signed-in user and an invitee (Meeting Context, speaker lines, known
  people); speaker segments winning on the preamble path. Fixtures use
  neutral names.
- Comments: the parts route's plain paragraph and the plain branch's known
  people. Inline the flag object in compileTemplatePrompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greatcoat
greatcoat requested a review from Chadpiha October 20, 2026 13:55
- A local model's part prompt no longer tells a plain transcript's parts that
  every line has a speaker label, so a part can't guess an owner the merge
  would then take as stated. The meeting part prompt is unchanged.
- The plain preamble now covers sections that attribute by label or by person
  (Stand-up's Updates by Person, User interview's quotes): only named people,
  the rest by topic, and never "[name unclear]" as a speaker or owner.
- An upload whose provider returned no segments saved its transcript as the
  note's text; it now runs as a plain transcript instead of as personal notes.
  The content hash is unchanged.

@Chadpiha Chadpiha left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I reviewed this end to end and found no blockers. There's one route for every template or summary-action run (runNoteAction → runBackgroundAction), so a plain-text transcript reaches the new prompts from the callout, TemplatePicker, the action chips and picker, the / menu and shorten/lengthen. That covers personal, team and space notes. Cloud mode applies it too, since api/reason uses the client's systemPrompt as-is.

Unchanged:

  • Meetings and personal notes compile as before.
  • The NOTHING_TO_SUMMARIZE rule still reaches plain transcripts, through the template prompt, the sectioned footer and the first-summary prompt.
  • The content hash formula is the same, so no existing summary turns stale.
  • Chat actions don't depend on the flag.

Follow-up pushed in 2b0fd03:

  • Local-model parts: partNotesPrompt told every part of a plain transcript that each line has a speaker label. A part could guess an owner, and the merge would take it as stated. Plain transcripts now get a no-label variant; the meeting part prompt is byte-identical.
  • Per-person sections: the plain preamble now covers sections that attribute by label or by person (Stand-up's Updates by Person, User interview's quotes, 1:1): only named people, the rest grouped by topic, and never [name unclear] as a speaker or owner.
  • Uploads with no segments: the transcript is saved as the note's content and the transcript column stays empty. These notes used to get the personal-note prompt; they now run as a plain transcript. The content hash is unchanged.

Verified:

  • The new tests fail on the base for the stated reasons and pass on the branch.
  • Each follow-up fix was checked by reverting it: its test fails.
  • The 18 related test files pass, and the builtinActions DB suite passes 41/41 under Electron's Node.
  • typecheck, lint and format:check are clean, and the branch merges cleanly with main.

Not covered:

  • Real-model behaviour. Worth a short replay of the plain prompt through scripts/llm-canary.mjs; the note probe only compiles the meeting prompt today.
  • Low-priority follow-ups:
    • A whitespace-only or "[]" transcript is classified as plain, because the check isn't trimmed.
    • Recording again into a note with a plain transcript overwrites that transcript. This predates the PR: seedSegments comes back empty for plain text.

@Chadpiha
Chadpiha marked this pull request as ready for review October 20, 2026 15:25
@Chadpiha
Chadpiha merged commit 8924df3 into main Oct 21, 2026
19 checks passed
boseq added a commit that referenced this pull request Oct 21, 2026
Resolves CLAUDE.md section 19 (keeps main's plain-transcript line, then this PR's default-template line) and templatePrompts.test.js (the AI Summary invariant now also runs with the plain-transcript preamble from #2815). main's flat plain-transcript test now uses a customized flat AI Summary, since the built-in AI Summary is sectioned on this branch; the sectioned plain-transcript path is pinned by the invariant test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meeting AI summary: action items come out as a table with no owners instead of a - [ ] Action — Owner checklist

2 participants