feat(voice-call): Microsoft Teams provider β CVI voice/video calls#91438
feat(voice-call): Microsoft Teams provider β CVI voice/video calls#91438ahenawy wants to merge 62 commits into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 9:34 AM ET / 13:34 UTC. Summary PR surface: Source +4073, Tests +2999, Docs +177. Total +7249 across 47 files. Reproducibility: not applicable. this is a new feature PR, not a current-main bug with a reproduction path. Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Choose this PR as the canonical Teams CVI foundation only after conflicts are resolved, current-head checks pass, and maintainers explicitly accept the provider, SDK, companion-worker, session, and security boundaries. Do we have a high-confidence way to reproduce the issue? Not applicable: this is a new feature PR, not a current-main bug with a reproduction path. Is this the best way to solve the issue? Unclear as a final product decision: the implementation is a credible focused foundation and not obsolete, but maintainers still need to choose the canonical Teams/CVI/API/security shape before merge. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against be2c4c65abd9. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +4073, Tests +2999, Docs +177. Total +7249 across 47 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
b431b9e to
5a9d226
Compare
5a9d226 to
f11e881
Compare
f11e881 to
011ee29
Compare
|
@clawsweeper review Rebased onto latest
All gating checks were green before the rebase ( The remaining item is the maintainer product/API-acceptance decision for the new |
Maintainer sign-off context β new
|
|
π¦π§Ή I asked ClawSweeper to review this item again. Re-review progress:
|
streamPcmFrames relied on session.send throwing to abort playback when the Teams worker socket closed mid-frame, but session.send -> sendTo dropped the frame silently and returned, so playback advanced seq/timestamps and let manager.speak treat the audio as delivered on a dead socket. - sendTo / MsteamsSession.send now return whether the message was delivered (false on a closed/missing socket); best-effort control frames ignore it - streamPcmFrames throws when an audio.frame is dropped, so playTts/speak finalize the turn instead of reporting false success - test: session.send is true while open, false once the socket has closed
|
Update β rebased on latest Rebased onto current Review findings β fixed1.
2. Closed-socket sends could report playback success on a dead socket.
Also on the branch: realtime visual tools honor New config / SDK surface β compatibility rationaleFlagged for maintainer signoff (
No Validation
|
|
@clawsweeper review The last two re-reviews crashed before completing ("off-meta tidepool / review did not complete"). Requesting a fresh review of head |
|
π¦π§Ή I asked ClawSweeper to review this item again. Re-review progress:
|
- msteams-realtime.test.ts: the mock session's `send` returns boolean (was the number from Array.push) now that MsteamsSession.send reports delivery - msteams.test.ts: the getCallStatus realtime test reads the provider from setup()'s non-undefined return instead of the module-level optional
β¦enclaw#91438 + openclaw#92081) docs/channels/msteams.md never carried the Teams integration's feature config β add the channel governance keys (dlp / auditChannel / transcribeVoiceMessages), a Governance section (DLP redaction, audit-log mirror, voice-message transcription), the 'Ask OpenClaw about this' message action, and a Voice/CVI summary that cross-links the voice-call plugin doc. In docs/plugins/voice-call.md fill the msteams-voice gaps: greet-by-name + deterministic verbal interrupts (EN/AR), bilingual, meetingRecap, DTMF/IVR, outbound notify modes (defaultMode/notifyHangupDelaySec/greet-on-answer), and the realtime echo / self-answer guard (suppressInputDuringPlayback + window/RMS knobs). Covers the foundation (openclaw#91438) features too since that PR is frozen. Co-Authored-By: Claude Fable 5 <[email protected]>
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
|
Thanks, Alaaeldin. Peter has decided to keep this work alive. Since this branch was written, current main gained the shared meeting-bot and This PR was auto-closed by the stale bot on July 23; reopening it as the focused foundation record while the staged path is reviewed. The broader plan lives on #94978. |
|
ClawSweeper status: review started. I am starting a fresh review of this pull request: feat(voice-call): Microsoft Teams provider β CVI voice/video calls This is item 1/1 in the current shard. Shard 0/1. This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking. Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted. |
|
Closing with the same maintainer decision as #94978 (comment) follow-up there: the required |
Summary
Adds the Microsoft Teams (
msteams) provider to thevoice-callextension β inbound voice + inbound video vision, group/meeting awareness, and outbound "call me back" β over an HMAC-authenticated WebSocket endpoint. Works in 1:1 and group/meeting calls, on both voice-call modes (streaming: STT β agent β TTS, and realtime: speech-to-speech).This is a refreshed, standalone superset of the earlier Teams PRs (see Relationship below) and is intended to replace them. It also carries the CVI rendering drivers β emotion expression cues, the viseme lip-shape spike, and assistant visual sharing (
show_to_callerβ the bot shows an image on its tile) β plus realtime continuous vision (sendImage, a small core plugin-sdk addition). The companion .NET worker renders; this PR sends the drivers.β¨ The big picture: a Conversational Video Interface (CVI) for OpenClaw
This PR is the foundation for AI Video Agents β bringing an interactive, face-to-face video interface to OpenClaw. The Conversational Video Interface (CVI) is the end-to-end pipeline for face-to-face AI, and its three pillars map directly onto what lands here:
video.framevision βlook_at_screen+ streaming auto-attach + realtime continuous vision (sendImageambient frame push); camera + screen-share; per-participant attribution in meetingsspeech.marks), and visual-sharing (show_to_callerβdisplay.image) senders β the companion worker also adds head bob/sway, eye-blink, and gaze toward the active speaker (the tile is rendered by the companion .NET media worker; this PR sends the drivers)Together these turn a Teams call into a true two-way video conversation with the agent: it watches what you show, talks with you in real time, and appears as an animated face. This is the centerpiece β perception and dialogue land in this PR; the rendering pillar runs in the companion media worker (source to be shared β see below), and richer continuous face-to-face video is the roadmap.
What's in this PR
Voice:
msteamsprovider, caller allowlist (safe-by-default, closed unless matched), streaming + realtime paths, recording-status gate before any media-derived data is processed.Inbound video (see):
video.frameingestion (buffered per source), thelook_at_screentool (realtime) + automatic per-turn frame attachment (streaming, so the agent is visually aware each turn), recording-gated and rate-limited. The agent sees the caller's camera and screen-share (VBSS). A per-call vision spend cap (msteams.maxVisionPerMinute, default 30, 0 = unlimited) bounds vision-model cost.Group / meeting calls:
video.framecarries the participant it belongs to (participantId/participantName); the streaming path labels the attached image andlook_at_screennames the participant, so the vision model can reason about who is showing what.wakePhrases(case-insensitive, boundary-aware) β mirroring the chat channel's group @mention gate. Once addressed, a short follow-up window (followUpWindowMs) lets the back-and-forth continue without repeating the name, then it goes quiet again. 1:1 calls respond to everything (gate off). Deterministic on the streaming path (shouldRespondToGroupTurnkeyed on the liveparticipantscount); instruction-based on realtime.Realtime delegation & background tasks: in realtime (speech-to-speech) mode the model handles small talk itself and delegates real work to the OpenClaw agent β
openclaw_agent_consultfor quick in-line answers (a short "working on it" filler covers longer agent runs), and (underrealtime.toolPolicy: "owner")openclaw_agent_taskto run a long job in the background. A background result is delivered two ways: a Teams chat message (deliverVia:"message", the default) or an outbound voice call-back (deliverVia:"call"). Both are gated on recording status. If the caller has no Teams chat target (anonymous), a background task is refused up front rather than acking a delivery that can't happen.Outbound (call me back):
initiateCallplaces a Teams call via the provider's authenticated endpoint (SSRF-guarded); on the call-back the agent speaks the result, then hangs up (the result-delivery instruction is hardened so it states the result, not a generic greeting). If the callee doesn't answer, the bot leaves that same result as a Teams voicemail β which Teams' own transcription then emails to them β so the answer reaches the caller even when the call-back is missed. A configurable no-answer timeout (msteams.outbound.answerTimeoutMs, default 120s) finalizes a call that never connects, and timed-out call ids auto-expire so the dedupe set can't grow unbounded.tenantIdinherited fromchannels.msteams.Avatar expression cues: a cheap, dependency-free lexical heuristic (
expression.ts, no extra model call) infers a coarse emotion β neutral / happy / sad / surprised β from the assistant's reply text and sends it to the worker as an additiveexpressionmessage, on both the streaming (playTts) and realtime (assistant transcript, cued early + self-correcting) paths. The avatar shapes its mouth (smile / frown / round "O") accordingly. Best-effort and cosmetic: a send failure never disrupts the call, audio is untouched, and an older worker ignores the message.Avatar viseme lip-shape (spike): an additive
speech.marksmessage carries a viseme timeline so the avatar mouth changes shape per sound (wide "ee", round "oo", closed "m/b/p"), blended over the worker's RMS openness. The spike derives marks from the reply text (estimateVisemes); the full pass uses real ElevenLabs/with-timestampscharacter timing (visemesFromAlignment, landed) instead of an even spread. Streaming path only; falls back to RMS-only when absent.Assistant visual sharing (
show_to_caller): the bot can show the caller an image on its video tile mid-call. A realtime toolshow_to_callerruns the agent (so its screenshot / image-generation tools are available) to produce one image;consultRealtimeVoiceAgentreturns the produced trusted-local file path (remote URLs excluded β no SSRF surface), and the bridge reads it (image-type + 4 MB cap) and sends an additivedisplay.imagemessage the worker renders fullscreen for a few seconds, then returns to the avatar. Recording-gated like the other agent tools.Realtime continuous vision (
sendImage): a newRealtimeVoiceBridge.sendImage(a small core plugin-sdk addition + the openai/azure realtime provider) pushes the latest changed inbound frame into the realtime session as an ambientinput_imageconversation item with no forced response, every ~6 s, budget-capped and recording-gated β so the realtime model is continuously visually aware, not just onlook_at_screen.Sessions & memory: caller identity (Teams AAD object id, else a per-call-unique id) keys the session, so per-caller memory works and distinct anonymous callers never collide; sessions end cleanly on socket close.
Code quality / refactoring
Shared types & single-source defaults:
MsteamsVideoFrametype (msteams-video-frame.ts) β the provider and the realtime path reference one type instead of a structural duplicate (type-only import, no runtime cycle).resolveGroupCallGateConfigis the one source of the group-call defaults, used by the config layer, runtime wiring, and provider (no triplicated?? true / ?? [...] / ?? 12000).MSTEAMS_PCM_SAMPLE_RATE_HZ(16 kHz) defined once; provider/realtime/tts alias it.Extracted shared helpers (kill copy-paste in the provider + realtime paths):
createStreamingCallStateβ one STT-session + per-call-state + connect/flush/fail factory shared by the inbound and outbound session-start paths.runMsteamsConsultβ one consult invocation (model + thinking-level resolution +consultRealtimeVoiceAgent) shared by the consult /look_at_screen/ background-task handlers.readArgText(inutils.ts) β shared tool-arg reader (replacedreadArgString/readConsultArgText).describeMsteamsVideoFrameOwnerβ one place builds the "<name>'s camera/shared screen" vision-attribution string for both streaming and realtime.clearTimedOutOutboundhelper + auto-expiry β one place forgets a timed-out outbound id and cancels its (unref'd) expiry timer; called from late-connect, session-end, and teardown.Correctness/robustness:
getLatestFramenow passed on call-backs too (was inbound-only), solook_at_screenworks if the callee shares video.TtsConfigSchema.parse, not a bare cast) and surfacedaudio.framesend failures (streamPcmFramestry/catch β stop on WS failure) instead of silently succeeding.group-call-gate.ts:isAddressed,shouldRespondToGroupTurn).Relationship to #89724, #91107, #91176
msteamsprovider.Safety & compliance
Real behavior proof
Behavior or issue addressed: Inbound Teams voice + video (
look_at_screen) in 1:1 and group calls, the group "speak only when addressed" gate, a Teams-chat task callback, and an outbound voice call-back.Real environment tested: Live 1:1 and 3-party group Microsoft Teams calls against a running OpenClaw gateway (realtime path, vision-capable model). Real calls, not mocks.
Exact steps or command run after this patch:
pnpm openclaw gateway runwith themsteamsprovider + realtime + outbound enabled.Evidence (copied live output, redacted):
Observed result: On a live call the agent described the shared screen, correctly tracked a content switch (openclaw.ai β the openclaw GitHub page), described the camera, responded to the "Hey assistant" wake phrase, and stated the resolved result on the outbound call-back (incl. to voicemail when unanswered). The avatar renders as a live video tile.
Attached evidence (redacted, from the live call):
1. Be-seen avatar β the bot publishes its avatar as a live video tile
2. Outbound "call me back" β the bot rings the caller on Teams
3. Result delivery even to voicemail β Teams transcribes the call-back's spoken result
4. Screen recording of the live call
5.
show_to_callerβ the bot screenshots a page and displays it on its own video tile mid-callAgent transcripts (from the gateway logs, redacted):
show_to_callerdisplay path (gateway + worker logs, redacted): on a live call the bot browsed to the OpenClaw GitHub page, screenshotted it, and rendered it on its own video tile.What was not tested: None β all paths in this PR were exercised on live Microsoft Teams calls (including
show_to_caller, proven above).Proof limitations or environment constraints: Evidence is copied live output, redacted to avoid publishing screen/camera frames, call identifiers, and account details.
Config (example)
tenantIdis read fromchannels.msteams.tenantId.groupCallonly affects calls with 2+ humans; 1:1 always responds.maxVisionPerMinute(default 30,0= unlimited) caps the per-call vision spend across all three consumers βlook_at_screen, streaming auto-attach, and the realtime ambientsendImagepush.Companion media worker & follow-ups
Microsoft.Skype.Bots.Media, Windows-only) that pairs with this provider over the HMAC WebSocket bridge. It renders the be-seen avatar β RMS lip-sync, subtle head bob/sway, eye-blink, attention gaze toward the active speaker, emotion-shaped and viseme-shaped mouths (driven by this PR'sexpression/speech.markssenders), and thedisplay.imagetile (show_to_caller) β and does adaptive inbound-video sampling (caller-speech + scene-change β faster sampling) with static-frame dedup. Per the maintainer's suggestion the worker source will be shared; candidate home isopenclaw/openclaw(vendored subdirectory) or a dedicatedopenclaw/openclaw-windows-noderepo. The bridge protocol is already versioned and additive, so either layout works β it's a repo-layout / release-ownership decision, not a code one./with-timestampsreal timing needs the TTS-path threading (optionalspeechMarkson the telephony result βplayTts), additive/opt-in so other channels are untouched.