Skip to content

Record Teams calls locally and transcribe them with Whisper on the CPU - #15

Merged
SychPL merged 37 commits into
mainfrom
feat/local-transcription
Nov 10, 2026
Merged

SychPL merged 37 commits into
mainfrom
feat/local-transcription

Conversation

@SychPL

@SychPL SychPL commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Status: the live test with a second person in a Teams call (spec steps 0b and 7) has not been done yet. Everything here is tested with fakes and synthetic audio; no real call has gone through detection, capture and transcription end to end. Until step 0b confirms it, CONFIRMED in recording/detect.rs stays false, so the recording's end is not trimmed: it runs until Teams lets go of its audio sessions, about 3 minutes after hang-up. Please do not merge before the live test.

Change

open_mess can now record the owner's Teams calls on their own Windows computer and turn them into text there, with Whisper (large-v3-turbo) in pure Rust (candle) on the CPU, inside the daemon. The owner decided this on 2026-10-28 (automatic recording of every call, a reminder toast, audio kept until transcribed and at most 7 days, text kept until deleted) and on 2026-11-02 (implement, with an own copy of the Whisper network).

  • Detection (recording/detect.rs): the audio sessions Windows lists for ms-teams.exe. A call starts on an active session and ends 15 s after Teams removes its sessions. ConsentStore and process loopback failed live (step 0, docs/protocols/teams.md section 11).
  • Capture (recording/capture.rs): endpoint loopback of the device Teams plays the call on (others, the whole call, so it also holds notifications and other apps), and the microphone only while Teams has it on (me). Both tracks are written by the clock, follow device changes, and a device that cannot be opened ends the recording without skipping the call.
  • Recorder (recording/recorder.rs): start, stop, skip.json across restarts, limits (6 h, disk, audio and text totals), toasts under their own AUMID with Stop and Stop-and-delete buttons, tray tooltip and menu item, status.recording.
  • Transcription (recording/stt.rs, recording/whisper.rs, recording/queue.rs): own copy of candle-transformers' Whisper with a decoder KV cache and a cancel check between encoder layers; only the speech is packed into the 30 s windows; one recording at a time, never while a call is recorded, behind a 6.6 GB memory gate. Measured on the owner's i9-13900K: about 42 minutes per hour of two-sided call (target 30, gate 60), peak 4.54 GB.
  • Model (recording/model.rs): downloaded only from the configuration window, pinned revision, exact sizes and SHA-256, hosts huggingface.co and *.hf.co.
  • Tools, the same over MCP and the CLI: list_recordings, get_recording_transcript (cues under untrusted) and stop_recording, under the read permission of the reserved account local. Agents cannot start a recording, change a setting or delete anything. The tools' descriptions point agents to the meeting's chat (teams_list_meetings, then read_messages), per the owner's decision of 2026-11-03; the recording tools' names are reserved against module tools.
  • Window: a "Call recording" section (settings, model, recordings, transcript), in Polish and English like the rest of the window.
  • Updates: an update waits for a call's end; a call that starts during the handover stops it (UpdateLink).

Spec

.specs/2026-10-28-local-transcription.md. Rounds R1-R15 before the implementation (R10: GLM APPROVE, Codex's P2 rebutted with the owner's agreement; R15: Codex APPROVE, GLM's P2s fixed). The owner then decided on 2026-11-02 to implement without further rounds. The meeting-chat change of 2026-11-03 went through R16-R19: GLM APPROVE in every round; Codex's P2s were fixed or added to the rebuttal register (no upper time bound in read_messages: a stage-1 tool, out of scope). Rounds stopped after R19, where neither reviewer had anything left on the chat change. Merging main added only the two-language texts (spec 2026-10-30-ui-languages) and moved the Windows audio facts to section 11 of docs/protocols/teams.md, with no further round.

Tests

The new behaviour is covered by tests that fail without it:

  • Recording folder and WAV: crash recovery, retention and limits.
  • Transcription: queue order, memory gate, damaged model, cancel, packing, KV cache equal to the full decoder, and token decoding against a slice of the real tokenizer.json.
  • Detection and capture with fake sessions and sources: mute, device change, dead device, clock gaps, a ten-minute silent source.
  • Recorder: skip across restarts, limits, a capture panic, the update handover.
  • Tools: permission re-check after reading, paging, reserved names.
  • Window commands, the model download against a local server, and the i18n checks from main.
  • tests/recording_live.rs: an ignored live check for steps 0b and 7.

cargo xtask ci on Windows at ecdaac8 (after merging main with release 0.1.0 and the Paperclip server): green, 771 passed, 0 failed. Earlier runs on a heavily loaded machine twice timed out one test from main, open-mess-host too_many_meetings_ask_for_a_narrower_window ("the service did not answer in time"). It passes alone in 0.8 s and passed in the last full run.

Commit reviews

Every commit was reviewed by Codex and GLM and reconciled in .reviews/<sha>.processed. P1 and P2 findings by area:

  • Recorder: a skip cleared on detection loss and a loop on capture failure (P1); a six-hour recording not skipping the rest of the call; skip.json writes. All fixed in fa142e9.
  • Devices: a dead device skipped the rest of the call (P1, fixed in a6be15b). The lost-device block was keyed on the wrong device (fixed in 3ad3c9a).
  • Updates: a race between the update handover and a recording start, which could end the daemon in the middle of a new recording (P1, fixed in a6be15b). The reason an update stopped was lost (fixed in a6be15b).
  • Queue: a damaged-model loop and a tight loop (P1 + P2, fixed in d11aef5).
  • Recovery and retention: stuck or lost tracks after a crash (fixed in 167d79f, 704af6a and 1ad1127).
  • Decoding: a 224-token cut (fixed in 911d28d). A speech scan without cancel (fixed in ef2add1).
  • Model download: leftovers of an interrupted download (fixed in da84822).
  • Live check test: the shared toast app id and leftover folders (fixed in 3ad3c9a and 024dc70).
  • README wording (fixed in 5bbb98c).
  • Merge: the tray could lose its recording state when the language changed (fixed in c0e9629). That fix then held a lock while Tauri changed the menu on the main thread, which could deadlock against a relabel (P1, fixed in 245b030: redraws are posted to the main thread and no lock is held).

Rejected with reasons:

  • GLM's claim about sysinfo features: system gates Process.
  • Codex's concern about leftovers of an earlier version of the live check: it never ran, and %TEMP% holds none.

Coexistence (platform modules only)

Not a platform module, and nothing talks to Teams' services. Detection only reads the audio session list. Capture is shared-mode and passive: no volume, mute or ducking changes, and no access to Teams' files or settings (checked live in step 0, where the call went on unchanged). The full check beside the official client during a real call is the pending live test.

Checklist

  • No secrets, tokens or real message contents in code, fixtures, logs or this PR (the live check prints only session states and levels)
  • No unwrap / expect / panic! / unchecked indexing in non-test code
  • New tools available over both MCP and the CLI (list_recordings, get_recording_transcript, stop_recording)
  • New dependencies named here, with their licence and why:
    • candle-core, candle-nn and candle-transformers 0.11.0 (MIT OR Apache-2.0): Whisper in pure Rust on the CPU, so no C++ toolchain or GPU SDK is needed.
    • rayon 1.12 (MIT OR Apache-2.0): the transcription's own thread pool. candle already pulls it in.
    • miniz_oxide 0.8 (MIT OR Zlib OR Apache-2.0): the compression-ratio check against looping text. flate2 already pulls it in.
    • sysinfo 0.37 (MIT): free memory and disk, and Teams' process name.
    • wasapi 0.26 (MIT): Windows audio sessions and capture through safe functions, Windows only.
    • deny.toml ignores paste, which candle's gemm uses (unmaintained only). It also ignores three zip extract advisories: candle reads zip only for .npz and .pth weights, never extracts, and open_mess loads safetensors.

SychPL added 30 commits October 28, 2026 21:03
The owner decided on 2026-10-28 (decision D1 of the stage 2 meetings spec)
that open_mess should transcribe calls that have no Teams transcript. This
spec settles how: the daemon notices that Teams holds the microphone (the
Windows consent store), records the Teams process's loopback audio and the
microphone in shared mode, and after the call turns them into text with
Whisper large-v3-turbo on candle, inside the daemon, on the CPU. Audio lives
in %LOCALAPPDATA% only until it is transcribed; the text stays until the
owner deletes it. Agents read transcripts through three core tools under a
reserved `local` read permission; only the window can enable recording,
change settings or delete.

Reviewed by Codex and GLM for 10 rounds; GLM approved, and the last Codex
P2 (no visible indicator with a hidden tray icon, muted notifications and
no microphone track) is a rebuttal the owner accepted.
The review of f42c13f found that a release dropping a model revision from the allowed list would delete the only model on disk at start and stop recording until the owner downloads again. The list now only grows, and the daemon deletes a model folder only after a newer revision downloaded or on the owner's click.
Step 1 of the local transcription spec (.specs/2026-10-28-local-transcription.md,
4.4): each recording is a folder under %LOCALAPPDATA%\open_mess\recordings with
meta.json, the tracks' WAV files until transcription, and transcript.json. The
data folder may roam to a server, so nothing of a recording goes there.

Crash safety comes from the order of writes and a recovery pass at start: a
recording the daemon left gets valid WAV headers and waits marked interrupted,
a transcription that wrote its transcript is done, leftover audio of a done
recording goes. Audio not transcribed within 7 days is deleted, and the size
and disk limits refuse a new recording. Ids are checked before they touch a
path, and the WAV reader takes only the header open_mess writes, with every
size checked against the file. Nothing calls this yet; detection, capture and
transcription come in the next steps.
Review of 82a012c (Codex and GLM P2): a track shorter than its header (the daemon stopped as the track was created) made the recovery fail and left the recording in state recording forever, outside retention and inside the audio limit. Such a track holds no audio and is now deleted; a recording with no track left is failed, and the writer puts the header on disk at once. The end of a left recording is also read from the tracks' last write before the header fix, so the 7-day retention no longer restarts at the daemon's restart.
Review of 167d79f (Codex and GLM P2): recovery deleted a track whenever fixing its header failed, so a recording another program held open at start could lose hours of audio. Only a file shorter than the WAV header (no samples) is deleted now; any other error leaves the recording as it is for the next start, with a regression test.
Review of 704af6a (Codex P2): a recording whose track another program held at start stayed in state recording until the next daemon start, outside retention. recover now takes the recording and transcription running now and leaves them alone, so the daily pass with expire can run it too and pick the recording up once the file is free.
Step 2 of the local transcription spec (.specs/2026-10-28-local-transcription.md,
4.3 and 4.7), the owner's choice of engine: Whisper large-v3-turbo in pure Rust,
in the daemon, on the CPU.

model.rs pins revision 41f01f3f of openai/whisper-large-v3-turbo (MIT) and
reads each file whole against its SHA-256 and size; the weights go to candle
from that buffer, since candle maps files only as unsafe. stt.rs cuts each
track into 30 s windows, skips silent ones (Whisper invents text over
silence), detects the language per track unless one is set, and decodes with
Whisper's timestamp rules and temperature fallback, including the
compression-ratio check against looping text. The model's text is untrusted:
cues and transcripts are capped, and a cancel flag is checked before the
encoder and after each decoder step. isolated() runs a transcription on its
own thread with half the cores, so a panic in candle ends as an error, not
in the daemon.

Tests run every step on a tiny zero-weight model and decode real tokens of
the model's tokenizer, so CI downloads nothing. cargo deny: candle's gemm
uses the unmaintained paste, and candle holds zip at 8, whose advisories are
about extract, which neither candle nor open_mess calls; both are ignored
with reasons. Nothing calls this yet; the queue and the daemon come next.
Review of 826ea0c (Codex P2): the decoder stopped at half of Whisper's 448 decoder positions, so the end of fast speech in a 30 s window was lost without a mark. It now decodes until the positions run out, and a window cut at the limit marks its last piece, as the spec's caps require. The pieces of a window come from one function now, with a test.
Step 3 of the local transcription spec (.specs/2026-10-28-local-transcription.md,
4.5): list_recordings and get_recording_transcript, the same over MCP and the
CLI. They answer under the read permission of the reserved account `local`,
which no real account (`<module>:<account>`) can be; the permission is read
before the files and again before the answer, so taking it back mid-call lets
nothing out. The transcript comes in pages by size, its text under `untrusted`
with the notice; ids are checked before any path, and a recording not done
says so.

The window shows a "Nagrania lokalne" row with one read box, even with no
account; its write permission is stored as never whatever is sent, and it has
no conversations. The daemon reads recordings from %LOCALAPPDATA% where
updates install (Windows); elsewhere the tools say they are not supported.
stop_recording and the status object come with the capture, which they control.
Step 4 of the local transcription spec (.specs/2026-10-28-local-transcription.md,
4.6 and 4.7). The new Nagrywanie rozmów section shows the model and offers
"Pobierz model" and "Usuń model"; only the window starts a download, nothing
else of the recording reaches the network.

A revision downloads file by file into its folder under %LOCALAPPDATA%, each
streamed through the update downloader's checks: HTTPS, Hugging Face and its
CDN hosts only (a `*.hf.co` entry, since the weights redirect to regional
hosts), every redirect checked, the exact size and SHA-256 compiled in, three
hours for the 1.6 GB weights. A failed download leaves no folder; a whole one
replaces the other listed revisions. Files are now pinned by exact size, also
when they are read for a transcription. The update downloader's per-file loop
became the shared download_file it uses too.

The recording settings and the list of recordings join the section with the
recording itself.
Review of 2771cef (Codex P2): a daemon stopped during the model download left an incomplete revision folder and its .part file, up to 1.6 GB, for good. At start, a listed revision's folder without all its files or with a .part file is deleted; unlisted folders are left alone. GLM P3: tests for the *.hf.co host rule and for the window's 'newer model' flag.
The live check of step 0 showed that Windows no longer writes the
ConsentStore times on the owner's computer and that process loopback of
Teams gives silence during a call, while the loopback of the headset
carries it. The spec now detects a call by the audio sessions of
ms-teams.exe, records the other side from the device Teams plays the
call on for the whole call, and records the microphone only while it is
on in Teams.

The CPU measurement of step 0e is in too: the spec's path needs about
72 minutes for an hour of one track. On the owner's decision the
transcription gets its own copy of the Whisper network, with a decoder
that keeps its keys and values and a cancel check between encoder
layers, and packs only speech into the model's windows. Rounds R11-R15
of the spec review are in its changelog; the owner chose to implement
without further rounds.

docs/protocols/teams.md gets section 10 with the redacted step 0
results: audio sessions, loopback, model download hosts and the CPU
numbers.
list_recordings returned at most the 500 newest recordings, and since
only filters newer ones, so an older recording could not be reached.
It now takes before, the last recording of the previous page, as
read_messages does.

The language of a transcript was written only to meta.json, after
transcript.json, so a crash between the two left a done recording with
no language. transcript.json now carries it too, and the repair at start
copies it into meta.json.
…yers

candle's Whisper runs the decoder's whole token sequence again at every
step and has no way to stop inside the encoder. On the owner's computer
that made a 30 s window take about 33 s, and a call that started during
a transcription waited for a whole encoder pass, about 9.5 s, while the
spec allows 5.

recording/whisper.rs is a copy of candle-transformers 0.11's Whisper
network (MIT OR Apache-2.0) with two changes: the decoder keeps the keys
and values of the tokens it has seen, and the encoder checks the cancel
flag between its layers. Weight names and shapes are unchanged, so the
same model file loads. Measured on the pinned model: a 30 s window in
22.8 s instead of 32.8 s with the same text, and a cancel during the
encoder returns after 0.11 s. A test checks that the kept keys give the
same output as running the whole sequence.
A track was cut into 30 s windows and a window was skipped only when it
was silent as a whole. In a call each track is silent while the other
side speaks, but the turns are shorter than a window, so in a ten-minute
two-track dialogue all 40 windows went through the encoder although
less than half of the audio was speech.

The speech is now found in 30 ms frames, kept with 300 ms around it and
pauses under a second joined, and packed into windows of at most 30 s
with 200 ms of silence between pieces; the same dialogue needs 16
windows. Times in a window are mapped back to the track through the
piece they fall in, and a time from the model that lies in a pause or
past the pieces moves to the nearest piece, so a cue stays on speech.
The model's times are untrusted, so the mapping saturates instead of
overflowing on a huge one.
Recordings in the waiting state had nothing to transcribe them. A queue
thread in the daemon now takes them one at a time, the oldest first,
and only while nothing records: a recording that starts cancels the
running transcription, which goes back to waiting and starts over after
the call. Reading the model checks the cancel flag every 64 MiB, so a
call waits a fraction of a second even then. The model is loaded for
each recording and dropped after it, so the daemon's memory comes back
between recordings. A transcription starts only with the model on disk
and 6.6 GB of memory free (the measured peak and 2 GB), because a
failed allocation ends the process.

A panic or an error fails only its recording; a model that fails its
checksum leaves the recording waiting and holds the queue until the
model is downloaded again. Deleting a recording stops its transcription
first, so no file of the folder is open when it goes. The same thread
runs the retention pass every hour. list_recordings shows the progress
of the running transcription.

The measured result of the whole new path goes into
docs/protocols/teams.md: a ten-minute two-track call in 418 s, about 42
minutes per hour.
Finding the speech reads the whole track before the first window, and
packing reads through the silence between pieces; neither looked at the
cancel flag. With a six-hour track that kept the transcription busy for
seconds after a call started, where the spec allows a fraction of one.
Both now check it every 30 s of audio. Codex's review of 49e06a3.
Step 0 showed that the ConsentStore times are not written on the owner's
computer, while the audio sessions of ms-teams.exe follow a call
closely: active during it, inactive while muted, removed about 180 s
after the hang-up. recording/detect.rs reads those sessions on every
active device (wasapi, safe functions; process names from sysinfo) and
turns the reads into a call: it starts on an active session, lasts while
a session is listed, and ends 15 s after none is. A read that fails is
skipped; reads failing for a minute make detection unavailable and end
the call. The 30-minute cap on a quiet call waits for step 0b, as the
spec says.

The logic takes the reads and the time, so it is tested without
Windows; an ignored test reads the live sessions of the computer it
runs on.
The ignored test that reads this computer's audio sessions printed its
counts, which the workspace lints refuse; 88a1690 went in without a
green cargo xtask ci because of it. It now asserts what it read instead.
recording/capture.rs records the device Teams plays the call on, by
loopback, and the microphone Teams uses, each into its own 16 kHz mono
track (spec 4.2). A track is written by the clock rather than by the
packets: what did not arrive is silence and what came past the clock
is cut, so both tracks keep the recording's length and their times
agree, also when a device is quiet for minutes. The microphone track
gets silence while Teams has the microphone muted, and what the
microphone heard then is dropped in memory.

The capture follows the devices the detection reports when Teams moves
to another one, opens a device again after an error, and leaves a track
silent after three failed tries 20 s apart. Devices are reached through
a trait, so the logic is tested with fakes; the WASAPI side opens shared
streams only. An ignored test reads this computer's loopback briefly.
… a failed read

Reviews of 3c5f2d8 and 88a1690:

- Any InvalidData from a transcription was taken for a damaged model, so
  a recording with a bad WAV header left itself waiting and held the
  whole queue (GLM P1). Only the model's own size or checksum check now
  reports a damaged model (SttError::Damaged); a bad track fails its
  recording, as the spec says, and the queue goes on.
- Nothing cleared the damaged state after the model was downloaded
  again (Codex P2). The gate now holds only while the weights are the
  ones that failed, so a new download lets transcriptions start.
- A transcription that came to nothing (stopped, a meta.json that cannot
  be written) was started again at once, in a tight loop (GLM P2). The
  queue now waits for a change, as it does when empty, and shows the
  reason meanwhile.
- When the process list could not be read, the Teams sessions came back
  as none, which ended a call that went on (Codex P2). A lookup that
  reads none of the processes asked for is now a failed read, which the
  detection skips and reports only after a minute. The capture uses the
  same lookup.
recording/recorder.rs takes the detection's read every second and runs
a recording's life (spec 4.1, 4.2). A recording starts while a call is
on, a Teams session is active, the automatic mode is on, the call was
not stopped, the tray is up, the model is there and the disk and the
limits allow it; otherwise the owner is told once per call why it is
not recorded. It ends with the call, a stop, six hours, less than 2 GB
free, the detection lost or the capture failing, each with its reason,
and the recording then waits for the transcription queue.

A stopped call is not recorded again until it ends, also across a
restart of the daemon (skip.json, kept while Teams lists sessions at
start). Turning the automatic mode off ends a recording without that.

The owner sees it in toasts under their own AppUserModelID,
open_mess.recording, whose buttons Zatrzymaj and Zatrzymaj i usuń act on
the recording they name only, and in the tray: "open_mess - nagrywanie"
and a Zatrzymaj nagrywanie item while a call is recorded. Agents get
stop_recording, under the read permission of local, and status gets the
recording object, with the reason the queue waits (Codex on 3c5f2d8).
Outside, the recorder reaches the settings, the disk, the capture and
the notices through hooks, so its logic is tested without Windows.
The Nagrywanie rozmów section gets what spec 4.6 gives it beside the
model: the automatic mode (off by default, confirmed when turned on),
the microphone and the language, saved through changes_allowed; the
recording going on with its own Zatrzymaj; why calls are not recorded
or transcriptions wait; and the recordings with Pokaż, Usuń, Ponów and
Usuń wszystkie.

Pokaż is the owner's read of a transcript, whatever agents may read, so
a refusal for agents never locks the owner out; the text goes into the
page through textContent only. Deleting stops the recording going on or
its transcription first, and a folder another program holds stays with
an error while the rest go. Ponów puts a failed recording whose audio
is left back in the queue. Usuń model is refused while a call is
recorded or transcribed.
Reviews of b9b8687 and 922ccde:

- A call stopped by hand started recording again when the detection
  failed for a minute and came back (Codex P1): only a working read
  now tells that a call is over.
- A capture that failed or panicked ended the recording, and the next
  read started a new one in the same call, every few seconds with its
  toast (GLM P1). Six hours, a failed capture and a failed write now
  end a recording as a stop does: the rest of that call is not
  recorded (Codex P2, GLM P2).
- A skip.json that could not be written was forgotten; it is written
  again on the next read (Codex P2).
- A track lost after three failed tries stayed silent when Teams moved
  to a working device (Codex P2, GLM P2): another device starts afresh.
- Samples that came after a gap were written right after the silence,
  about 200 ms early; they now end at the clock (Codex P2).
- A device that opens but fails every read never counted as lost; only
  a read that works now resets the count (Codex P2).
Spec 4.8 (and Codex's review of 922ccde): an update stopped the daemon
whatever was being recorded, so the rest of the call was lost.

- Zainstaluj and the toast refuse to start while a call is recorded
  ("aktualizacja po końcu nagrania"); installing on its own waits for
  the call's end like it waits for other work.
- A call recorded during phase A or B stops the installation as a
  cancel does, everything resumed, and the window reads "przerwano:
  nagrywanie rozmowy". Phase A therefore never gives up on its 30
  minutes during a call. Installing on its own may start the same
  release again after the call: its once-only mark is cleared then.
- Once the daemon hands over to the helper, no recording starts; the
  rest of the call is recorded after the restart.

The recorder and the update share an UpdateLink: whether a call is
recorded, and whether the daemon is handing over.
What is recorded and when, where it stays and for how long, who reads
the text, and that telling the participants and following the
organisation's rules is up to the owner (spec 2026-10-28-local-transcription,
section 7).
An ignored test that runs the real detection, capture and toasts in a
temporary folder while the owner is in a Teams call, so the daemon the
owner runs is not touched. It prints only the Teams sessions' states
and each track's level per second, and deletes the recording at the
end: what step 0b needs to settle (the other side heard while muted, a
long mute, joining muted, a device change) without keeping any audio.
…topped

Codex's reviews of db70d3c, fa142e9 and 5d6b1fa:

- A recording could start between the update's last look and its
  handover to the helper, and the daemon then ended in the middle of it
  (P1). UpdateLink is now one atomic state: a recording and a handover
  each claim it, and only one of them can.
- Why phase A or B stopped was read again after resume(), so a call
  that ended meanwhile lost the reason, and a cancel during a call was
  taken for the call's and let installing on its own start the release
  again (P2 x2). The phases now say why when they stop.
- A capture with no track left open ended the recording and skipped the
  rest of the call, which spec 4.2 says it must not (P1). It no longer
  skips; no new recording starts while Teams stays on the device that
  did not open, and one does once Teams moves.
- Deleting the recording going on reported success when its folder
  could not go (P2); the error now reaches the window.
- Usuń model checked for a recording and then deleted, so one could
  start in between (P2). The check and the deletion now run while no
  recording can start; a transcription that finds the model gone waits.
- The window also refreshes while a recording waits (P3).
Codex on 990f2f2: a recording lasts until Teams lets go of its audio,
about three minutes after the hang-up, not the hang-up itself; and an
agent allowed to read the transcripts passes them to its model
provider, so "never sent anywhere" was wrong.
A recording whose tracks could not be opened remembered only the speaker,
and took it from the detection read rather than from the capture. If Teams
moved to other speakers just before that read, the new, working device was
blocked for the rest of the call; and moving only to another microphone never
let the recording start again.

The capture now publishes the devices its dead flags belong to, so a failure
counts only for the devices it happened on, and the block holds the pair of
speaker and recorded microphone.

The live check removes the folders a killed run left behind and says that it
must not run while the daemon records, since both share the toasts' app id.
The live check removed every folder in %TEMP% whose name started with its
prefix, so a folder of the same family kept on purpose (a backup, say) would
have gone with it. Its temporary folder now sits inside one folder with an
exact name, and only that folder is removed before a run.
The owner decided on 2026-11-03 that a meeting's context is its transcript
and its chat. A recording does not know its meeting, so the recording tools'
descriptions tell the agent how to join them by time: teams_list_meetings for
the meeting that overlaps the recording, then read_messages of its
conversation, or list_conversations and search where the meetings tool is not
offered. No new field and no second way to the chat: it still comes from
read_messages, under untrusted and the Teams account's permissions.

The spec records the decision, the rounds R16-R19 with their fixes (the
measured 42 minutes per hour as a known limitation, the retention promise in
line with a transcription that runs at the deadline, the reason set) and the
promised comment against panic = "abort" in the workspace Cargo.toml.
Brings in stages 2 and 3 (meetings, transcripts, live captions), the module
tools, the two-language window and the release pipeline.

The recording side follows them: its window section, toasts and tray menu
now have English beside Polish (spec 2026-10-30-ui-languages), while the
reasons in status and list_recordings stay English for agents and are shown
in the window's language; an update stopped by a call is recorded as an
English outcome like the others. The recording tools' names are reserved
against module tools as the shared ones are. The Windows audio facts move to
section 11 of docs/protocols/teams.md, since section 10 now holds live
captions. The lockfile keeps candle 0.11.0, tokenizers 0.22.2 and zip 8.6.0,
the versions the transcription was measured with and deny.toml covers.
Relabelling the tray read whether a call was recorded and then set the menu,
so a recording that started in between was shown, then overwritten with the
state read before it: no stop item and no recording tooltip for the rest of
the call. The tray's state now sits behind one lock that both the recorder's
indicator and the relabelling hold while they change the tray.

The last window messages of the recordings without an English version
(unknown recording, transcript not ready or damaged, no model to download)
now follow the window's language too.
The tray's lock was held while the menu changed, and Tauri changes a menu on
the main thread: the recorder's indicator, already under the recorder's lock,
held the tray's lock and waited for the main thread, while the main thread,
relabelling after a change of Windows's language, waited for the tray's lock.
The window, the tray and the detection would have stopped for good.

The indicator now only stores whether a call is recorded and posts a redraw
to the main thread; relabelling posts one too. Each redraw reads the language
and the state when it runs, so nobody waits and the last redraw shows the
latest state.
The test helpers from both sides are kept: the sign-in page forwarding from
main, the recordings folder from this branch. The new admin tests build the
window state with the recording fields too.
RUSTSEC-2026-0429 is another `ZipArchive::extract` advisory, like the three
already ignored: candle reads zip only for .npz and .pth weights, never
extracts, and open_mess loads safetensors. The fix is in zip 9, which candle
0.11.0 does not allow; the transcription was measured on 0.11.0.
@SychPL
SychPL merged commit a4ced06 into main Nov 10, 2026
3 checks passed
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.

1 participant