Skip to content

feat(msteams): Teams voice (CVI) + chat + governance integration#92081

Closed
ahenawy wants to merge 117 commits into
openclaw:mainfrom
ahenawy:feat/msteams-cvi-next
Closed

feat(msteams): Teams voice (CVI) + chat + governance integration#92081
ahenawy wants to merge 117 commits into
openclaw:mainfrom
ahenawy:feat/msteams-cvi-next

Conversation

@ahenawy

@ahenawy ahenawy commented Jun 11, 2026

Copy link
Copy Markdown

Stacked on #91438 — review the increment (df7c31a106…a4477208de, 55 commits): the foundation is #91438. The full diff includes #91438's commits only until that PR lands; this branch then rebases to exactly the increment. A commit→feature map is below.

Summary

Builds on the msteams voice provider (#91438) along six lines:

  • Correctness / security — fixes the realtime self-answer (echo) bug, makes the bridge handshake replay-proof, enforces group etiquette and verbal interrupts deterministically (in code at the audio egress, not model-trusted), and fixes outbound call-backs that spoke to a still-ringing phone (and the context-bleed that caused).
  • Call UX — verbal interrupts ("stop" / wake-phrase forms, English and Arabic), notify call-backs (deliver → hang up, un-clipped), greet-by-name from the roster, bilingual Arabic/English, DTMF / IVR ("press 1 to…").
  • CVI — retroactive vision ("what did the earlier slide say?"), scene-change + camera/screen ambient vision, captioned/slideshow visual sharing as a picture-in-picture overlay over the avatar, a "thinking" expression, per-thread sessions.
  • Meeting productivity — opt-in end-of-meeting minutes posted to the Teams chat/thread, on-demand "summarize the meeting" mid-call, a Word-openable minutes document, and per-person attribution from unmixed meeting audio.
  • Chat — a Teams "Ask OpenClaw about this" message action routes a selected chat message to the agent; opt-in voice-message transcription folds spoken clips into the agent's turn.
  • Governance — opt-in audit-log channel mirroring agent replies and DLP-aware outbound redaction.

A post-feature self-review hardening pass then audited the whole increment and fixed everything it found (2 critical, 13 bugs, 1 security finding, structural refactors) — see "Review hardening" below.

Layer Themes
Correctness / security echo self-answer fix (suppressInputDuringPlayback → on), replay-proof HMAC tuples, deterministic group gate on realtime, playout clock for echo + notify drain, greet-on-answer (outbound call-backs no longer deliver to a ringing phone)
Call UX verbal interrupts (EN + AR), notify call-backs (deliver → hang up), roster greeting by name, bilingual Arabic/English, DTMF / IVR
CVI retroactive vision (16-keyframe ring), scene-change + camera/screen vision, captions + slideshow, PiP overlay, "thinking" expression, per-thread sessions
Meeting productivity end-of-meeting recap to chat/thread, on-demand /summarize, DOCX minutes artifact, per-speaker attribution (unmixed audio)
Chat "Ask OpenClaw about this" message action, voice-message transcription
Governance audit-log channel, DLP outbound redaction
Review hardening single per-call cleanup owner, deliver-to-ringing (streaming), un-hangup-able call, 10 more bugs, withConsultGuards + structural refactors

⚠️ Behavior changes

  • realtime.suppressInputDuringPlayback default flipped off → on: default-off was the live self-answer bug — the bot answered its own echo on every realtime deployment. Set false to restore; tune via realtime.echoSuppressionWindowMs / realtime.echoBargeInRms.
  • Outbound call-backs now honor outbound.defaultMode (default notify = state the result, then hang up after the audio drains) and speak only once the callee answers (recording-active), not when the bridge connects — on both the realtime and streaming paths. Previously they greeted on connect — i.e. while the phone was still ringing — so the result was lost and the idle model could surface unrelated context. Set "conversation" to keep the lingering behavior.
  • msteams.meetingRecap (default off) — when on, the bot posts meeting minutes to the caller's Teams chat (1:1) or the meeting thread (group) at call end. The bot posting unprompted is gated behind this opt-in.
  • msteams.bilingual (default off) — when on, the realtime model is pinned to detect/mirror Arabic↔English and translate on request. The deterministic layers (verbal interrupts, visemes) understand Arabic regardless.
  • channels.msteams.transcribeVoiceMessages (default off) — when on, inbound Teams voice clips are transcribed (STT cost) and folded into the agent's message text.
  • channels.msteams.auditChannel (unset by default) — when set to a conversation id, agent replies are mirrored there for compliance review.
  • channels.msteams.dlp.enabled (default off) — when on, built-in categories and customPatterns are redacted from every outbound surface (block messages, streamed replies, edits, adaptive cards). Because per-token streaming cannot be redacted retroactively (a secret completing across chunk boundaries would already be on screen), DLP also downgrades streaming.mode: "partial" to "progress": the live card shows working status and the redacted full reply lands on it in place.

🛡️ Security & correctness hardening

  • Single-use HMAC upgrade tuples (replay guard) — a captured handshake replayed within the HMAC window could open a ghost session after the live call ended. (callId, timestamp, signature) tuples are single-use; a legitimate reconnect carries a fresh timestamp; only verified tuples are recorded (no unauthenticated map growth) and entries expire with the timestamp's own validity window (a future-dated handshake from worker clock skew can't outlive its record). Tested: connect OK → exact replay 401 → fresh-timestamp reconnect OK.
  • Deterministic group-call gate (realtime) — every finished caller turn is evaluated with the same shouldRespondToGroupTurn core the streaming path uses; an unaddressed meeting turn's reply audio is dropped at the egress. 1:1 never gates.
  • Echo guard on a playout clock — the realtime model streams faster than realtime, so last-send time is not play-time; the guard and the notify drain both key on a playout estimate (accumulated chunk durations, collapsed on barge-in). Regression test: burst 10 s of audio → echo still suppressed at +2 s. Opening-greeting hardening (live-reported): until the caller's first real turn the guard allows no barge-in, so the bot's own greeting echoing back on a speakerphone can't make the model interrupt itself and re-greet in a loop while the caller is silent.
  • Greet-on-answer (outbound call-backs) — the greeting (the delivered result) now fires on recording-active (the callee answered), not on bridge-connect; this both delivers the result at the right time and closes the idle window where the model would otherwise free-associate from its context.

📞 Call experience

  • Deterministic verbal interrupts — "stop" / "wait" / "hold on" / "never mind" handled in code: streaming cuts playback and suppresses the reply; realtime flushes the worker queue. Whole-utterance match with filler and configured wake-phrase stripping"⟨name⟩, stop" cuts as instantly as bare "stop"; "stop by the store" never triggers. Unicode-aware: Arabic interrupts ("توقف", "خلاص", …) cut identically, with tashkeel-safe normalization (same \p{L} approach as the group gate).
  • Roster greeting — the model greets the caller by first name (from the Teams roster) and addresses participants by the per-utterance speaker label.
  • Bilingual Arabic/English (opt-in) — detect, mirror, switch-with-caller, translate on request; the viseme estimator also maps Arabic graphemes so lip-sync isn't RMS-only in Arabic.
  • DTMF / IVR — in-band keypad tones from the caller are surfaced to the realtime model as a user message ("the caller pressed 1"), so the agent can run "press 1 to…" flows. (Worker companion forwards the tone.)
  • Notify call-backs deliver-and-hang-up without clipping — drain-aware (playout clock) + notifyHangupDelaySec grace; a failed hangup falls back to a direct teardown.

🎥 CVI extensions

  • Retroactive vision (frame history) — a ring of 16 scene-change keyframes per call; look_at_screen scope:"history" attaches up to 6 timestamped, attributed keyframes to one budgeted consult. No eager OCR-per-keyframe.
  • Scene-change-driven ambient vision — frames reach the realtime model on arrival; camera + screen simultaneously, per-source dedup + labels.
  • show_to_caller captions + slideshow + PiP overlay — caption strip on the tile; multi-image results play as a paced slideshow; images ride as a bottom-right inset over the live avatar (display.image mode:"overlay") so the bot stays visible. Additive protocol; older renderers fall back to fullscreen.
  • "Thinking" avatar expression while a tool runs; the reply re-cues the real emotion.
  • Real lip-sync timing — the elevenlabs provider's telephony synthesis uses /with-timestamps and surfaces per-character alignment (optional field through textToSpeechTelephony); msteams speech.marks now carry each sound's actual start time instead of an even spread, falling back to the estimator when alignment is absent. Audio never depends on timing: a failing /with-timestamps endpoint degrades to plain synthesis.
  • Per-thread sessionssessionScope: "per-thread" keys the agent session by the Teams meeting/chat thread; parallel conversations stay isolated. PSTN falls through to per-phone.

📝 Meeting productivity

  • End-of-meeting recap (opt-in msteams.meetingRecap) — key points / decisions / action items posted to the caller's Teams chat (1:1) or into the meeting thread (group) when the call ends.
  • On-demand minutes — "summarize the meeting" mid-call posts the running minutes to chat without ending the call (post_meeting_minutes tool).
  • Word-openable minutes artifact — the recap attaches a formatted document the caller can open in Word.
  • Per-person attribution — with unmixed meeting audio the worker tags each turn with the speaker, so minutes and transcripts read "Sara: …" instead of one mixed stream.

💬 Chat & governance

  • "Ask OpenClaw about this" message action — a Teams message-context command (composeExtension/submitAction) quotes the selected chat message into a prompt and dispatches it through the normal message path, so the answer posts back into the conversation. (Requires the Teams app manifest to declare the command: composeExtensions > commands, type: "action", context: ["message"], id: "askOpenClaw".)
  • Voice messages (opt-in transcribeVoiceMessages) — inbound Teams audio clips are transcribed and folded into the agent's turn as a quoted block; a clip that fails transcription keeps its attachment placeholder.
  • Audit-log channel (opt-in auditChannel) — agent replies are mirrored to a named Teams conversation for compliance review. The mirror fires at the delivery choke point, so natively-streamed replies (the DM default) are in the trail too, not only block deliveries; the loop guard keeps the audit channel's own traffic out.
  • DLP outbound redaction (opt-in dlp.enabled) — built-in categories plus custom patterns are redacted with a configurable placeholder, on every outbound surface: block messages, streamed replies (see Behavior changes), message edits, and adaptive cards (deep-redacted per string value). The audit excerpt is redacted before truncation so the cap can never slice a secret into an unrecognizable fragment.

🔎 Review hardening (post-feature self-audit)

After the feature work, a full self-review of the increment found and fixed — each with a regression test:

  • 2 critical — realtime mode never wired the CallManager (the stale-call reaper force-ended connected outbound callbacks ~2 min in); the msteams safe-allowlist inbound default was dead code (msteams+streaming rejected ALL inbound calls in production config). (c38d360642)
  • 1 security — HMAC replay records now expire at ts+window, not now+window, closing a replay window for future-dated (clock-skewed) handshakes. (bd5dbc06f3)
  • 4 state-leak paths, one design fix — a single releaseCallState owner that every teardown funnels through: host-initiated closes no longer suppress onSessionEnd (dead calls reported in-progress forever); a realtime provider WS drop no longer strands the caller on an un-hangup-able call; manager hangups release vision/recording state (~1–2 MB/call); a declined outbound call no longer leaves its CallRecord active against maxConcurrentCalls. (15f5efbe2e)
  • 9 individual bugs — deliver-to-ringing on the streaming path; custom-tool calls stalling the model turn; unbounded/misattributed transcript coalescing; the message action being mention-gated to silence in group chats; history-look poisoning the live vision cache; the minutes tool skipping the recording gate + the task tool acking a no-op; empty-aadId cross-caller session bleed; lost-and-budget-burning ambient vision pushes; voice-clip placeholder mishandling. (cb29dcae1b)
  • Structural refactors so this class of bug has fewer places to hide — withConsultGuards (one copy of the recording-gate/deps/error/filler boilerplate for all agent-running tools), emitCallEnded, a single hoisted consult session key, the tool surface split into msteams-realtime-tools.ts, and the msteams runtime no longer building the webhook-plane realtime handler Teams never uses. (9305586f1a)
  • Bilingual honored by the deterministic layers — verbal interrupts match Arabic (Unicode normalization + Arabic phrase set) and the viseme map covers Arabic graphemes. (eace301545)
  • 4 governance bypasses (the review's most severe findings) — DLP/audit only covered the block-delivery text path. Now: native streaming redacted (token streaming downgrades to progress under DLP — append-only streams can't retract a secret that completes across chunks); the audit mirror moved to the delivery choke point so streamed replies are in the trail; message edits and adaptive cards redact (closing the "edit the secret in" / "put it in a card" loopholes); the secret pattern catches segmented sk-proj-/sk-ant- keys and the audit excerpt redacts before truncating. (4bf60c2850)
  • Poll-card DLP redaction (Codex review follow-up) — the ClawSweeper Codex pass caught a remaining card surface: sendPollMSTeams sent the poll Adaptive Card unredacted (the S3 fix only covered the generic adaptive-card path), so a secret in a poll question or option label reached the wire. The poll's agent-supplied question and option strings are now redacted before the card is built — visible text only, leaving the internal option indices and pollId intact so vote matching is unaffected. (fe2f789935)
  • DTMF recording gate (Codex review follow-up) — DTMF keypad input was forwarded to the realtime model before the call's recording status was active, bypassing the Media Access API recording gate that governs audio/video/transcripts/consults. notifyDtmf now drops keypresses until recording is active (regression test included). (63f7f30e88)

Commit → feature map (increment beyond #91438)

Feature Commits
Refactors (vision store, tts playback) df7c31a106, f5cf55f995
Echo / self-answer fix (+ knobs, playout clock) 7dadcdfb12, 05733cf2e6, 93173defcf
Captions + slideshow 54f6e6acc1, 741040a223
Notify call-backs (mode + drain + greet-on-answer) 41be3c690a, c7fbb6e356, 2cfcb9ddad
Scene-change + camera/screen vision 8556d2b7a4, 303a49a0c8
"Thinking" expression 0b4396b737
Replay guard (+ ts-window expiry) 323188145c, bd5dbc06f3
Deterministic group gate 208329c03e
Retroactive vision (keyframe ring) 074fcd6a4a
Per-thread sessions 54bbe7f30e
Verbal interrupts (+ wake-phrase form, + Arabic) e93b06676e, 4ef6fe9d5f, eace301545
Meeting recap + thread + on-demand + DOCX + attribution 4be87282dd, 79d9c845df, ee1b8f7baa, 6a5a1f3214, 86dd65ebc8, bbd4cff526
PiP overlay (#17) f5c8700ee1
Roster greeting (#20) c1d1b6a8ec
Bilingual (#19) 7e151742ce, eace301545
DTMF / IVR (#21) 4ac7780eed
"Ask OpenClaw about this" message action (#10) 6005f65383
Voice-message transcription (#13) 15517f6657
DLP outbound redaction (#16) ef5edc119c
Audit-log channel (#15) 0d2d12fd45, 4f40555b4e
Review hardening (critical / security / bugs / refactors) c38d360642, f37488cf1f, 15f5efbe2e, cb29dcae1b, 9305586f1a
Governance bypass closure (streaming/edit/card DLP + audit choke point) 4bf60c2850
Regenerated bundled channel metadata (governance keys were unconfigurable) 90b7805cfd
Real viseme timing (ElevenLabs with-timestamps alignment) 0ef6f8e570
Poll-card DLP redaction (Codex review follow-up) fe2f789935
Regenerated config artifacts after upstream rebase a447350c52
Docs: Teams channel governance + voice/CVI config (#91438+#92081) a20d9077fd
Fix: opening-greeting echo loop on speakerphone (msteams realtime) be9cff4615
Fix: DTMF recording-gate bypass (Codex P1) 63f7f30e88
Chore: re-sync onto upstream — SDK export budget 10285/5164 (+1 RealtimeVoiceImageInput) + regen baseline a4477208de
Regenerated config artifacts (upstream re-sync) 6285200889

Each feature ships with unit tests (matcher/positive-negative units, display-path, history-ring, group-gate egress drop, replay 401/reconnect, recap prompt, DTMF mapping, message-action extraction, teardown/leak regressions, Arabic interrupt/viseme cases, etc.).

✅ Live validation (2026-06-12, real Teams calls)

Foundation flows (base voice/video, group calls, background-task chat delivery, outbound callback) were separately live-proven on the #91438 foundation PR — see its Real-behavior-proof section; this PR’s diff includes those commits until #91438 lands.

Re-validated end-to-end on this branch's exact head against a production Teams tenant (gateway from source, companion media worker as a Windows service), in two calls:

  • Inbound 1:1 call (~7.5 min) — roster greeting by name; no echo self-answer with the reply on open speakers; barge-in; deterministic verbal interrupts in English, wake-phrase form, and Arabic ("توقف"); bilingual Arabic↔English mirroring + on-request translation; openclaw_agent_consult with the "thinking" avatar expression held across six tool runs (worker log); emotion cues (Happy/Neutral transitions) tracking reply tone; screen-share vision (look_at_screen, scene-change awareness, retroactive keyframe history — two VBSS share windows in the worker log); show_to_caller rendered two images as a PiP overlay with caption, paced as a slideshow with exactly the coded hold times (4500 ms non-final / 5000 ms final).
  • Outbound notify call-back (placed by an in-call background task): outbound call placed → Established → recording-active gate opened → result spoken only after answer (greet-on-answer) → notify-mode auto-hangup after the audio drained — complete, un-clipped delivery, clean teardown, no leaked call state.
  • Meeting productivity — on-hangup recap fired (posting meeting recap) and the minutes landed in the Teams chat; in-call background task delivered its answer to chat.
  • Both calls tore down cleanly (connection closed with no lingering state) — the review-hardening teardown paths behaving live.
  • The validation also caught that the governance keys were unconfigurable (stale generated channel metadata + a Windows-silent regen script) — fixed in 90b7805cfd; the gateway now starts with dlp/transcribeVoiceMessages/auditChannel configured.

Chat & governance (same day, follow-up session):

  • Audit mirror on streamed replies (S2) — with auditChannel set to a dedicated group chat, every streamed DM reply produced a 🧾 Reply for <sender> in <conversation>: <excerpt> line in the audit chat; the bot's own reply inside the audit chat produced no self-mirror (loop guard).
  • DLP on the wire (S1) — a DM reply containing an email rendered as …([REDACTED:email]) in the Teams client: the model emitted the address, the redaction layer scrubbed it on the streamed path — the exact bypass this PR closes. (The secret category never reached the wire live because the model itself refused to repeat even a dummy sk-proj-… token — twice; the category patterns are unit-tested and the wire path is category-agnostic.)
  • Message action (Fix path traversal vulnerability in media server #10)Ask OpenClaw about this on a DM message dispatched the quoted-message prompt and produced a substantive reply in the conversation. (The group/mention-gate case is unit-tested.)
  • Voice-message transcription (Feature Request: Group Chat Mention Detection #13) — not live-testable in this tenant: Teams desktop's mic-only "video clip" is a stream attachment that delivers no media to bots (the bot correctly reported only the text label arrived), and the tenant's mobile client has no voice-clip button. Unit-tested; the graceful no-media handling was observed live.
  • Mention gating — unmentioned messages in the group chat were correctly ignored; a mentioned message was answered.

Real lip-sync timing (2026-06-13, follow-up): the ElevenLabs /with-timestamps alignment path (0ef6f8e570) was validated on a live Teams call with the SVG face avatar and debug logging. The greeting's mouth track was driven by real per-character timing, not the even-spread estimator:

04:53:23 MsteamsProvider: session.start callId=22005380-…
04:53:24 MsteamsProvider: recording.status 22005380-… = active
04:53:2x MsteamsProvider: expression cue 'neutral' for 22005380-…        ← debug level active
04:53:2x MsteamsProvider: speech.marks 60 visemes (aligned) for 22005380-…   ← real ElevenLabs alignment
04:53:36 [worker] Call status updated to Terminated - The conversation has ended
04:53:36 [worker] OpenClaw bridge closed for call 22005380-… (call-ended); disposed 0 audioMediaBuffers

(aligned), not (estimated) — the with-timestamps endpoint succeeded with no fallback, and the 60 marks carry each character's actual start time. The caller-hangup teardown was clean on both sides (Graph Terminated → bridge closed → disposed, no leak). Full evidence: docs/proofs/LIVE-TEST-92081.md in the companion worker repo.

Real behavior proof

  • Behavior or issue addressed: End-to-end behavior of this PR on real Microsoft Teams calls and chats: realtime voice (echo self-answer fix, deterministic verbal interrupts incl. Arabic, roster greeting, bilingual), CVI vision (look_at_screen, scene-change ambient push, retroactive history, show_to_caller PiP/caption/slideshow), outbound notify call-back (greet-on-answer, drain-aware auto-hangup), meeting minutes, and governance (audit-channel mirror on streamed replies, DLP redaction on the wire).

  • Real environment tested: Production Microsoft 365 tenant (PCFC, Dubai) with the Teams desktop client; OpenClaw gateway run from source at this branch's head on a Windows Server VM; the companion app-hosted media worker running as a Windows service; Azure OpenAI realtime voice model; ElevenLabs STT/TTS.

  • Exact steps or command run after this patch: pnpm openclaw gateway run on the branch head with msteams.dlp.enabled, auditChannel, meetingRecap, and bilingual configured → placed a 1:1 Teams voice call to the bot and exercised greeting, echo, interrupts ("stop" / "assistant, stop" / "توقف"), Arabic conversation, screen-share vision, "show me a screenshot", and "summarize the meeting" → said "look up the time in Tokyo and call me back", hung up, and answered the bot's call-back → ran the chat pass: DM questions including "reply with exactly: contact our support team at [email protected]", with an "Audit Log" group chat configured as the audit channel.

  • Evidence after fix: Copied (redacted) gateway runtime log from the live calls:

    06:18:54 MsteamsProvider: realtime session.start callId=24005480-… caller.aadId=9a78…
    06:18:54 MsteamsProvider: recording.status 24005480-… = active (realtime)
    06:26:22 MsteamsRealtime: posting meeting recap for 24005480-…
    06:26:32 MsteamsProvider: outbound call placed callId=2b005180-… -> 9a78…   (the caller)
    06:26:34 MsteamsProvider: outbound realtime session.start … attached to internal e4ad…
    06:26:38 MsteamsProvider: recording.status 2b005180-… = active (realtime)  ← greet-on-ANSWER trigger
    06:26:52 MsteamsProvider: notify-mode auto-hangup for 2b005180-… after delivery
    

    Worker runtime log excerpt: [display] showing image for 4500 ms (mode=overlay, caption=True) followed by 5000 ms — the show_to_caller slideshow pacing constants observed on the wire. Copied live output from the Teams client: the bot's streamed DM reply rendered as Noted — thanks, Alaaeldin. I've got the support contact ([REDACTED:email])., and the audit chat received 🧾 Reply for Eng. Alaaeldin Mohamed Elhenawy in a:180Gt…: … mirror lines — one per streamed DM reply.

  • Observed result after fix: Caller greeted by name; the bot never answered its own echo; all three interrupt forms cut playback instantly; vision answered correctly across two screen-share windows including "what did the FIRST screen say"; the captioned PiP slideshow displayed; the call-back spoke only after the callee answered, delivered the complete un-clipped result, and hung itself up; recap + minutes landed in the Teams chat; the email in a reply reached the client as [REDACTED:email]; every streamed DM reply was mirrored to the audit channel and the bot's own reply inside the audit chat produced no self-mirror.

  • What was not tested: Voice-message transcription live (this tenant exposes no audio voice-message capability on either Teams client — the desktop mic-only "video clip" delivers no media to bots, and the bot's graceful no-media reply WAS observed live); the DLP secret category on the wire (the model refused to repeat even a stated-dummy sk-proj-… token, twice — the email category proves the same redaction path); the group-chat form of the message action; DTMF (no dialpad on VoIP 1:1 calls); group/meeting calls with a second human participant.

Foundation recap (from #91438, included in this diff until it lands)

The msteams provider: inbound Teams voice + video vision (look_at_screen, streaming auto-attach, realtime continuous vision via RealtimeVoiceBridge.sendImage), group/meeting awareness (per-participant attribution + speak-only-when-addressed), outbound "call me back" with voicemail fallback, avatar rendering drivers (expression cues, viseme lip-shape, show_to_caller), caller allowlist (closed by default), recording-status gate on every media-derived path, HMAC-authenticated bridge WebSocket, SSRF-guarded outbound. Live-proven on 1:1 and 3-party group Teams calls.

Notes

  • Several features (DTMF tone capture, unmixed meeting audio, avatar rendering) have a companion in the application-hosted media worker; the protocol additions are backward-compatible (older workers ignore unknown fields).
  • The avatar art is a worker-side choice (not an OpenClaw config key): the companion ships two styles — an SVG face with full lip-sync/viseme shapes, expressions, and blink (AvatarImagePath=avatar, the default), and a Lottie loop with speech-reactive zoom (AvatarImagePath=jarvis) — plus explicit file paths and a procedural fallback. The expression / speech.marks / display.image messages this PR emits drive whichever style is configured.
  • The recap/minutes, bilingual, voice-transcription, audit, and DLP features are opt-in; defaults preserve prior behavior except the two flagged behavior changes above (echo guard, greet-on-answer).

Acknowledgements

Contributed by the team at the Ports, Customs and Free Zone Corporation (PCFC) — Dubai, United Arab Emirates — bringing Microsoft Teams voice & video (the Conversational Video Interface) to OpenClaw for public-sector use. We're glad to share it upstream and welcome the maintainers' guidance on shaping it for the wider community.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: msteams Channel integration: msteams channel: voice-call Channel integration: voice-call extensions: openai size: XL triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. labels Jun 11, 2026
@clawsweeper

clawsweeper Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper review: did not complete due to Codex infrastructure failure. Reviewed June 16, 2026, 1:18 AM ET / 05:18 UTC.

Summary
Review failed before ClawSweeper could summarize the requested change.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: extensions/elevenlabs/speech-provider.test.ts, serialized state: extensions/elevenlabs/tts.test.ts, serialized state: extensions/openai/realtime-voice-provider.test.ts, serialized state: extensions/voice-call/src/msteams-realtime.ts, serialized state: src/talk/session-runtime.ts, unknown-data-model-change: src/config/bundled-channel-config-metadata.generated.ts, and 1 more. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Not assessed.
Failure reason: invalid structured output.

This is a ClawSweeper/Codex infrastructure failure, not a PR readiness or patch-quality verdict.
Keep any merge decision on the normal maintainer review path until ClawSweeper can complete a fresh review.

Risk before merge

  • [P1] No close action taken because the review did not complete.

Maintainer options:

  1. Decide the mitigation before merge
    Retry the Codex review after fixing the execution failure.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • Review did not complete, so no work-lane recommendation was made.
Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

Do we have a high-confidence way to reproduce the issue?

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

AGENTS.md: unclear because the file could not be read completely.

Codex review notes: model internal, reasoning high; reviewed against 7ac2bbaaf0ae.

Label changes

Label changes:

  • remove P2: Current review triage priority is none.
  • remove rating: 🦐 gold shrimp: Current review failed before PR readiness was assessed, so no rating label should remain.
  • remove merge-risk: 🚨 compatibility: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 session-state: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 security-boundary: Current PR review selected no merge-risk labels.
  • remove status: 👀 ready for maintainer look: Current PR status no longer selects a status label.
Evidence reviewed

What I checked:

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)
How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@ahenawy ahenawy changed the title feat(voice-call): msteams CVI v2 — enterprise-ready Teams calls: echo fix, deterministic gating & interrupts, retroactive vision, per-thread sessions feat(voice-call): msteams — deterministic meeting etiquette & verbal interrupts, retroactive vision, captioned visual sharing, per-thread sessions Jun 11, 2026
@ahenawy ahenawy changed the title feat(voice-call): msteams — deterministic meeting etiquette & verbal interrupts, retroactive vision, captioned visual sharing, per-thread sessions feat(voice-call): msteams — enterprise & governance features + natural extensions of the CVI work (deterministic meeting etiquette, verbal interrupts, retroactive vision, per-thread sessions) Jun 11, 2026
@clawsweeper clawsweeper Bot added the rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. label Jun 11, 2026
@ahenawy ahenawy changed the title feat(voice-call): msteams — enterprise & governance features + natural extensions of the CVI work (deterministic meeting etiquette, verbal interrupts, retroactive vision, per-thread sessions) feat(voice-call): msteams — governance hardening + CVI extensions (echo fix, replay guard, retroactive vision) Jun 11, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 11, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 11, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. label Jun 11, 2026
ahenawy and others added 25 commits June 17, 2026 12:20
Opt-in msteams.transcribeVoiceMessages: when a chat message carries an audio
attachment (a Teams voice clip), each is transcribed via media-understanding
STT (transcribeAudioFile) and the text is folded into the message — replacing
the opaque '<media:document>' placeholder with a quoted '🎙️ Voice message: "…"'
block — so the agent reads what was said. Off by default (incurs an STT call);
transcription failures keep the placeholder and never block the reply.

voice-message.ts holds the pure detection + merge logic (5 tests). Outbound TTS
replies are a separate follow-up.
Opt-in channels.msteams.dlp: scrubs sensitive values out of text the bot SENDS
to Teams so the agent can't leak data into a chat. Built-in detectors —
credit cards (Luhn-validated, so random 16-digit numbers are left alone),
emails, US SSNs, IBANs, AWS keys, common provider secret prefixes (sk-/ghp_/
xox.-/AIza), and phone numbers — plus org-specific customPatterns and a
configurable {category} placeholder. categories[] narrows the built-ins.

Applied at both outbound chokepoints on the FULL text before chunking (so a
split value can't slip through): renderReplyPayloadsToMessages (agent replies)
and sendMessageMSTeams (proactive / cron sends). Off by default; a malformed
custom pattern is skipped rather than throwing on the send path.

dlp.ts is pure + deterministic; 7 tests. lint + types clean.
…aw#15)

Opt-in channels.msteams.auditChannel: a compact, DLP-redacted line of each agent
reply ('🧾 Reply for <name> in <conversation>: <excerpt>') is mirrored to a
configured Teams audit conversation, giving admins a governance trail without
reading session files. Fire-and-forget from the reply dispatcher so it never
blocks or fails the user-facing reply; the mirror goes through sendMessageMSTeams
(so DLP redaction applies and it can't recurse through the dispatcher), and a
loop guard skips replies that are themselves in the audit channel. Off by default.

audit.ts is pure (resolve target + format line); 5 tests. lint + types clean.
…he validation schema

The openclaw#13/openclaw#15/openclaw#16 features added these fields to the MSTeamsConfig TS type + wired
them, but the runtime VALIDATOR (zod MSTeamsConfigSchema, strict) didn't list
them — so the gateway would reject channels.msteams.{dlp,auditChannel,
transcribeVoiceMessages} as additional properties at config load. Add them to
the zod (mirrors the meetingRecap manifest fix on the voice-call side) and
refresh the generated config docs baseline.

Confirmed: MSTeamsConfigSchema.safeParse now accepts all three.
buildEvent hardcoded direction:"inbound", so outbound call.answered/initiated
events carried wrong metadata (OpenClaw couldn't tell a call it placed from one
that dialed in). Derive it: a call tracked in pendingOutbound /
outboundRealtimeInternalIds is "outbound", otherwise "inbound".
…, C2)

C1 — realtime mode never wired the CallManager: runtime.ts's realtime branch
called only setRealtimeRuntime, so this.manager stayed null → outbound call.answered
no-op'd → CallRecord never got answeredAt → the stale-call reaper force-ended
connected outbound realtime callbacks ~2 min in, and notify auto-hangup silently
no-op'd. Now the realtime branch wires setCallManager + setResponseRuntime too
(parity with the streaming wireMsteamsRuntime). (The provider unit test masked
this by calling setCallManager directly.)

C2 — the msteams safe-allowlist inbound default was dead code: the schema filled
inboundPolicy.default("disabled"), so resolve's 'inboundPolicy === undefined →
allowlist' branch never fired in production (schema parses before resolve). Result:
msteams+streaming rejected ALL inbound calls, msteams+realtime failed startup with
a misleading error. Make inboundPolicy .optional() so resolve sees 'unset' and
applies the provider default — msteams → allowlist, others → disabled. Verified:
resolveVoiceCallConfig({provider:'msteams'}).inboundPolicy === 'allowlist'.

Type-clean; config+provider suites (56) green.
…window (review S4)

A future-dated handshake (worker NTP skew δ) stays signature-valid until ts+window,
but its replay record was swept at now+window — leaving a δ-wide window where the
same verified tuple could be replayed. Key the record's expiry off the timestamp's
own validity end. Lint clean.
…/B2/B3/B5)

Four per-call teardown paths leaked state or stranded the call; one design fix
funnels every teardown through a single owner.

B1 — closeSession deleted the connection meta before the ws close event, so a
host-initiated close (realtime connect failure, policy reject, hangup) suppressed
onSessionEnd: the realtimeCalls entry leaked forever and getCallStatus reported a
dead call in-progress. closeSession now delivers onSessionEnd itself (started +
not-ended guard keeps session.end-driven closes single-delivery).

B2 — the realtime bridge's onClose (provider WS drop) set closed=true but closed
NOTHING; a later close(reason) early-returned, so the caller sat in silence on a
Teams call that could never be hung up, and the recap was skipped. onClose, a
failed connect(), and the returned close() now funnel through one closeCall()
that also closes the Teams worker session (and handles recap + vision timer).

B3 — manager-driven hangups never released vision frames / recordingActiveByCall
(~1-2 MB/call); cleanup lived only in handleSessionEnd, which B1 could suppress.
New provider-level releaseCallState(providerCallId) (vision, recording gate,
no-answer timer, timed-out guard, pendingOutbound) runs unconditionally at the
end of teardownCall, so every end path releases it.

B5 — session.end before session.start (declined/busy placement) cancelled the
no-answer timer but finalized nothing: the CallRecord stayed active forever
(counting against maxConcurrentCalls) and pendingOutbound leaked.
handleSessionEnd now resolves the internal id from streaming state, the outbound
realtime map, OR the pending entry, and finalizes the record.

Tests: host-close onSessionEnd single-delivery; bridge onClose + failed connect
hang up the Teams session; connect-failure releases realtime state (terminal
status); manager hangup releases vision; declined-before-start finalizes the
CallRecord. Suites green (91 msteams + full voice-call); tsgo prod+test clean;
oxfmt clean.
B4  — streaming outbound still fired call.answered at media-WS attach, TTS'ing
the notify result into a RINGING phone (the realtime path was already fixed via
greetingOnRecordingActive). Defer call.answered until recording goes active
(= the callee picked up); fire immediately when the session starts answered.

B6  — an operator-configured custom realtime tool was advertised to the model
but had no handler: the tool call got no submitToolResult and the model's turn
stalled forever. Unknown tool names now get an explicit 'not available' result.

B7  — transcript coalescing merged ALL consecutive same-role fragments: one
entry could grow unbounded (defeating the 40-entry cap) and a group call's
'Name:' attribution absorbed other speakers' words. Coalescing now never
crosses speakers and caps an entry at 1000 chars.

B8  — 'Ask OpenClaw about this' (and card actions) in group chats acked 'On it'
then never replied: the synthetic dispatch had no @mention, so mention-gating
dropped it after recording it into group history. The invoke wrapper now stamps
a bot-mention entity (the user explicitly invoked the bot).

B9  — a history-scope look_at_screen wrote the live-look cache, so later live
looks on a static screen returned the history answer. History runs no longer
cache.

B10 — post_meeting_minutes was the only agent-running tool that skipped the
recording gate (Media Access API); gate it like consult/look/show/task. The
async-task tool acked delivery then silently dropped when the task arg was
missing; it now refuses up front instead.

B11 — the session.start schema permits an empty-string aadId, which survived
'aadId ?? fallback' everywhere: all such callers shared ONE consult session key
(cross-caller memory bleed). Blank caller ids are normalized to null at the
media-stream boundary, and the realtime/provider fallbacks use || not ??.

B12 — the ambient vision push latched 'already pushed' and consumed budget
BEFORE sendImage, which can throw: the frame was lost (never retried) and the
burned budget starved look_at_screen. Latch only after a successful send and
refund the budget hit on failure (new VisionBudget.refund).

B13 — mixed voice-clip batches: a failed clip's placeholder was removed anyway
(the attachment vanished) and a combined '<media:document> (N files)' token
left a dangling '(N files)' residue. Only transcribed clips consume a
placeholder, and the combined counter is decremented properly.

Tests per fix (12 new). Affected suites green (115 targeted; full voice-call +
msteams runs show only the documented Windows EBUSY sqlite flake and a
pre-existing messenger.test.ts runtime-stub failure, both reproduced on the
clean tree). tsgo prod+test clean; oxfmt clean.
…ards & friends)

The B-series bugs lived where the structure was hard to audit; this pass
removes the duplication they hid in. No behavior change except where noted.

- realtime bridge: extract withConsultGuards — the recording-gate refusal,
  wired-deps check, error reporting, and 'working on it' filler shared by the
  agent-running tool handlers (consult / look_at_screen / show_to_caller /
  post_meeting_minutes) now live in ONE wrapper; review B10 existed because a
  handler restated (and partially missed) that boilerplate. onToolCall routes
  by name with the unknown-tool (B6) answer as the explicit fallback.
  Behavior note: post_meeting_minutes with unwired deps now answers 'I can't
  post minutes' instead of acking and silently doing nothing.

- realtime bridge: hoist consultAgentId/consultSessionKey — previously
  recomputed verbatim in five handlers.

- realtime bridge: move the declarative tool surface (4 tool definitions,
  3 consult system prompts, 6 canned spoken results) to a new
  msteams-realtime-tools.ts; msteams-realtime.ts now holds behavior only
  (1652 -> 1449 lines).

- provider: emitCallEnded() — one construction for the three hand-rolled
  call.ended events (the B5 leak hid in one of the copies); name the
  thrice-restated PendingOutboundCall tuple; drop an orphaned doc comment.

- runtime: skip the webhook-plane RealtimeCallHandler for msteams — Teams
  never arrives on the webhook plane, so the handler, the stream-session
  issuer only telephony providers read, and the duplicate realtime
  instructions build were dead weight.

- config: groupCall schema defaults come from GROUP_CALL_GATE_DEFAULTS
  instead of restating the literals; document the worker endpoint as
  /api/calls (the URL actually POSTed) not /api/calls/place.

- message-handler: extract applyMSTeamsVoiceTranscription from the ~760-line
  handleTeamsMessageNow (the voice-clip transcription block).

- small: drop dead SURPRISED_PUNCT alternations ('\?!|!\?' are subsets of
  '[?!]{2,}'); correct the stale speech.marks comment (current workers ignore
  viseme marks — RMS-driven lip-sync; the marks are a forward-compat hint).

Verified: 189 tests across the 12 affected suites green; full voice-call +
msteams runs match the pre-refactor baseline exactly (only the documented
Windows EBUSY flake + pre-existing messenger.test.ts failures); tsgo
prod+test clean; oxfmt clean.
…ic layers (review weak area)

The bilingual claim was only true for the model: the deterministic layers were
Latin-only. verbal-interrupt stripped all non-Latin letters ([^a-z]) — an
Arabic "توقف" could never trigger the instant cut — and the viseme map covered
only a-z, so Arabic replies fell back to RMS-only lip sync. group-call-gate
already did Unicode correctly (\p{L}); apply the same approach:

- verbal-interrupt: normalize with \p{L} (any script) and delete combining
  marks + tatweel in place (so tashkeel doesn't split words); add a
  conservative whole-utterance Arabic interrupt set (توقف / خلاص / اسكت /
  انتظر / لحظة / كفى …) and Arabic fillers (طيب / لا / يا) so "يا مساعد توقف"
  cuts like "hey ⟨name⟩, stop".
- viseme-estimate: map Arabic graphemes to the same Azure viseme classes
  (long vowels carry the shape; consonants by articulation class; tashkeel
  short vowels when present) in both the estimator and the alignment path.

Tests: Arabic interrupts (incl. tashkeel + wake-phrase stripping, and the
"stop by the store" negative); Arabic visemes in both paths. 108 tests green
across the 5 affected suites; tsgo prod+test clean; oxfmt clean.
…3/S5)

The governance features redacted and audited only the block-delivery text
path; every other outbound surface bypassed them. With dlp.enabled the
DEFAULT personal-chat path (native streaming) sent raw agent text.

S1 — native streaming bypassed DLP entirely. Per-token streaming cannot be
redacted retroactively: a secret that only completes across chunk boundaries
is already on the caller's screen, and the SDK stream is append-only. With
DLP enabled, 'partial' mode now downgrades to 'progress' (the live card shows
working status; no token ever reaches the wire) and the single full-text
final emit — the one place agent text reaches the native stream — is
redacted in the controller.

S2 — the audit mirror lived in the block-delivery path, so natively-streamed
replies (again: the DM default) left no governance trail. The mirror now
fires at the deliver() choke point, on the payload BEFORE the stream
controller decides which path carries it. Loop guard unchanged.

S3 — editMessageMSTeams and sendAdaptiveCardMSTeams bypassed redaction:
'send something innocuous, then EDIT the secret in' and 'put the secret in a
card' were agent-reachable loopholes. Edits redact like sends; cards get a
new redactOutboundMSTeamsCard that deep-redacts every string value.

S5 — the secret detector's sk- class now includes - and _ so segmented keys
(sk-proj-…, sk-ant-api03-…) match; buildMSTeamsAuditLine redacts BEFORE
truncating, so the excerpt cap can no longer slice a secret mid-match into
an unredactable fragment.

Tests: streamed-reply audit mirror + loop guard at the choke point; DLP
stream downgrade + redacted final emit; card deep-redaction (no mutation,
no-op when off); sk-proj-/sk-ant- patterns; redact-before-truncate. msteams
suites green (1005 passed; only the documented EBUSY flake + pre-existing
messenger failures); tsgo prod+test clean; oxfmt clean.
…e keys

a298657 added dlp / auditChannel / transcribeVoiceMessages to the msteams
zod schema but the checked-in bundled-channel-config-metadata.generated.ts was
never regenerated — the gateway's raw-channel AJV validation uses THAT
artifact, so a config using the new keys failed startup with 'must not have
additional properties'.

Root cause for the missed regen: scripts/generate-bundled-channel-config-
metadata.ts guards its main with import.meta.url === new URL(process.argv[1],
'file://'), which never matches on Windows (new URL('C:\...', 'file://')
parses the drive letter as a URL scheme) — so 'pnpm config:channels:gen' exits
0 having written nothing. Regenerated by invoking
writeBundledChannelConfigMetadataModule directly; the Windows guard bug is
upstream-wide (same pattern in other generators) and left as-is here.

Verified: built CLI now loads a config with dlp/transcribeVoiceMessages
(previously: startup failure).
…ers consume the marks

The refactor pass over-corrected this comment to claim current workers ignore
speech.marks; the companion worker's AvatarViseme layer consumes them (coarse
open/wide/round/closed shapes blended over RMS openness). Restore the accurate
framing: capable workers blend, older workers fall back to RMS-only.
…alignment

The msteams speech.marks timeline was an even spread of the reply text across
the audio duration (CVI Phase 5 spike). Now the elevenlabs provider's telephony
synthesis calls /v1/text-to-speech/{voiceId}/with-timestamps and surfaces the
per-character alignment (normalized_alignment preferred; falls back to plain
synthesis if the endpoint fails, so audio never depends on timing), an optional
'alignment' field flows through SpeechTelephonySynthesisResult ->
textToSpeechTelephony -> the voice-call TTS adapter, and msteams playback emits
visemesFromAlignment marks - real per-sound mouth timing - estimating only when
no alignment arrived. Alignment is wall-clock seconds, so the 22050->16k
resample leaves it valid.
ClawSweeper Codex flagged a DLP bypass: sendPollMSTeams sent the poll
Adaptive Card unredacted while the S3 fix only covered the generic
sendAdaptiveCardMSTeams path. A secret in a poll question or option label
('put the secret in a poll') therefore reached the wire. Redact the
agent-supplied question and option strings before building the card — the
visible text only, leaving the internal option indices and pollId untouched so
vote matching is unaffected. Two send.test cases cover the redaction and the
DLP-off passthrough.

Co-Authored-By: Claude Fable 5 <[email protected]>
mock.calls[0] types as an empty tuple, so indexing [2] and the card cast
tripped check-test-types (TS2493/TS2352). Cast through unknown first.

Co-Authored-By: Claude Fable 5 <[email protected]>
…stream rebase

The rebase onto current upstream/main carried the governance schema keys
(dlp/auditChannel/transcribeVoiceMessages) forward; regenerate the two derived
artifacts against the merged schema so the generated-files-up-to-date checks
pass. (config:channels:gen no-ops on Windows — main-guard URL parse bug — so the
metadata module was regenerated via a direct writeBundledChannelConfigMetadataModule call.)

Co-Authored-By: Claude Fable 5 <[email protected]>
…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]>
… (msteams realtime)

Reported: caller stays silent after the greeting, bot keeps re-greeting in
different ways. Cause: the bot's own greeting echoes back on a speakerphone loud
enough to clear the echo guard's barge-in RMS, so it reaches the realtime
server-VAD, which (with interruptResponseOnInputAudio + autoRespondToAudio) makes
the model interrupt itself and re-greet — an echo loop while the caller is silent.

Fix: until the caller's first real turn, the echo guard allows NO barge-in —
every in-playback-window frame is treated as echo (allowBargeIn:false), so the
opening greeting can't trigger the model. Normal RMS barge-in resumes once a real
caller transcript arrives. Streaming path unaffected (allowBargeIn defaults true).

Co-Authored-By: Claude Fable 5 <[email protected]>
…ss API)

ClawSweeper Codex flagged that DTMF keypad input reached the realtime provider
before the call's recording status was active, bypassing the recording gate that
governs audio, video, transcripts, consults, and background tasks. DTMF tones are
in-band, media-derived caller input, so notifyDtmf now returns early when
recordingGateBlocks() — no forward, no transcript — until setRecordingActive(true).
No-op when requireRecordingStatus is off. Regression test: DTMF ignored while
recording inactive, accepted after recording active.

Co-Authored-By: Claude Fable 5 <[email protected]>
…ate baseline

- Bump plugin-sdk public export budget to upstream 10284/5163 + RealtimeVoiceImageInput (+1) = 10285/5164.
- Regenerate docs/.generated/config-baseline.sha256 after upstream config-surface drift.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@ahenawy

ahenawy commented Jun 17, 2026

Copy link
Copy Markdown
Author

Re-synced onto current upstream/main (the branch had drifted well behind). Rebased clean → 0 behind, all conflicts resolved:

  • generated artifacts regenerated (docs/.generated/config-baseline.sha256, bundled channel-config metadata);
  • plugin-sdk public export budget reconciled to upstream's 10284/5163 + RealtimeVoiceImageInput (+1) = 10285/5164;
  • one real source conflict in extensions/msteams/src/monitor-handler.ts — upstream's adaptive-card submit/imBack value helpers vs. this branch's invoke-dispatch bot-mention helper — merged keeping both. Adaptive-card and msteams-realtime suites pass locally.

The commit→feature map and range header in the description are updated to the new SHAs.

One note for the review queue: this PR is stacked on #91438 (the foundation), so its diff still carries #91438's commits until that lands. Taking #91438 first would collapse this to just the ~55-commit increment and remove the recurring rebase churn — I'll rebase to the pure increment the moment #91438 merges.

@ahenawy

ahenawy commented Jun 19, 2026

Copy link
Copy Markdown
Author

Consolidating the Microsoft Teams CVI voice/video work into a single refreshed PR (rebased onto current upstream); the foundation PR #91438 stays open. Closing as superseded - its content/summary is carried into the consolidated PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams channel: voice-call Channel integration: voice-call docs Improvements or additions to documentation extensions: elevenlabs extensions: openai feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants