Skip to content

Join Teams meetings for their live captions - #13

Merged
SychPL merged 86 commits into
mainfrom
feat/teams-live-captions
Nov 5, 2026
Merged

SychPL merged 86 commits into
mainfrom
feat/teams-live-captions

Conversation

@SychPL

@SychPL SychPL commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Change

open_mess can now join a Teams meeting the user is already in and read its live captions, as a
second endpoint of the user with no audio or video (spec .specs/2026-10-29-live-captions.md,
steps 1-5):

  • host: the rtc import - one WebRTC data channel per module connection, only to the address
    ranges in the module's media manifest field, never through a proxy, never audio or video
    (crates/open-mess-host/src/rtc.rs, proxy.rs for WinHTTP proxy resolution);
  • host and core: end-sessions before a module goes (daemon stop, rescan, update) and restore
    after a crash, so a meeting is always left;
  • core: module write tools run as pending actions that always wait for the owner's approval in the
    configuration window, even when writing alone is allowed;
  • Teams module: Trouter v4c, teams_join_captions (write, approved), teams_read_captions and
    teams_leave_captions, the meeting chat through read_messages with since (spec 4.5),
    wam-token naming the account, read_messages that errors instead of returning a short list;
  • OPEN_MESS_LOG prints the daemon's and modules' own logs on stderr for diagnostics (off by
    default; dependencies' logs are not printed).

It is stacked on #12 (which is stacked on #11 and #10): the branch grew from early copies of those
branches, and the merge commit puts it on their reviewed tips. The diff against #12 is this work
only; the commit list also shows the early copies, which squash away.

Spec

.specs/2026-10-29-live-captions.md, reviewed by Codex and GLM: R19 both APPROVE without findings
(spec approved); the meeting chat addition R20-R25, both APPROVE in R25. Steps 0a-0c and 5 were
live checks with the owner's OK in chat for each run, recorded redacted in
docs/protocols/teams.md ("Live results" 13-19).

Tests

  • crates/open-mess-host/tests/rtc.rs: loopback against a second WebRTC host - no RTP from the host,
    an answer with audio or video refused, candidates only from media.peers, limits.
  • crates/open-mess-host/tests/teams/: join, read and leave against a fake Teams and a fake Trouter
    (compressed callbacks too), the user not in the meeting, one session an account, end-sessions,
    a failed leave sent again from the record, /call/end/ as refused and conversationEnd as
    meeting_ended.
  • modules/teams/src/captions.rs, sdp.rs, trouter.rs: the pure session (deadlines, buffer,
    data channel decoder, roster), MS-SDP both ways, Trouter frames, gzip and zlib callbacks with a
    size cap.
  • crates/open-mess/tests/pending.rs, shared_tools.rs, ui.rs: module write tools through
    approval, voiding, Gate, the module swap guard, the media range as a change to approve.
  • crates/open-mess/src/daemon.rs: OPEN_MESS_LOG passes only open_mess's targets.

cargo xtask ci on the merge commit: fmt, clippy -D warnings and tests green, 42 test binaries
test result: ok.

Commit reviews

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

  • b5bc0df (both P2: data-only and inactive offers not tested with the fixed data section): fixed in
    0ab6a10 and checked live in step 0c.

  • b70e9bb (Codex P2: a peer-reflexive address bypassed the per-candidate proxy check): fixed in
    1451f19; 1451f19 (Codex P2: the pin checked the address, not the port): fixed in dc9908e.

  • f071865 (Codex P1: an approved module built for the older contract did not load, so the update
    found no predecessor): fixed in 65d5796.

  • 7e9e7e2 (Codex P2: a slow describe spanning the block start stored the action): fixed in a8d2993.

  • d49e4ac (Codex P2: wam-token's new argument at the same contract version): rejected - the branch
    already changes the 0.1.0 contract (required end-sessions and on-rtc exports), every module is
    built here and released with the daemon, and the predecessor is found by its approval.

  • 96070fd (Codex P1: roster reads stopped during a Trouter gap; P2: the 30 s captions check
    skipped): fixed in 263a526.

  • 7385fdd (Codex P2 x3, GLM P2 x2: join without a record, unregistration before leave, sign-out
    skipping records, a session without tokens dropping its retries, no test of the leave from the
    record): fixed in 6e2f1f2.

  • 263a526 (GLM P1, Codex P2: Trouter stayed down after a first socket fell before
    trouter.connected; Codex P2: a roster read in a gap did not stop the captions-off clock): fixed
    in 54f5d0d.

  • 0cff6c1 (Codex P1: rmcp's debug logs carried tool results under OPEN_MESS_LOG): fixed in
    a2bae39.

  • b21b25d, the merge (Codex P2: proxy settings that could not be read were taken as no PAC
    script, so a media connection could go direct): fixed in 13dcacd, which also allows the
    Windows-only auto-configuration answer as dead code on Linux.

13dcacd: Codex and GLM APPROVE. Not reviewed by GLM: a2bae39 and b21b25d (GLM's weekly limit ran
out on 2026-11-05; skipped for now on the owner's decision). Codex reviewed both: a2bae39 APPROVE,
b21b25d fixed as above.

Coexistence (platform modules only)

Yes, live, step 5 ("Live results" 19): a scheduled meeting of the owner's team, the owner in it in
the official client with captions on. Nothing was marked read (no read-receipt calls, the meeting
chat's read state unchanged), the official client was not logged out, showed nothing about another
device and did not list the owner twice; the other participants noticed nothing. The one accepted
change to the official client is the owner's decision O1 (AGENTS.md, Non-negotiables).

Checklist

  • No secrets, tokens or real message contents in code, fixtures, logs or this PR
  • No unwrap / expect / panic! / unchecked indexing in non-test code
  • New tool available over both MCP and the CLI
  • New dependencies named here, with their licence and why:
    • webrtc 0.21.1 (MIT/Apache-2.0), host: the data channel; 0.21 verifies Teams' ECDSA P-384
      certificate with SHA-256.
    • async-trait 0.1.92 (MIT OR Apache-2.0), host: implementing webrtc's async traits.
    • flate2 1.1.10 (MIT OR Apache-2.0, pure Rust backend), Teams module: Trouter callbacks come
      gzip-compressed.
    • tracing-subscriber 0.3.23 (MIT), moved from dev-dependencies: OPEN_MESS_LOG.

SychPL added 30 commits October 28, 2026 15:10
D1: local transcription is wanted, in its own spec. D2: no extra gate,
open_mess is for the owner's own use, so transcripts reach agents under
the account's read permission like chat messages. D3: a transcript fetch
must never count as a view in the recording owner's analytics, so step 0
gets a gate that checks the counters on the user's own recording with a
control, and the download path ships only if it passes.

Reviewed by Codex and GLM in round 11 (both approve).
Stage 2 needs the Teams module's own tools to reach agents, and a
SharePoint token whose audience is the user's own tenant, which no
manifest built once for everyone can spell out.

The registry now lists the read tools of active modules as
<module>_<tool>, with an optional account the core adds and resolves.
The core checks the arguments against the declared schema, the read
permission before the call and again after it for the same account,
drops items of conversations the agent may not read, and takes only a
JSON object of at most 1 MiB back. Write tools, bad names, names taken
twice or by a core tool, schemas the core cannot check and schemas
declaring account are not listed; status says why. Every MCP session
hears tools/list_changed when the list changes, from a scan or from the
host stopping a module after crashes.

A WAM scope may now be https://*.<domain>/.default, checked at load like
a host pattern; it covers one lowercase DNS label on the module's host
list and nothing else. Literal scopes compare as before. The scope check
now runs before the tests' fake WAM too, and the approval window says
what the SharePoint pattern hands over.
Agents can now see the Teams meetings of the user's calendar with the
meeting chat each one has, which read_messages already reads. The module
asks the middle tier's calendarView, as the official client does, with
the authsvc token and the skype token it now keeps from the exchange;
a skype token about to expire, or a 401, gets a new exchange.

The live check of step 0 (docs, "Live results" 10) showed one answer for
a dense month, times always with an offset, the enum values the module
now maps, and no cancelled or private marker in the window. Only Teams
meetings the user has not declined are returned, ids as opaque tokens,
subject and organizer under untrusted; a continuation key or a field in
another shape is an incompatible answer, never a silent first page.

meetings.rs also holds the WebVTT parser and the paging of cues for the
transcript tool that follows.
Review of fc631e3 (Codex, GLM): a module file that declared, say,
`read` + `messages` made every read_messages call end as that module's
unknown or disabled tool, even while the module waited for approval. A
core tool's name now never routes to a module.

Codex also found that the declaration checked was read before the
account's module handle was taken: a replacement approved in between
could run a tool that writes in the new version under the read
permission. The call now goes ahead only if the module it reaches
declares the very same tool.
Review of 5802273 (GLM, two P3): list_meetings treated a `from` or `to`
that is not text as left out and answered for the default window, and a
NOTE block with `-->` in its text failed the whole transcript. A value
of another type is now invalid, and NOTE, STYLE and REGION blocks are
skipped before the timing line is looked for.
The owner asked whether open_mess could get live captions by joining a
meeting itself, as the user's second endpoint without audio or video.
The installed client shows that caption text comes from a server-side
bot over the call's data channel, a media modality: signaling and
Trouter carry only the bot's roster entry and commands, and the
signaling-only "subscribe" join gets no data channel. So it needs a
negotiated media session (SDP, ICE, DTLS, SCTP), and the user's own
client would show the meeting as running on another device.

Static analysis of the package only, marked as not verified live; no
code changes (the tree is the one d28cfff passed CI with).
Review of 57a056a (GLM, two P3): section 10 used markers outside the
file's legend and its open questions were missing from "Needs live
capture", which is what live checks are planned from; the date and the
status line were off by the new section. Docs only.
Review of c46fca7 (GLM, P3): two UNCONFIRMED facts of section 10.4 - an
organizer option for captions alone, and whether the service keeps
anything of captions without transcription - had no entry in "Needs
live capture". Docs only.
The owner wants agents to read a Teams meeting's live captions while
the user is in it. Captions reach a participant only on the call's data
channel (docs 10), so open_mess would join as the user's second endpoint
with no audio or video: a new host import for one data channel through
TURN relays on the module's host list to address ranges the manifest
names, module write tools through pending actions, Trouter in the Teams
module, and leave guaranteed on stop, update, replacement and crash.

Reviewed in eleven rounds (the eleventh with the owner's consent): GLM
approved R9-R11; Codex rejected R11 over one race, fixed here without a
further round at the owner's call. Steps 0a and 0b (static and live
checks) gate the code and send the spec back to review if the protocol
differs; owner decisions O1-O3 are open.
Review of add1643 (Codex, P2): after adopt put a replacement Teams
module in place, an agent could join a meeting through it before
end-sessions had ended the old version's session, leaving one account
with two sessions. A rescan now takes the same order as an update:
block module writes, let running ones finish, adopt, end the old
sessions, drop the old handles, unblock. Spec only.
Review of 0b2b0ff (Codex P2, GLM P3): a second rescan started while the
first waited on end-sessions could lift the first one's write block and
let a join through the new module version while the old session still
ran. Rescans now hold one lock for their whole run, and the spec adds a
test of the block during a rescan. Spec only.
The owner accepts that, while a live-captions session they approved is
in a meeting, their official Teams client shows them in the meeting on
another device. AGENTS.md names this as the one exception to "Never
disturb the official clients"; nothing else of the official client may
change. O2 and O3 stay open.
Review of 61091e3 (Codex, P2): with write=autonomous an agent could join
a meeting for captions without the owner approving that session, while
the O1 exception in AGENTS.md covers only sessions the owner approves.
Module write tools now always wait for approval in the window;
autonomous acts like confirm for them. Spec only.
open_mess reads live captions only when someone in the meeting already
has them on, and never adds the caption bot or sends it any command.
With nobody's captions on, a session ends with no_captions after 30 s;
captions turned off mid-session show as captions_on false and end the
session after 5 minutes. The bot's command URL and the tokens it would
need drop out of the design. O2 stays open until step 0b.
Review of 3836639 (GLM, three P3): the timer list in 5.3 lacked the new
waits for captions to be on, 4.4 did not say that open_mess never turns
captions on, and the manifest note still named the bot as a token
audience. Spec only.
Static analysis of the installed client for step 0a of the live
captions spec: the conversation-service join (default epconv URL, body
keys, SDP offer in callInvitation.mediaContent, callback links over
Trouter), the answer and roster callbacks with the caption bot's
processing modes, leave, a presence check in the user's call store that
needs no join, TURN credentials from trap/tokens and the relay hosts,
no-trickle ICE, and the 11-byte framing of the data channel. The steps
of the spec hold; the open points go to step 0b. Docs only.
With the owner in a meeting and their OK for reads only, the call
store answered with the active call but holds no meeting thread, so it
can tell that the user is in a call, not in which meeting; the relay
credentials endpoint answered with the shape static analysis found. No
join and no Trouter connection. Docs only.
The captions probe and the live captions module both depend on how
Trouter delivers the call callbacks and on what the join body and its
callbacks look like. AGENTS.md wants those facts written down before
code depends on them.

Static analysis of the shipped client shows that v4c (one WebSocket,
no allocate step) is the default and v4a is the fallback, gives the
Socket.IO 0.9 frames, the downstream request and response envelope,
gzip callbacks, the endpoint capability bits, that callModalities has
no data modality, the MS-SDP transform, the callAcceptance
acknowledgement and the join response keys. Section 5 and "Needs live
capture" 7 now point there, and "For the spec" lists what step 0b has
to settle. No network request was made for this.
The owner approved reading the transcript of one recurring meeting of
their own organization, although the recording is a colleague's. The
probe made the requests of spec 4.2 steps 1-6 once, printing counts,
key names and character classes only.

What it settled: MCPS paginationInfo is {hasNextPage} and the calendar
event's iCalUID picks the occurrence; TranscriptV2 metadata times are
.NET ticks, not ISO times; the ODSP transcript id equals
metadata.transcriptId; streamContent?is=1 is WebVTT with a byte order
mark and voice tags, and the module's own parse_vtt and page handle
the real file. The allTime view counters stayed at 0 right after the
download, which is not yet a D3 result: they are read again a day
later, and the check on the owner's own recording still stands.
Review of 800ab63 (GLM P1) pointed out that the download ran on a
colleague's recording before the D3 gate, while the doc called it step
0. It was the owner's explicit decision for that one meeting; the doc
now says so, says what the owner accepted and that the D3 gate on the
owner's own recording still has to pass before the tool ships.

Also stop item 9 from saying that a download in the owner's
organization is still to be captured (GLM P3).
The live download in the owner's organization (docs "Live results" 12)
showed that TranscriptV2 times are .NET ticks, not ISO times, and that
MCPS pages carry only hasNextPage with no continuation field. The spec
now converts ticks with a format check and treats hasNextPage true as
an incomplete list instead of paging blindly. It also records the
owner's one-meeting exception to the D3 order, which the commit review
of 800ab63 found missing from the spec; the D3 gate itself is unchanged.

Review round 12 found that a VTT file over 100 000 cues was cut
silently, so the last page would claim the transcript ended. The parser
now returns an error instead, with a test. Round 13: both reviewers
approve.
The owner wants an agent to summarize tomorrow's meeting from its
transcript, before the D3 view-count check is settled. A view of a file
lands in the analytics of whoever owns the place the file lies in, so
the tool reads only transcripts whose location is under the user's own
OneDrive site (from personalFileSite) and refuses anything else before
asking for any SharePoint token. metadata.ownerId is not used: what it
names is not confirmed (spec review R14).

The path is spec 4.2 as checked live: MCPS contents with the MCPS
token, TranscriptV2 times as .NET ticks, hasNextPage true never picked
from, the origin only one of the organization's two, the ODSP list read
before and after the WebVTT so a changing transcript is an error, and
pages pinned to one version. SharePoint refusals are answers about the
transcript, not a lost sign-in. The manifest asks for the MCPS scope and
the SharePoint pattern, so the module needs approval again.

Tests run the built module against a fake Teams with fixtures of the
live shapes and synthetic values. Spec rounds R14-R16; both reviewers
approve R16.
The owner decided on 2026-10-30 that teams_get_transcript should work for
every meeting he attends, not only for recordings in his own OneDrive, and
accepted that a download may count as one view in the recording owner's
analytics (spec section 10, D3). Tomorrow's meeting is likely organized by
someone else, so its transcript sits in the organizer's OneDrive.

The refusal for transcripts outside the user's personal site is gone, and
so is the path check behind it; the session now keeps only the tenant
label it needs to derive the organization's two SharePoint origins. The
other checks stay: only GETs, no read receipts, the owner tenant must be
the user's, and no SharePoint token is asked for any other host. The
manifest's SharePoint scope pattern already covers every OneDrive on the
tenant's -my host, so the scopes and hosts are unchanged.

The refusal test now proves that a colleague's OneDrive and a team site
are read with a token for their own origin; the tests for other
organizations and foreign hosts stay. Spec rounds R17 and R18.
c85deae dropped the lowercasing of the personalSiteUrl path when the path
stopped being compared with transcript locations, so a site path written
as /Personal/ would fail the shape check and break teams_get_transcript.
SharePoint paths ignore letter case; the check does again, with a test.
Found by GLM in the review of c85deae.
Trouter v4c authenticates with a user.authenticate event after 1::, not
on the upgrade request; that missing event was the 5 s timeout of the
first try. A running call is joined at its own conversation URL, not at
the epconv service URL. Joins there are still refused with 503
(subCode 70051) whatever the body, so the refusal and what was ruled out
are written down for the next step.
The static reading of the join was not enough to make the probe's join
work, so the owner joined his own meeting from the Teams web client in a
separate browser profile and only the shape of its call-signaling
requests was recorded. It shows the real sequence (regional epconv
without a call invitation, notificationLinks, then the join at the
conversation URL), the x-data form of the data channel in the offer and
the capabilities the service needs to find the running call. The probe
runs that followed, and what is still different, are written down too.
With HTTP/2 the probe's notificationLinks request to the conversation
succeeds and Trouter callbacks arrive, so the service knows the probe's
endpoint; the join itself is still refused with 503. The variants tried,
and the second capture showing that the web client always offers audio,
are written down so the next step starts from them.
The join was refused because the probe wrapped the body in a payload
object the client strips before sending. Unwrapped, the join succeeds;
with a send-only audio section and no track ICE connects, and with the
media processor's P-384 certificate verified correctly DTLS completes
and the leg stays up. The data section is still rejected, so no caption
arrived yet; what is left is written down.
… is not

The capture of the web client's answer shows the service accepting its
x-data section, while every no-audio variant of the probe still gets the
data section rejected. The variants and the client's rules for media
descriptions are written down for the decision on how to go on.
…esult

The client joins without the data channel and adds it later with a
media renegotiation. The probe now does the same: the renegotiation is
accepted and answered, but the service still rejects the data section,
so no caption has arrived yet.
SychPL added 28 commits November 3, 2026 15:09
Review of 57a056a (GLM, two P3): section 10 used markers outside the
file's legend and its open questions were missing from "Needs live
capture", which is what live checks are planned from; the date and the
status line were off by the new section. Docs only.
Review of c46fca7 (GLM, P3): two UNCONFIRMED facts of section 10.4 - an
organizer option for captions alone, and whether the service keeps
anything of captions without transcription - had no entry in "Needs
live capture". Docs only.
The owner wants agents to read a Teams meeting's live captions while
the user is in it. Captions reach a participant only on the call's data
channel (docs 10), so open_mess would join as the user's second endpoint
with no audio or video: a new host import for one data channel through
TURN relays on the module's host list to address ranges the manifest
names, module write tools through pending actions, Trouter in the Teams
module, and leave guaranteed on stop, update, replacement and crash.

Reviewed in eleven rounds (the eleventh with the owner's consent): GLM
approved R9-R11; Codex rejected R11 over one race, fixed here without a
further round at the owner's call. Steps 0a and 0b (static and live
checks) gate the code and send the spec back to review if the protocol
differs; owner decisions O1-O3 are open.
Review of add1643 (Codex, P2): after adopt put a replacement Teams
module in place, an agent could join a meeting through it before
end-sessions had ended the old version's session, leaving one account
with two sessions. A rescan now takes the same order as an update:
block module writes, let running ones finish, adopt, end the old
sessions, drop the old handles, unblock. Spec only.
Review of 0b2b0ff (Codex P2, GLM P3): a second rescan started while the
first waited on end-sessions could lift the first one's write block and
let a join through the new module version while the old session still
ran. Rescans now hold one lock for their whole run, and the spec adds a
test of the block during a rescan. Spec only.
The owner accepts that, while a live-captions session they approved is
in a meeting, their official Teams client shows them in the meeting on
another device. AGENTS.md names this as the one exception to "Never
disturb the official clients"; nothing else of the official client may
change. O2 and O3 stay open.
Review of 61091e3 (Codex, P2): with write=autonomous an agent could join
a meeting for captions without the owner approving that session, while
the O1 exception in AGENTS.md covers only sessions the owner approves.
Module write tools now always wait for approval in the window;
autonomous acts like confirm for them. Spec only.
open_mess reads live captions only when someone in the meeting already
has them on, and never adds the caption bot or sends it any command.
With nobody's captions on, a session ends with no_captions after 30 s;
captions turned off mid-session show as captions_on false and end the
session after 5 minutes. The bot's command URL and the tokens it would
need drop out of the design. O2 stays open until step 0b.
Review of 3836639 (GLM, three P3): the timer list in 5.3 lacked the new
waits for captions to be on, 4.4 did not say that open_mess never turns
captions on, and the manifest note still named the bot as a token
audience. Spec only.
Static analysis of the installed client for step 0a of the live
captions spec: the conversation-service join (default epconv URL, body
keys, SDP offer in callInvitation.mediaContent, callback links over
Trouter), the answer and roster callbacks with the caption bot's
processing modes, leave, a presence check in the user's call store that
needs no join, TURN credentials from trap/tokens and the relay hosts,
no-trickle ICE, and the 11-byte framing of the data channel. The steps
of the spec hold; the open points go to step 0b. Docs only.
With the owner in a meeting and their OK for reads only, the call
store answered with the active call but holds no meeting thread, so it
can tell that the user is in a call, not in which meeting; the relay
credentials endpoint answered with the shape static analysis found. No
join and no Trouter connection. Docs only.
The captions probe and the live captions module both depend on how
Trouter delivers the call callbacks and on what the join body and its
callbacks look like. AGENTS.md wants those facts written down before
code depends on them.

Static analysis of the shipped client shows that v4c (one WebSocket,
no allocate step) is the default and v4a is the fallback, gives the
Socket.IO 0.9 frames, the downstream request and response envelope,
gzip callbacks, the endpoint capability bits, that callModalities has
no data modality, the MS-SDP transform, the callAcceptance
acknowledgement and the join response keys. Section 5 and "Needs live
capture" 7 now point there, and "For the spec" lists what step 0b has
to settle. No network request was made for this.
A module may now declare its own write tool (step 2 of the live captions
spec): joining a meeting for its captions is a write the user approves,
never something an agent starts alone.

- extra.rs lists a write tool only when its schema requires
  `conversation`, and publishes `account` as required, as for `send`.
- tools/mod.rs routes a call to pending.rs instead of the module, under
  the write permission only, and the Gate treats it as a write by its
  declaration.
- pending.rs stores it as `module:<full name>`. It waits for approval
  under `autonomous` too. The approval binds to the conversation and its
  title, not its participants, and to the tool's declaration: another
  title voids it, another declaration fails it.
- A block counter in Accounts refuses module writes from before a module
  swap or an update's phase A until the old version has ended its
  sessions, after the module calls already running end. Scans run one at
  a time, so a second scan never lifts the first one's block.
- The approval prompt and the window show the tool's description, every
  argument, and who is in the conversation now, for information.
- The module actor skips a request whose caller stopped waiting, so a
  write reported as uncertain never goes out late. end-sessions and
  sign-out still run.
…ntext

The owner asked on 2026-11-03 that an agent reading a meeting gets its chat as well as its captions. The chat thread is already readable through read_messages and search; how the captions tool points to it is decided in a spec change before step 4.
A module write tool checked the block on module writes only before the module described the target. A description that ran past the start of a scan or an update's phase A still stored the action, although no module write is to be made once the block holds (spec 2026-10-29-live-captions, 4.3; Codex review of 7e9e7e2). A swap meanwhile was already refused by insert; the block is now checked again after the description.
A captions session gets its call callbacks over Trouter (spec
2026-10-29-live-captions, 5.3). This adds the protocol side as plain
code with native tests: the v4c URL and query, user.authenticate, the
Socket.IO frames (heartbeat, acks, pings), trouter.connected with its
surl, registrar and reconnect URL, answers to downstream requests with
only call callbacks handed on, the registration and its renewal, and
reconnection with doubling waits that ends the session after two
minutes. The values are those the live probe sent, now written down in
docs/protocols/teams.md.

The wiring to the host and a WebSocket in the fake Teams server come
with teams_join_captions in step 4: before it nothing would call them.
The owner decided on 2026-11-03 that a meeting's context for agents
includes its chat. Spec section 4.5 settles how: no second chat
reader. read_messages on the same conversation reads the chat under
the same read permission, and the tool descriptions tell agents so,
including the bounds of a recurring meeting's occurrence.

Review rounds R20-R25 also made read_messages return an error instead
of a silently short list, retried a failed leave on every path before
the session record goes, and had the module read the roster over HTTPS
while Trouter is down. Codex and GLM approved in R25.
Two preparations from the live-captions spec for step 4.

wam-token takes an optional account. A call may name only its own
account; an event may name any account of the module signed in with
Windows. Live-captions sessions handle Trouter frames and timers as
events, and with several Windows accounts an event that named nobody
got no token at all (spec 5.2). The Teams module now names the account
in every silent request.

read_messages no longer returns a list that the call's time or page
limit cut short; it fails instead. An agent reading a meeting chat with
since must be able to trust that a short list is complete before it
moves since forward (spec 4.5, Codex R22-R23). mentions and
writing_context keep their partial reads.
Step 4 of the live-captions spec starts with the parts that need no
host: captions.rs holds a session's state and deadlines (join, captions
wait and off, idle, three hours, the Trouter gap's roster polls), the
data channel's framing and caption messages, the cue buffer agents
page through, and the conversation service's requests and answers:
the call store, the join body, the roster, the callbacks, leave and the
session records. sdp.rs turns the host's data-only offer into the web
client's MS-SDP and Teams' answer back, strictly.

Everything follows what the probe sent in the data-only joins that got
captions live; those values are now written down in the protocol docs.
The module's glue comes next.
Step 4 of the live-captions spec wires the pure session, Trouter and
MS-SDP code to the host. Three new Teams tools are added:
teams_join_captions (a write that always waits for the user's
approval), teams_read_captions and teams_leave_captions.

After the approved call, the module finds the meeting's call in the
call store, opens Trouter and returns. The rest runs in events:
registration, the media connection's offer, a session record, the join,
the SDP answer, the data channel's captions and roster reads. Every way
out sends leave as open_mess's own endpoint: the tool, deadlines, the
meeting's end, the user leaving, end-sessions, sign-out and a restored
instance. A leave that fails is retried from the record.

The manifest gains the tools, the conversation service host and media
peers from Microsoft's published Teams media ranges; the meeting-chat
pointers of spec 4.5 go into the tool descriptions. In tests the host
can now serve module WebSockets from a fake server, and the Teams tests
run whole calls against a fake Trouter and a WebRTC peer on loopback.
Two findings of the review of 96070fd.

While Trouter was down, the first roster read that still found the
user ended the 10 s reads, so a user who left afterwards stayed shown
in the meeting until Trouter came back. Trouter being down is now its
own state: roster reads go on every 10 s until it is back, and captions
wait for it.

The 30 s check after the data channel opened was skipped whenever
captions had been on at the join, so captions turned off before the
channel opened kept the session for 5 minutes. The check now looks at
the captions 30 s after the channel opened, whatever came before.

An offer with no IPv4 candidate is recorded as a known limit.
Findings of the review of 7385fdd, all about the leave that ends what
others see:

- no join without its session record, so a crash can always leave it;
- leave before the registration's removal, so it fits in end-sessions'
  5 s;
- sign-out tries every recorded leg once more, also one no session
  holds any longer;
- a session whose tokens WAM stopped giving keeps its record and its
  retries, which go on without a session, and signing in again leaves
  the recorded legs.

A new test fails a leave and checks that restore and sign-out send it
again from the record.
Two findings of the review of 263a526. A first socket that fell before
its trouter.connected left the session marked down after the next one
connected, so every caption was dropped. A roster read during a gap
saw captions come back but did not stop the five minutes of captions
off, which could end a session whose captions were on again.
The live acceptance test (step 5 of the live captions spec, "Live
results" 19 in docs/protocols/teams.md) found two things the module got
wrong against the real service:

- Callbacks after a join come gzip-compressed with a base64 body, since
  the probe's endpointCapabilities has bit 128. The module answered them
  400, and Teams ended the leg (/call/end/, 430, 10067). They are now
  inflated (gzip, else zlib, at most 1 MiB).
- The data-only SDP answer carries RTP stream lines (a=ssrc-group), which
  the strict converter refused. a=ssrc*, a=msid* and a=rtcp-rsize are now
  dropped; other unknown lines are still an error, naming all of them.

/call/end/ ends our leg, not the meeting, so it now ends the session as
refused, and meeting_ended is kept for /conversation/conversationEnd/;
the end code and sub-code go to the log. For diagnosis the daemon logs
to stderr when OPEN_MESS_LOG names a level (off by default), and the
module logs each session step with states, codes and counts only.
At debug level rmcp logs whole tool results, so OPEN_MESS_LOG=debug put
message and caption text on stderr (Codex review of 0cff6c1). The
diagnostics subscriber now passes only the open_mess and open_mess_host
targets. A test of the two end callbacks through the fake Trouter covers
the /call/end/ (refused) and conversationEnd (meeting_ended) mapping
(GLM review of 0cff6c1, P3).
The live captions branch grew from early copies of the module read
tools, meetings and captions spec branches (PRs 10, 11 and 12). This
merges it onto their reviewed tips, with our copy of their commits as
the merge base, so their review fixes stay: the WAM scope texts and test
from PR 10, and the meetings and transcript fixes from PR 11. The rest
of the conflicts were the captions work's additions (module write tools,
media ranges, webrtc) next to lines the PRs left as they were.
WinHttpGetIEProxyConfigForCurrentUser can fail for reasons other than
there being no settings, and that was taken as no auto-configuration: a
PAC script that requires a proxy could then be skipped and a media
connection made directly, showing the network the user's address (Codex
review of b21b25d). Only "no settings at all" still means none; any
other error refuses. The auto-configuration answer is unused off
Windows, which clippy on Linux reports as dead code; that is allowed
there now.
#12 landed on main squashed, so this branch and main no longer share its
commits. The tree is this branch's own tree unchanged; main brings nothing
new beyond what the branch already had.
@SychPL
SychPL changed the base branch from docs/teams-live-captions to main November 5, 2026 12:54
@SychPL
SychPL merged commit e82d8cc into main Nov 5, 2026
3 of 6 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