docs/design/web-shell-live-voice-codex-parity-refactor.md
This document records the architecture direction accepted on 2026-07-30 for the WebShell Live Voice refactor. On 2026-07-30 the user authorized completing the implementation and its real provider, Host, and UI validation without user-assisted testing. The bounded protocol qualification experiment completed on 2026-07-30 is recorded below.
The reference is Codex origin/main at
c126f206dafb9b7fe8e1d6b990e67f0535de79f4, corroborated by a real Codex
Desktop Live session. The Qwen implementation must reproduce that architecture
and behavior. A Qwen or WebShell incompatibility is a stop condition: document
the exact incompatibility and obtain explicit approval before implementing any
substitution, approximation, fallback, or optimization.
The reference anchors were refreshed against Codex origin/main at
f0c30e528a54bdf0fa9a4d52ff74b34383434811 on 2026-08-01. The relevant
Realtime prompt, session, Appshot, task-context, and permission behavior is
unchanged.
On 2026-08-04 the contract was updated with the later verified Codex
speak_to_user lifecycle and the explicitly approved Qwen transport mapping.
This update supersedes the earlier requirement to wait for handoff completion
before requesting one final Realtime response. It does not relax the exact
parity rule: the Qwen mapping may replace only a provider primitive that the
public DashScope protocol does not expose, and only with the approved behavior
recorded below.
This document supersedes the earlier design for these areas:
WebShell Live is a normal projectless Qwen Code session with a Realtime-model-driven, full-duplex voice conversation attached to it. The Realtime model is the conversational frontend and the Qwen Code model is the execution backend. The Realtime model is not an ASR/TTS wrapper around a text model, and the backend is not a separate restricted Coordinator persona.
flowchart LR
U["User audio"] <--> R["Qwen Omni Realtime
conversation owner"]
R -->|"direct answer for self-contained conversation"| U
R -->|"handoff only when backend execution is useful"| S["Same projectless Live Qwen Code session"]
S --> T["Normal Qwen Code tools"]
S --> V["Live and task tools"]
S --> P["Normal permission pipeline"]
S -->|"incremental agent output"| R
The following rules are mandatory:
capture_screen_context tool and returns the result through the normal
backend-to-Realtime path.speak_to_user(message) tool; backend completion alone never authorizes a
Realtime response.Codex attaches a Realtime conversation to an ordinary persistent Codex task. GPT-Live owns turn detection, direct conversational answers, output audio, and the decision to request backend execution. A backend prompt explicitly tells GPT-Live to respond directly when the request is self-contained and to request a handoff when the Codex backend is useful.
The provider emits a dedicated handoff request. Codex converts its text into a normal user turn on the same task. The handoff does not close the Realtime conversation or transfer permanent ownership to another session.
Codex observes backend agent-message lifecycle events. Agent message deltas are
buffered per item and, on the current dedicated V3 protocol, flushed to GPT-Live
at an approximately 200 ms cadence by handoff_append. Completion is signaled
separately. Speakable content and progress/commentary are distinct channels.
Codex also contains a distinct Realtime V2 compatibility path. V2 keeps the
original handoff function call open, sends completed backend Agent messages as
user conversation items, then closes the original function call and requests
the Realtime response only when the backend turn completes. V2 conversation
items are message-level updates; they are not the V3 200 ms delta stream.
This bidirectional stream is essential behavior. It lets GPT-Live retain the live conversation, react to progress, accept steering, and begin producing a natural response without waiting for the complete backend turn.
The current Codex backend receives a dynamic speak_to_user tool with one
required message argument. Ordinary backend text is mirrored into GPT-Live as
silent context. It is not automatically spoken, including at turn completion.
When the backend decides that the user should hear a progress update, question,
or final result, speak_to_user routes through
thread/realtime/appendSpeech. Codex appends the message to the same Realtime
session as StandaloneSpeech on the speakable channel, and GPT-Live produces
the audio in the existing conversation and voice.
The tool has no priority, urgent, or interrupt argument. Codex does not
implement an application-level ordinary-versus-urgent queue. Provider protocol
ordering and user barge-in remain distinct from the backend's decision to
speak.
Delegation acknowledgement is also distinct from backend speech. Codex exposes
an optional delegationAckFiller provider setting, while the verified installed
client did not hard-code a prompt rule requiring one acknowledgement sentence.
Qwen's current pre-handoff acknowledgement must therefore remain an isolated
compatibility item until its provider event timing is measured; it must not be
added to, removed from, or justified by the silent-context implementation.
GPT-Live does not receive Appshot as a provider tool. A deictic or screen-aware
request causes a backend handoff. The same ordinary Codex Live task invokes the
Voice/App-specific capture_screen_context tool alongside its normal tools.
The captured context is consumed inside that backend turn, whose Agent output
then returns to GPT-Live through the active handoff.
Codex provides both passive context and active task operations:
handoff_thread),
fork, title, archive, and navigation are outside this feature scope.Reading or following an existing task must never be implemented as creating a replacement task.
There are two internal layers behind one user-visible Live session:
The backend task follows the same sandbox, approval mode, and permission handling as an ordinary task. Approval requests carry task, turn, item, and request identity to the app UI and are resolved asynchronously. The Realtime bridge mirrors agent messages, not raw approval or tool-protocol events.
The target reproduces the same ownership boundaries with existing Qwen Code infrastructure:
| Codex responsibility | Required Qwen mapping |
|---|---|
| Persistent ordinary task | One normal projectless Qwen Code session surfaced in the dedicated Live group |
| GPT-Live conversational frontend | qwen3.5-omni-plus-realtime owning ordinary dialogue, VAD, barge-in, and spoken output |
| Selective backend handoff | One narrow Realtime handoff operation routed into a normal turn on that same Live session |
| Normal backend tool surface | The existing Qwen Code tools, configured MCP tools, sandbox, and approval mode remain available |
| Voice/App tools | Built-in Appshot and call-control capabilities attached to the backend Live session, never exposed as Realtime-provider tools |
| Task operations | List/read/wait/send/create operations backed by existing WebShell session and bridge services |
| Incremental backend return | Agent-message deltas aggregated at about 200 ms and appended as silent ordered conversation context |
| Explicit backend speech | A backend-only speak_to_user(message) tool flushes silent context and requests one Realtime spoken response |
| Steering | New speech during backend work uses the existing same-session mid-turn/steering path |
| Permissions | Existing session permission events and WebShell approval UI; no child-session escape hatch |
The Live overlay does not duplicate approval controls. While its coordinator has an unresolved tool permission, it shows one shortcut that opens the exact projectless Live session in WebShell; approval and denial remain owned by the existing WebShell permission UI.
WebSocket transport is retained. WebRTC is not required for the Codex conversation/session architecture and must not be used as an explanation for missing handoff, task, tool, permission, or interruption behavior.
~/Documents/Qwen Code/Conversations/ storage root, with one
direct child directory per projectless Live or projectless created task;delegate_to_coordinator;create_sub_session in Live;turn_complete and returning it as one final
function result for speech synthesis;handoff_thread) as Realtime voice
handoff;accepted and later presenting only a
separate final-result announcement as though it were the same handoff;Qwen does not expose Codex V3's handoff_append or a public speakable
channel. The user explicitly approved the following bounded mapping after the
real-provider ordering probe: aggregate backend deltas at approximately 200 ms,
append them as silent conversation.item.create text items without
response.create, and request a text-and-audio response only for
speak_to_user. The explicit modalities are required because a response to
text-injected backend speech may otherwise complete without audio. This is a
Qwen provider transport adaptation for the same observable lifecycle; it is not
authorization for sentence-by-sentence speech, local TTS, immediate handoff
completion, or multiple overlapping responses.
Live Voice is an experimental macOS WebShell capability. It must not be
advertised by the CLI/TUI, SDK-only daemons, non-macOS daemons, or a WebShell
started without its native Host integration. Platform support is decided by
the daemon from process.platform, never from browser user-agent data.
The complete first-use path is:
qwen3.5-omni-plus-realtime and optionally change the global shortcut./Applications, and launches it.The feature defaults to disabled. Enabling is resumable: closing either UI or restarting the daemon may preserve completed setup steps, but cannot expose a partially ready Live path. Disabling stops an active call, withdraws native discovery, and removes the Live capability without uninstalling the Host or deleting conversations.
The dedicated Realtime API key is user-scoped. WebShell receives only whether a key is configured; it never receives the persisted value. API responses, logs, telemetry, errors, renderer state, and Host messages must not contain the key. Replacing or clearing it is explicit. Provider validation may make one bounded Realtime connection for the user's enable action; status polling and Host readiness must never open provider connections or retry billable traffic.
The settings UI reuses WebShell primitives and adds one compact Live Voice card to Experimental settings. The card owns only enablement, masked key replacement, shortcut capture, install progress, and permission/readiness status. Native permission actions remain in the Host, and ordinary WebShell dictation remains visible and unchanged.
Release publishing must include notarized arm64 and x64 Host ZIP/DMG assets and a machine-readable manifest containing the protocol version, application version, architecture, fixed asset name, size, and SHA-256 checksum. The installer never accepts a caller-provided URL, path, executable, or shell command and never falls back to an unsigned/development build.
Enabling or disabling Live is hot-applied. A user must not restart the daemon: the same process publishes or removes Host discovery, creates the projectless Live runtime lazily, refreshes capabilities, and keeps all conversation ownership and handoff rules in this contract unchanged.
Public Qwen Realtime behavior already establishes ordinary full-duplex
conversation and function-call output. The open-source qwen-audio-agent at
ab203c2567334255c69606d88f334edac770ad5a additionally demonstrates a
selective spawn_thinking call, an immediate accepted function output,
continued Realtime conversation while ACP work runs, and later final-result
injection through a new conversation item. This proves useful Qwen protocol
primitives, but its fixed cross-conversation backend session, private Work
ledger, immediately completed handoff, and final-only announcement lifecycle
do not reproduce Codex.
The bounded real-provider experiment verified:
user conversation items are acknowledged without starting a response;The approved probe used qwen3.5-omni-plus-realtime over the configured DashScope WebSocket endpoint in text-only mode. It changed no product code, did not record credentials, and did not exercise microphone input, output audio, VAD, or audio barge-in.
All required protocol cases passed in the final run:
Two compatibility details are mandatory for implementation:
The result confirms that the public Qwen protocol accepts the primitives needed
for the approved silent-context mapping, but one protocol gate remains open:
the earlier probe requested a response only after closing the original handoff.
Before product code permits speak_to_user while backend work is still active,
a bounded provider probe must verify whether response.create is accepted while
the original background_agent function call remains unresolved. If it is not,
implementation must stop and report the incompatibility instead of closing the
handoff early or inventing an asynchronous replacement lifecycle.
The public Qwen protocol also has no confirmed exact equivalent of Codex's
speakable channel. A second bounded probe must establish whether a silent
context item followed by an explicit speech instruction produces a prompt,
complete, stable-voice response that preserves the requested message. Prompt
compliance is not assumed from schema acceptance.
The bounded text-input/audio-output probe kept one background_agent function
call unresolved, appended silent context, and requested two sequential spoken
responses before submitting the function output. Both requests succeeded on
qwen3.5-omni-plus-realtime over the configured DashScope WebSocket:
ALPHA and BETA
messages and returned non-empty, frame-aligned PCM audio;This qualifies mid-handoff response creation and exact short-message prompt compliance for product implementation. Voice stability across longer real microphone scenarios remains an E2E acceptance item rather than a schema assumption.
The steering experiment qualifies the provider protocol only. Routing the second handoff into the same existing backend turn, preventing duplicate backend work, and preserving task identity remain Qwen Code implementation and E2E acceptance requirements.
Each phase is gated by the exact-Codex-parity rule and must not begin merely because the preceding phase produced passing tests.
speak_to_user(message) in
the existing Live session tool registry and route it through the authenticated
ACP bridge to the owning Live coordinator. It is an internal communication
tool and requires no user approval.agent_message_chunk by item/turn, flush
ordered non-empty context at approximately 200 ms, and force a flush before
tool calls, speak_to_user, errors, and turn completion. Silent context must
never call response.create.speak_to_user flushes pending context, appends the
explicit speech request, and asks the existing response arbiter for one
response.remain_silent and
retain it only if provider compatibility evidence requires it.Any source change that would introduce behavior not confirmed in Codex or not confirmed compatible with Qwen Code must stop before editing and request approval.
The refactor is not complete until real scenarios demonstrate all of the following:
| Scenario | Required evidence |
|---|---|
| Ordinary conversation | Natural Realtime answer with zero Qwen Code backend turns |
| Screen question | Exactly one handoff followed by one backend-session internal Appshot, streamed result, and natural spoken answer; Realtime itself has no Appshot tool |
| List or inspect tasks | Existing tasks are listed/read with no new session |
| Follow an existing task | Message reaches the selected existing session and retains its identity |
| Create a task | Exactly one requested project or projectless session is created and linked |
| Normal tool use | Live backend can use the same normal tool surface as an ordinary session |
| Permission request | Correct WebShell approval appears, the Live overlay links to that exact session, and approve and deny both resolve while Live remains usable |
| Long backend work | Ordered agent deltas reach Realtime during the turn rather than after turn_complete |
| Backend progress speech | Only an explicit speak_to_user produces audio; silent progress and final text do not create responses |
| Simple talk during work | Realtime answers directly while the original backend turn and silent context stream continue |
| Second handoff | A new execution request steers the active backend turn or starts the next turn in the same Live session; it never duplicates the task |
| Barge-in during work | Existing audio stops, stale queued speech is discarded, backend work continues, and any execution instruction steers without overlap or duplication |
| Speech fidelity | Requested speech is complete, begins before backend turn completion when invoked, keeps the configured voice, and does not resume after interruption |
| Continued conversation | Realtime remains context-aware after backend work and can again answer directly |
| Protocol isolation | Raw approval, MCP, and tool events are not spoken as conversation text |
Unit, integration, Host lifecycle, and microphone-to-spoken-response E2E are reported as separate validation layers. Passing tests cannot substitute for architecture or runtime evidence.
codex-rs/prompts/templates/realtime/backend_prompt.mdcodex-rs/core/src/realtime_conversation.rscodex-rs/core/src/session/mod.rscodex-rs/core/src/realtime_context.rscodex-rs/app-server/src/bespoke_event_handling.rscodex-rs/core/src/session/turn.rspackages/cli/src/serve/server/session-list.tspackages/cli/src/serve/routes/session.tsQwenAudio/qwen-audio-agent at
ab203c2567334255c69606d88f334edac770ad5a, especially
config/frontend-agent/PROMPT.md,
server/src/voice/realtime-provider.mjs, and
server/src/voice/tools/tool-call-handler.mjsThese anchors must be refreshed against the current Codex source before the corresponding implementation phase. Stale source or prior conclusions do not authorize a change.