Repository navigation
Keep every Teams call the owner is in from its live captions - #16
Merged
Merged
Conversation
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.
The owner wants every call he is in - meetings, Meet now, group chat calls and 1:1 calls someone joined - kept on his own machine, so he can go back later and know what was said. The archiver in the core joins each call through the live captions tools, turns captions on when no one has, and appends every cue with its speaker to the local recordings store, with gaps marking when it was not listening. Agents read it with the recording tools under the conversation's read permission. Reviewed by Codex and GLM over ten rounds; GLM approved the last five, every finding is fixed, and the owner approved it after round ten.
Specs and docs are Markdown that neither fmt, clippy nor the tests read, so running the full gate for them only costs time. The owner asked to exclude them.
Static analysis of the current Teams build (26276) answers what the spec left open: an item without groupContext in the call store is a 1:1 call, adding a person keeps the same call and attaches a group thread, and the client turns captions on by adding the bot with botData or sending it a start command without a language. The refusal codes for end-to-end encrypted meetings, the lobby and policy are written down, and roster endpoints carry media stream types, which tell a data-only leg from a muted official client. The spec follows these facts; what static analysis cannot settle moves to the live check of step 0b.
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.
Codex and GLM found that a live read after a third person joins a 1:1 call checked only the new group chat, though the session's buffer still holds what was said before, so every thread the session has had now needs the read right. The 1:1 rule follows the client's own test (no groupContext), and a thread the conversation reports after joining is added, so a meeting without groupContext never opens more. The bot is added through addParticipant when the call has no thread, as the client does, the consent text names the combined recording bot, the live check covers 1:1 calls, and the protocol notes say that the call store item of 2026-10-29 has the 1:1 shape. The code context now describes main.
A read-only run over a 1:1 call, the same call after a person was added, and a scheduled meeting settled what the static analysis of step 0a left open: the 1:1 item shape, a call id that survives the escalation, the userJoinTime format (and that it turns null after an escalation), the bot the official client adds to a 1:1 call, the commandUrl host, and that reads without a join are not throttled. The spec now says what series 1 answered and what waits for series 2 and 3, treats allowTranscription null as no refusal, and tells the captions bot apart by its MRI, since a meeting can hold another 28: participant.
The owner gave consent for the remaining live series of step 0b once, and wants steps 2-4 built without waiting for them: series 2 now runs by itself in the background on the next 1:1 call, and what it does not answer is left to the acceptance test.
…s-archive-core # Conflicts: # docs/protocols/teams.md
The captions archive (spec 2026-11-04-captions-archive, 4.4-4.6) keeps each Teams call it hears as a recording of source captions: its cues in captions.jsonl, word for word, and the times nothing was heard in gaps.jsonl, beside the archiver's state of the calls in captions-calls.json with its previous version. A torn last line is cut before the next append, and a damaged line is skipped and counted, never a panic. The recording tools read these recordings in every state, with the speaker's name, the gaps and the damaged lines, and only under the read permission of the call's conversation and of each of its threads on an account that is still there; unreadable recordings are left out before the list's limit. The audio recordings' start and daily passes leave captions recordings to the archiver, and their text counts towards the shared 1 GB limit.
Step 2 of the captions archive spec (.specs/2026-11-04-captions-archive.md, 4.2-4.3): the core's archiver will call the module's tools for every call the owner is in, so the module has to name those calls, keep a session in each, and turn captions on when nobody has. - teams_list_calls lists the call store's calls: a meeting or group call by its chat thread, a 1:1 call by the 1:1 chat with the other person, anything else as call:<id>. The call is the correlationId; items with another id shape or a conversation URL off the service host are left out and counted in the log. joined_at stays null until step 0b settles what userJoinTime means. - teams_join_captions takes a required call (and language, start_captions) and joins only that call. Before joining it reads the call's conversation: allowTranscription false is "policy", and without another device of the user with a media stream other than data it is "user_not_in_meeting", so nothing reaches the call. The same device rule applies to the roster during the session, so a data-only leg of another open_mess instance never keeps a session in a call. - Up to 3 sessions an account, 4 in the module, each a device of its own (an endpoint id per slot, so two Trouter sockets never share an epid). A join for a running call returns its state instead of joining twice; a session of another call in the same conversation ends first; an ended session stays readable with its own token for 15 minutes. - Captions are turned on the way the official client does it (docs 10.8): 10 s after the data channel opens without the captions bot on, the combined bot is added through addParticipantAndModality or, for a 1:1 call, addParticipant, with botData and a token for the recording resource (a new WAM scope), and gets start once it is in the call; a bot already there gets start on its commandUrl. At most 4 tries, 5 minutes apart, and never again once captions were on in the session or in that call within the day (a list in module storage). The old no_captions endings go: the archive records the gap instead. - Reads give call, captions_seen, skipped, threads (the call's chat threads, for the core's permission check when someone is added to a 1:1 call), own on each cue and at to the millisecond. - Join refusals and leg ends are told apart by their codes: e2ee, lobby (also a lobby acceptance) and policy. - describe-target for call:<id> describes the call from its conversation (subject, start time, roster) for the approval. Tests: the pure parts in captions.rs, and the module in the host against the fake server with the shapes of "Live results" 18-20. The core's window test counts the Teams manifest's scopes, now six.
…ssion A join for a call that already has a session returned its state at once and added the conversation given to the session's threads, without checking that the conversation is one of that call's names. An approved join under an unrelated chat's id could so hang that chat's name on another call's session, and an agent allowed to read only that chat would read the call's captions with it. The call store is now read first in every join, and the session is reused only under the call's own chat, its 1:1 chat or call:<id>; any other name is "that call is no longer running". Codex review of 7f866d9 (P1).
The owner wants a lasting record of every Teams call he is in (spec 2026-11-04-captions-archive). With the window's new switch on, the core's archiver lists each account's calls every 20 s through the module's list_calls, joins each one under the module write guard of approved actions, reads its captions every 5 s into a captions recording and leaves it when the owner says "Zatrzymaj". The switch is the standing consent: off by default, its turning off ends every call kept after a join running now, and its turning on never brings back what was said before it. Every gap is written down: the late start, cues lost from the buffer, captions turned off, and the time between a session's end and the next join. Stopped calls, given-up calls and the captions seen stay in captions-calls.json, so a restart never rejoins a stopped call, and a lost state joins no call listed at the start. Permanent refusals (E2EE, lobby, policy) leave a failed recording that says why; passing ones are tried every 2 minutes, at most 15 times (refused 3). An update, a scan of the modules and the daemon's stop read every session before the modules end them. Logs carry states and counts only. stop_recording now takes a recording and returns what it stopped, stopping only the captions of calls the agent may read. An agent reading or leaving a captions session must be allowed to read every thread the session named, before the module is called and after, so an escalated 1:1 call's earlier chat stays closed to an agent allowed only the new one. The window shows the switch, the language, the calls kept on top, the source and coverage of each recording and its captions with gaps; the tray says how many calls are kept and stops them all; a toast with "Zatrzymaj" tells of each start. test-echo stands in for the Teams module's captions tools in the tests. Also fixes Codex's findings on 4a885da: the list checks every recording's permission again once the page is made, a captions recording with a damaged meta.json stays hidden from agents, and a state file with an entry that is not a call's counts as unreadable, as it may hide a stop.
Codex's review of f3e3b79 found three ways the archive could lose what it promised. A stop whose captions-calls.json write failed left the old "active" state on the disk, so a restart could join the stopped call again: the archiver now blocks every call whose recording ended "stopped" in the last day at every start, not only when the state is lost, and deleting such a recording is refused until the block is on the disk. Cues that could not be appended to captions.jsonl went missing without a trace: they are a lost gap now. And an agent's first leave of an escalated call reached the module before the core knew the call's earlier chat: the archive's own reads now tell the core every thread a session names, so that call is refused before the module runs. From GLM's review of the same commit: a call given up is written down before its failed recording, and the core's memory of sessions' threads drops the one told of longest ago when full, never a session read now. Adds a test of a scan of the modules during a join: the version that goes reads the last cue first and ends the session the join made.
A session whose reads failed in a row kept its recording open for as long as the reads kept failing, even after its call was gone, which the captions archive spec rules out (4.4: a recording never stays in "recording" without a session). After a minute of failed reads the session counts as gone: the call is joined again with a rejoin gap, or its recording ends when the call is no longer listed. Codex on f2f52cb: deleting a recording waited for the calls' state to be written even when that recording keeps no call stopped, so one state that could not be written held back the deletion of every finished recording. Only a stopped call's recording, the trace of its block, waits for that now.
Codex and GLM on d8f68ae: after a minute of failed reads the archiver joins the call again, and the module answers that join with the session that was there all along. Its first read then starts from the oldest cue the buffer keeps, and the cues already written went into the recording a second time. The archiver now keeps the given-up session's token and cursor, and when the join brings back that very session it leaves out what it had read of it.
Codex and GLM on 0633178: the session given up after failed reads was forgotten when the next one was given up before its first read, and only the first page of a returning session was checked against what had been read of it, so a buffer over one page, or a second minute of failed reads, still wrote cues twice. The archiver keeps the last session it read until a page of it reaches its cursor, over as many pages as that takes, and a page it emptied that way no longer counts as the end of the buffer. test-echo's captions pages now hold 100 cues, as a module's pages do.
The archive joins every call the owner is in once the owner turns it on in the window, so the official client shows the owner in the call on another device then too (spec 2026-11-04-captions-archive, section 5).
rmcp before 3.9.0 reads non-streaming HTTP response bodies of its Streamable HTTP clients with no size limit, and cargo deny now fails the dependency check on it. 3.9.0 bounds them.
The test makes the calls state read-only to fail its write. On Windows that stops the rename of the new state over it; on Linux a rename replaces a read-only file, the write succeeds and the test failed in the Ubuntu gate. open_mess runs on Windows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
The owner wants every Teams call they are in written down privately on this computer, so they can come back to what was said later through an agent (word against word; a meeting whose data and tasks were not noted). This builds that from live captions:
modules/teams/src/live.rs,captions.rs,manifest.rs):teams_list_callstool: lists the user's running calls from the call store, as a chat thread, the 1:1 chat orcall:<id>.teams_join_captionsjoins any of these calls, with a requiredcall. Before joining it reads the roster: the user must be connected on another device, andallowTranscription: falsegivespolicy.addParticipantAndModality/addParticipantwithbotData, or sendsstarton the bot'scommandUrl. It never does so again in a call where captions were already on.e2ee,lobbyandpolicyrefusals from the docs 10.8 codes, and hasdescribe-targetforcall:<id>.teams_read_captionsaddscall,captions_seen,skipped,threads, cueownandatin milliseconds.crates/open-mess/src/recording/captions.rs,archive.rs): turned on by a new switch in the window, off by default.captionsrecording (captions.jsonl,gaps.jsonl, coverage).tools/extra.rs.list_recordingsandget_recording_transcriptread captions recordings under the conversation's read right (every thread).stop_recordingtakesrecordingand returnsstopped.Not verified live yet (spec 0b series 2 and 3, step 5):
The owner decided on 2026-11-09 to ship before those series. Step 5 is the live acceptance test.
Spec
.specs/2026-11-04-captions-archive.md, rounds R1-R10 with Codex and GLM; the owner approved it after R10. Step 0a (static REA, docs 10.8) and series 1 of step 0b (live reads only, "Live results" 20) are recorded. The owner's decision of 2026-11-09 to build now is in section 2.Tests
captions.rs, and host tests incrates/open-mess-host/tests/teams/main.rsagainst the fake server with the shapes from Live results 18-20, covering:call;setLanguageorstop;crates/open-mess/tests/captions_archive.rs, with test-echo standing in for the Teams module and a hand-set clock, plus unit tests inrecording/and window tests intests/ui.rs. They cover:cargo xtask cion Windows at f6119ba (the module merged into the core branch): green. The tree is unchanged since then, apart from AGENTS.md (6681fc3) and the merge of main, whose tree is byte-identical.Commit reviews
Every commit reviewed by Codex and GLM and reconciled in
.reviews/<sha>.processed.threads; the core checks every thread (spec 4.2), with a test of that case.Coexistence (platform modules only)
open_mess joins as a separate device of the owner (own endpoint id), data channel only, muted, never reads or writes the official client's data, never calls read receipts. The official client shows the owner on another device while a session runs (AGENTS.md exception). Turning captions on adds the Teams captions bot, as the client does. Not yet checked side by side live with the new join paths (step 5).
Checklist
unwrap/expect/panic!/ unchecked indexing in non-test code