The 76k-character migration guide mixed explanation, compatibility policy, an ordered how-to, import mappings, a maintainer procedure, and a Talk API reference on one page. It is now a short index plus six pages, one per reader job. Children (docs/plugins/sdk-migration/): - how-to-migrate.md - the ordered <Steps> sequence, kept whole and in source order so a reader mid-migration can finish on one page. - import-paths.md - Import path reference and the retained channel facade mappings. - removed-surfaces.md - Removed compatibility surfaces plus the Migration reference accordions (both answer "what replaces this API?"). - talk.md - Talk and realtime voice migration. - compatibility-policy.md - the compatibility policy and the nine dated per-surface records, including Verifying readers and the boundary-report flag reference. - removal-timeline.md - dates and gates. The index keeps the frontmatter, the intro, "What changed"/"Why", and "Related", and gains a "Where each topic lives" map. The index map is ordered by the reader's journey (how-to first), which is why the first migration task is no longer 1878 words down the page. Section order inside each child is unchanged from the source. Anchor strategy: every one of the 53 ids the single-page version published still resolves on the index. The index publishes what-changed, why and related itself; the other 50 are authored <a id="..." /> stubs, each linking to the section on its child page. Ids were enumerated with parseDocsDocument on the pre-split file and re-verified against the post-split index and children, including the encoded Step id migrate-runtime-config-load%2Fwrite-helpers and the 19 generated Accordion ids. parseDocsDocument reports 0 collisions on all seven pages, and no stub duplicates an id the index publishes. The 28 Step and Accordion topics that had no Markdown heading are now reachable from the index map. Losslessness (single page -> index + 6 children, frontmatter excluded): words 7354 -> 7938 (+584, all index map and child frontmatter/lead-ins); fences 16 -> 16, compared fence by fence on info string and body, 0 mismatches; table rows 134 -> 134; markdown links 13 -> 75 (+62 index map entries). A line-level diff of the body shows 0 lost content lines and exactly 3 changed lines, all of them cross-references to content that moved off the page: - index "Migrate embedded tool-result extensions to middleware" now points at /plugins/sdk-migration/how-to-migrate#how-to-migrate - removed-surfaces.md "Covered in How to migrate" now points at the how-to page instead of a same-page anchor that no longer exists - how-to-migrate.md "Retained channel facade mappings" now points at /plugins/sdk-migration/import-paths Inbound links from hooks.md, sdk-subpaths.md and the release notes are untouched and still resolve through the index stubs. Closes audit findings: r3-0527, r3-0528, r3-0530
9.2 KiB
| summary | read_when | title | sidebarTitle | ||
|---|---|---|---|---|---|
| Migrating realtime voice, telephony, and meeting code to the unified Talk session API |
|
Talk and realtime voice migration | Talk and voice |
The unified Talk session controller, its supported session combinations, and the method map from the removed Talk families. Part of the Plugin SDK migration guide.
Talk and realtime voice migration
Realtime voice, telephony, meeting, and browser Talk code shares one Talk
session controller exported by openclaw/plugin-sdk/realtime-voice. The
controller owns the common Talk event envelope, active turn state, capture
state, output-audio state, recent event history, and stale-turn rejection.
Provider plugins own vendor-specific realtime sessions. Browser-meeting plugins
use openclaw/plugin-sdk/meeting-runtime for session, browser, audio, node-host,
agent-consult, and voice-call mechanics, then implement MeetingPlatformAdapter
for URL rules, DOM scripts, manual-action mapping, captions, creation, and dial-in
plans. Platform REST APIs, OAuth, artifacts, selectors, and wire names remain in
the plugin. Browser permission plans receive the requested meeting URL so each
platform can grant only its exact supported origins. Session runtimes must also
normalize platform-specific live health after confirmed browser departure;
historical transcript fields may remain, but caption and audio readiness must
not stay active after leave.
All bundled surfaces run on the shared controller: browser relay,
managed-room handoff, voice-call realtime, voice-call streaming STT, Google
Meet realtime, and native push-to-talk. Gateway advertises one live Talk event
channel in hello-ok.features.events: talk.event.
New code should not call createTalkEventSequencer(...) directly unless
implementing a low-level adapter or test fixture. Use the shared controller so
turn-scoped events cannot be emitted without a turn id, stale turnEnd /
turnCancel calls cannot clear a newer active turn, and output-audio
lifecycle events stay consistent across telephony, meetings, browser relay,
managed-room handoff, and native Talk clients.
The public API shape:
// Gateway-owned Talk session API.
await gateway.request("talk.session.create", {
mode: "realtime",
transport: "gateway-relay",
brain: "agent-consult",
sessionKey: "main",
});
await gateway.request("talk.session.appendAudio", { sessionId, audioBase64 });
// Capture this before stopping playback from the active output `talk.event`.
const turnId = activeOutputTalkEvent.talkEvent.turnId;
await gateway.request("talk.session.cancelOutput", { sessionId, turnId, reason: "barge-in" });
await gateway.request("talk.session.submitToolResult", {
sessionId,
callId,
result: { status: "working" },
options: { willContinue: true },
});
await gateway.request("talk.session.submitToolResult", {
sessionId,
callId,
result: { status: "already_delivered" },
options: { suppressResponse: true },
});
await gateway.request("talk.session.submitToolResult", { sessionId, callId, result });
await gateway.request("talk.session.close", { sessionId });
// Client-owned provider session API.
await gateway.request("talk.client.create", {
mode: "realtime",
transport: "webrtc",
brain: "agent-consult",
sessionKey: "main",
});
await gateway.request("talk.client.toolCall", { sessionKey, callId, name, args });
await gateway.request("talk.client.steer", { sessionKey, text, mode: "steer" });
Browser-owned WebRTC/provider-websocket sessions use talk.client.create,
because the browser owns provider negotiation and media transport while the
Gateway owns credentials, instructions, and tool policy. talk.session.* is
the common Gateway-managed surface for gateway-relay realtime, gateway-relay
transcription, and managed-room native STT/TTS sessions.
Legacy configs that place realtime selectors beside talk.provider /
talk.providers should be repaired with openclaw doctor --fix; runtime Talk
does not reinterpret speech/TTS provider config as realtime provider config.
The supported talk.session.create combinations are intentionally small:
| Mode | Transport | Brain | Owner | Notes |
|---|---|---|---|---|
realtime |
gateway-relay |
agent-consult |
Gateway | Full-duplex provider audio bridged through the Gateway; tool calls route through the agent-consult tool. |
transcription |
gateway-relay |
none |
Gateway | Streaming STT only; callers send input audio and receive transcript events. |
stt-tts |
managed-room |
agent-consult |
Native/client room | Push-to-talk and walkie-talkie style rooms where the client owns capture/playback and the Gateway owns turn state. |
stt-tts |
managed-room |
direct-tools |
Native/client room | Admin-only room mode for trusted first-party surfaces that execute Gateway tool actions directly. |
Method map for readers migrating from the older talk.realtime.* /
talk.transcription.* / talk.handoff.* families (all removed):
| Old | New |
|---|---|
talk.realtime.session |
talk.client.create |
talk.realtime.toolCall |
talk.client.toolCall |
talk.realtime.relayAudio |
talk.session.appendAudio |
talk.realtime.relayCancel |
talk.session.cancelOutput |
talk.realtime.relayToolResult |
talk.session.submitToolResult |
talk.realtime.relayStop |
talk.session.close |
talk.transcription.session |
talk.session.create({ mode: "transcription" }) |
talk.transcription.relayAudio |
talk.session.appendAudio |
talk.transcription.relayCancel |
talk.session.close |
talk.transcription.relayStop |
talk.session.close |
talk.handoff.create |
talk.session.create({ transport: "managed-room" }) |
talk.handoff.revoke |
talk.session.close |
The unified control vocabulary is also deliberately narrow:
| Method | Applies to | Contract |
|---|---|---|
talk.session.appendAudio |
realtime/gateway-relay, transcription/gateway-relay |
Append a base64 PCM audio chunk to the provider session owned by the same Gateway connection. |
talk.session.cancelOutput |
realtime/gateway-relay |
Stop assistant audio output without necessarily ending the user turn. |
talk.session.submitToolResult |
realtime/gateway-relay |
Complete a provider tool call after any asynchronous completion exposed by its bridge; pass options.willContinue for interim output or, when supported, options.suppressResponse to avoid another assistant response. |
talk.session.steer |
agent-backed Talk sessions | Send spoken status, steer, cancel, or followup control to the active embedded run resolved from the Talk session. |
talk.session.close |
all unified sessions | Stop relay sessions or revoke managed-room state, then forget the unified session id. |
Do not introduce provider or platform special cases in core to make this work. Core owns Talk session semantics. Provider plugins own vendor session setup. Voice-call and Google Meet own telephony/meeting adapters. Browser and native apps own device capture/playback UX.