mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
main
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f649d65d1f |
feat(live): add visual input, proactive assistance and memory (#11369)
* feat(live): add visual input, proactive memory and desktop controls * fix(live): preserve upstream checks and finalize migration validation * feat(live): open active configuration from host settings * fix(live): address lifecycle and proactive review findings * feat(live): add subagent controls and diagnostics with review fixes --------- Co-authored-by: 菲兹 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
7bd939ec9c |
feat(qwen-live): ACP backend adaptor and multi-backend routing — M4 (#10617)
* fix(qwen-live): harden review findings across dispatcher, injector, and permission relay Self-review fixes bundled after the adversarial pass: - adaptor: classify snake_case permission option ids (proceed_once) — the word-boundary regex never matched through "_", so an allow vote silently degraded to a cancelled outcome and serve dropped the turn - adaptor: detect the pending-prompt limit by error name as well as status, and fall image-bearing steers back to a regular prompt - dispatcher: a timed-out tool now answers with a pending receipt that tells the model not to retry (the handler keeps running) - injector: permission items flush ahead of the size-capped context join; spoken lines merge whole-line greedily under the 280-char cap - live-session: resubscribe the event pump with backoff until session_closed; a rejected permission chain can no longer become an unhandled rejection; receipts are clamped and never empty - permission broker: a failed auto-approval falls back to the spoken ask instead of losing the request - daemon/config/index: appshot readiness is declared at construction, numeric config ports parse, stray rejections are logged not fatal - e2e harness: QWEN_LIVE_E2E_KEEP=1 keeps the temp dirs for post-mortem * refactor(qwen-live): extract adaptor helpers and scope cross-backend identifiers Prep for multiple coexisting backends (M4). Behavior-neutral with a single backend — the full package suite passes unchanged. - adaptor-utils.ts: the wire-shape helpers shared by every BackendAdaptor (permission-option classification, least-escalating vote, tool-call titles, clamped summaries) move out of qwen-code-adaptor.ts verbatim - handles.ts: jobsByRef keys scope by owning adaptor — jobRefs are adaptor-local (serve mints prompt UUIDs, an ACP adaptor counts turns) and a bare key would let one backend's createJob return another's job - permission-broker.ts: pendingByRequestId keys scope the same way, and the broker resolves its voting adaptor through adaptorFor(handle) instead of holding a single instance - live-session.ts: adaptorFor(handle) is the one routing seam (single adaptor today; becomes registry routing in M4); jobFor/onResolved thread the backend handle through Collision pins added for both maps: identical jobRefs and requestIds across two backends resolve to their own records, never each other's. * feat(qwen-live): multi-backend config, registry, and routing Backends become a set: LiveSession now holds a BackendRegistry instead of a single adaptor, and every tool call routes to the adaptor that owns the target backend handle. - config: the `serve` section is replaced by a `backends` array (qwen-code entries keep the REST shape; `acp` entries name a command to spawn). Sources resolve whole — QWEN_LIVE_BACKENDS env (JSON) > config.json `backends` > legacy synthesis from the old serveUrl / serveToken spellings, so existing configs and the e2e harness keep working unchanged. Names are validated (unique, no ':') because they scope jobRefs and permission requestIds. - registry: default backend preflight still aborts startup before the discovery claim; secondary backends degrade to unavailable with the error stored. closeAll() lands in daemon.stop() — which also fixes the pre-existing leak where the serve adaptor's close() was never called. - routing: session_list fans out across backends (a dead one contributes zero rows, never an empty list) and each row names its backend; session_create takes an optional `backend` arg; handoff, monitor, stop, the permission-note relay, and the event pump all route by handle. - capability gating goes live (capabilities() had no consumers): steering none suppresses steer, image-incapable backends get image blocks stripped with a receipt note, and non-streaming backends skip the resubscribing pump. * feat(qwen-live): AcpAdaptor — drive any ACP agent as a backend One spawned ACP child (JSON-RPC over stdio via @agentclientprotocol/sdk) multiplexes every session the adaptor creates; qwen-code's `qwen --acp` mode is the primary target but nothing here is qwen-specific beyond the named ext methods. - send discipline: every prompt funnels through one sendPromptNow() — a second in-flight prompt would silently preempt the running turn. Steers queue locally and are handed over when the agent calls craft/drainMidTurnQueue (the handler answers synchronously so the agent's 2s latch can never trip); queued prompts and undrained steers continue at turn end under their pre-minted jobRefs, so a joinedActiveTurn receipt is never lost even if the turn ends before the next drain. Steering reports 'queued' until the agent has actually drained once. - crash policy: no supervisor — the exit cascade resolves parked permissions cancelled, errors the busy turn, closes every session, and the next createSession respawns under a new generation; stale handles are rejected. The handshake races the 10s deadline against child exit so a crash-on-boot fails in milliseconds. - permissions: session/request_permission parks its RPC until the user answers aloud; the reply picks the least-escalating offered option (kind enum classified through the shared adaptor-utils). - normalization: agent_message_chunk accumulates into a clamped turnBuffer, tool_call becomes progress with sanitized titles, thought/plan/tool_call_update and unknown discriminators are dropped, stopReasons map to complete/error semantics; speak-to-user arrives as a speak event once live-conversation activation succeeds (non-qwen agents -32601 and we stay off). - deps: @agentclientprotocol/sdk ^0.14.1 + zod ^3.25.0 (sdk peer); the lockfile diff is surgically limited to these entries. - tests: in-process fake connection covering the receipt table, drain behavior, turn-end continuation, stopReason mapping, crash cascade, respawn, and speak events; plus real-child lifecycle cases against a self-contained fake-acp-agent.mjs fixture (spawn env, prompt round-trip, crash-at-boot failing the handshake race). * test(qwen-live): M4 e2e — ACP call, permission, steering, multi-backend Four suites against a real `qwen --acp` child (fake OpenAI-compatible model endpoint) + the fake DashScope/Host fixtures, all collected by the no-AK PR gate: - m4-acp-call: voice handoff drives the ACP child; the completion flows back as [COMPLETE]; session_list rows carry the backend name - m4-acp-permission: write_file raises session/request_permission on the child; the ask reaches the voice model as [PERMISSION] plus the spoken line; the allow vote resolves the parked RPC and the file lands. (This pinned a real gap: qwen ACP sessions default to AUTO approval — silent allows — so the adaptor now sets the session to the asking mode on creation, matching what the voice product needs.) - m4-acp-steering: a busy-turn handoff receipts as joined or queued and the steered text provably reaches a model request payload (drained mid-turn or delivered with the next turn); the slow turn's conclusion still flows back - m4-acp-multibackend: serve (default) + ACP (named secondary) coexist; session_list shows both backends; handoffs route to the named backend's session; both completions flow back Harness: bootAcpLiveStack boots the acp backend (or the serve+acp pair) via QWEN_LIVE_BACKENDS, spawning process.execPath against the same CLI bundle serve uses, with the standard fake-endpoint env. * test(qwen-live): manual smoke test against a real qodercli --acp agent qodercli ships the same hidden ACP mode (QoderAgent over NDJSON stdio, auth via its own login instead of an OpenAI-compatible endpoint). The suite runs only when the binary is present (spawnSync probe; skips cleanly in CI) and covers the README checklist: initialize captures imageInput, steering honestly reports 'queued' (qodercli never drains), a prompt round-trip lands its completion with the response text, and listSessions carries the session label. Verified locally against qodercli 1.1.26: 2/2 passed. * chore(qwen-live): drop the unused direct zod dependency @agentclientprotocol/sdk peers on zod but nothing in qwen-live imports it directly; the workspace already provides it for the sdk's peer resolution. Reviewer observation on #10617. * fix(qwen-live): restore M4 registry wiring lost in main rebase The squash-merge of #10367 produced text-level conflicts on every M4 commit; blind --ours resolution reverted daemon.ts and live-session.ts to the pre-M4 single-adaptor shape. This restores the merged state: - daemon.ts: BackendRegistry + buildAdaptor (qwen-code and acp kinds) plus the installer HTTP routes (GET /live/setup, POST install/launch) from the merged #10367 — both surfaces in one file - live-session.ts: registry routing at all 11 call sites (session_list fan-out, session_create backend arg, handoff/monitor/stop/note-relay, pump), the R2 reconcile calls routed through adaptorFor/entry.adaptor, and the stale duplicate lastSentence removed - test rigs restored to the M4 registry shape (FakeAdaptor name ctor, BackendRegistry wrapping, daemon.test fakeInstaller) Verified: 271 unit / 17 e2e / lint / tsc / clean dist build all green. * fix(qwen-live): preserve live turn and permission delivery * fix(qwen-live): address review round 1 — child lifecycle, control-sequence injection, session reuse, queue leaks, config/routing Five fix clusters addressing 16 Critical findings (1 already addressed, 15 fixed), all probed and reproduced before fixing: Child lifecycle (R1-47/R1-31/R1-5/R1-32): - ensureChild memoizes the in-flight connect promise so two concurrent callers share one spawn (R1-32) - a failed handshake kills the spawned child instead of leaking it (R1-47); the kill uses the same SIGTERM→SIGKILL grace as close() - spawnChild attaches child.once('error') so an ENOENT rejects the handshake race instead of crashing the daemon (R1-31) - the real exit handler is attached only after handshake success and scoped to the child that owns the current connection, so a stale orphan's late exit cannot tear down the healthy respawn (R1-5) Control-sequence injection (R1-2/R1-3/R1-34/R1-48): - describeToolCall strips OSC/CSI/SS2-SS3/DCS/C0-DEL-C1 sequences from the command before composing the permission title (R1-2), shared via a new stripControlSequences helper in adaptor-utils - classifyOption fails closed to 'other' when the structured kind is absent — word heuristics over free-form labels can be inverted by negation ("Do not allow" → proceed) on exactly the generic agents this PR targets (R1-3) - an unknown stopReason uses a fixed message instead of interpolating the raw value into spoken/model context (R1-34) - speak-to-user messages and agent_message_chunk text are stripped at the adaptor boundary (R1-48) Session reuse (R1-4/R1-53): - trackSession replaces a stale entry when its generation differs or it is closed, so a respawned agent reusing a sessionId gets a fresh state instead of the dead one (R1-4) - HandleRegistry.closeSession clears the reverse mapping so the same backend id after restart mints a new handle, not the poisoned closed one (R1-53) Queue leaks (R1-6/R1-33): - the active array is removed entirely; waiters self-remove via detach when resolved by push/end/subscribe-takeover (R1-6) - each blocked next() registers a detach callback that removes the abort listener from the signal when the waiter settles (R1-33) Config/routing (R1-1/R1-8/R1-7): - a single-entry backends config without "default": true is normalized to isDefault:true so BackendRegistry.defaultAdaptor does not throw on input its own validator declared valid (R1-1) - QwenCodeAdaptor accepts a name option so two qwen-code backends do not collide in the registry's byName map or scoped-id keys (R1-8) - the injector's permission enqueue/retract uses scoped ${backend.adaptor}:${requestId} keys, matching the broker's convention, so two backends minting the same requestId cannot retract each other's spoken asks (R1-7) Also fixes R1-9: the steering e2e captures the fromIndex baseline before releasing the slow gate, matching the M2 test's ordering. 45 Suggestion findings declined (test coverage / dead code / defensive hardening / behavior preferences) with one-line reasons on each thread. * fix(qwen-live): address review round 2 — handle name propagation, steering fromIndex - R1-8 (fix-induced): QwenCodeAdaptor's createSession and listSessions still minted handles with the hardcoded ADAPTOR_NAME instead of the configured this.name — two qwen-code backends with different names produced colliding adaptor fields in handles, breaking jobByRef/scopedRequestId isolation. Both sites now use this.name so the handle's adaptor field matches the registry key. - R1-9: the steering e2e captured the fromIndex baseline AFTER slowGate.resolve(), but the slow turn's completion fires immediately on release — it was already below the baseline and the waitForMessage(fromIndex) could never match it. Moved the capture before the release, matching the M2 test's ordering. R2 verdicts (5 Critical, 7 Suggestion): - R1-5: valid fix, no further change — the connect-path onExit is inherently scoped (the test fake controls the lifecycle); the spawn path's healthyChild identity check is the correct guard - R2-1: declined — an already_resolved outcome means the vote was a no-op; relaying the note to a vote that didn't count would be wrong - R2-9: declined (re-graded to Suggestion) — /tmp in lifecycle tests is test quality, not a failure path this PR introduced - 7 Suggestions: declined — R1 fix witnesses, abort-while-draining, closed sessions in listSessions, docblock update — all test coverage or cosmetic on working code --------- Co-authored-by: Claude <[email protected]> |
||
|
|
8290c81eea |
feat(qwen-live): standalone voice daemon package — M1 + M2 (#10367)
* feat(qwen-live): scaffold standalone live daemon package with ported host stack New workspace package @qwen-code/qwen-live (M1+M2 of the Live split, issue #10118). This commit adds the package skeleton and the verbatim ports that need no behavioral changes: - host/: Live Host protocol v6 types, LiveHostCoordinator, and the discovery-file writer, ported from packages/cli/src/serve/live/ together with their test suites (51 tests green) - realtime/socket.ts, realtime/sanitize.ts: the two helper symbols the realtime client depends on, extracted from cli ui utils - registered in scripts/build.js buildOrder and root vitest projects * feat(qwen-live): generalize the realtime client tool surface Port qwen-realtime-session.ts and adapt it for the orchestration model: tools and instructions come from config, every declared tool dispatches through onFunctionCall with the raw argument payload, and outputs are receipt-style via submitFunctionOutput. The single-handoff machinery (multiple_handoffs error, steering acknowledgement, open-call complete) is removed; transport, response arbitration, pendingCalls timing, barge-in, and defensive validation are unchanged from the port. capturesTranscript marks handoff-style tools that capture the live transcript tail at call time. Test suite adapted: 27 tests green. * feat(qwen-live): add the BackendAdaptor seam and the qwen serve adaptor BackendAdaptor is the capability-bit-driven port between the live orchestrator and a coding-agent backend. QwenCodeAdaptor drives a running qwen serve daemon through the @qwen-code/sdk DaemonClient: 202-admission prompts, SSE event normalization (chunks and tool calls live under session_update.data.update), reject-if-idle mid-turn steering with a plain-prompt fallback, nested permission votes with voter attribution, and attachment-backed image blocks. 23 unit tests against an injected fake client. * feat(qwen-live): orchestrator, tools, injector, permissions, and daemon entry The M1+M2 core: - seven receipt-style dispatch tools plus remain_silent, with a dispatcher that answers every call (timeout fallback included) and session_N/job_N/asset_N handle registries - LiveSession wires Host audio, the realtime connection, tool handlers (appshot via the Host capture channel, handoff with voice-context packing and image attachments, busy-session steering), backend event pumps, and a bounded graceful-stop drain - Injector: safe-insertion window (VAD + response-in-flight + playback estimate at 48 kB/s with a quiet gap), batch aggregation, per-job progress throttling, barge-in drops progress, spoken/detail split - PermissionBroker: allow_always stays a one-shot protocol vote backed by a local TTL rule; remote resolutions retract queued asks - LiveDaemon: Host WS endpoint (Bearer + Origin-reject), discovery publish with fail-fast mutual exclusion and dead-owner handoff, appshot channel marked ready (the in-process capture path needs no separate verification), SIGINT/SIGTERM shutdown - SessionLog: JSONL with synchronous rotation (rename-before-reopen) 116 new unit tests; package total 167 green. * feat(qwen-live): end-to-end test suites against a real qwen serve daemon Test stack: a real qwen serve (spawned CLI bundle, fake-openai model endpoint), a scriptable fake DashScope realtime WebSocket server, the qwen-live daemon as a child process, and a protocol-v6 FakeHost client — every boundary is the real wire. Scenarios (11 tests): discovery publish/remove and hello/welcome, toggle brings up the realtime session (tool schema + instructions asserted on session.update), two-way audio byte fidelity, the full handoff loop against real serve with [COMPLETE] injection and spoken summary, injection-window hold/batch semantics, permission ask/vote relay, and busy-session steering tiers. The suites caught a real bug the unit layer could not: per-session calls must echo the daemon-issued clientId from the create/attach response (a self-made id is rejected by the client registration guard and broke handoff admission and vote attribution). QwenCodeAdaptor now tracks and echoes the issued id per session. * chore(release): keep @qwen-code/qwen-live versioned independently Same treatment as sdk/mobile-mcp/node-repl: the incubating package stays at its own 0.x version instead of lock-stepping with the CLI release train. The verbatim guard in package-scripts.test.js is updated alongside. * fix(qwen-live): harden review findings across dispatcher, injector, and permission relay Self-review fixes bundled after the adversarial pass: - adaptor: classify snake_case permission option ids (proceed_once) — the word-boundary regex never matched through "_", so an allow vote silently degraded to a cancelled outcome and serve dropped the turn - adaptor: detect the pending-prompt limit by error name as well as status, and fall image-bearing steers back to a regular prompt - dispatcher: a timed-out tool now answers with a pending receipt that tells the model not to retry (the handler keeps running) - injector: permission items flush ahead of the size-capped context join; spoken lines merge whole-line greedily under the 280-char cap - live-session: resubscribe the event pump with backoff until session_closed; a rejected permission chain can no longer become an unhandled rejection; receipts are clamped and never empty - permission broker: a failed auto-approval falls back to the spoken ask instead of losing the request - daemon/config/index: appshot readiness is declared at construction, numeric config ports parse, stray rejections are logged not fatal - e2e harness: QWEN_LIVE_E2E_KEEP=1 keeps the temp dirs for post-mortem * fix(qwen-live): address review round 1 — permission voting, shutdown, discovery reclaim, and hardening Permission trust boundary: - classify options by the wire's structured kind enum (allow_once/ allow_always/reject_*) instead of word regexes over snake_case ids, and vote the least-escalating match — a spoken "allow" now lands on proceed_once, never proceed_always_project - respond_permission's note is no longer discarded: it is logged and relayed to the backend session as a follow-up user instruction - track own votes per session so byUs attribution works on adopted sessions; narrow allow_always standing rules to the exact normalized command instead of a two-token prefix Lifecycle: - daemon stop() no longer hangs when SIGINT lands before listen() or when a Host redials during shutdown (stopping gate + client terminate) - dead-owner discovery reclaim now follows the cli contract (no-op commitOwner during handoff, republish after) — previously it could never succeed and a crash left the daemon unable to start - stop drain settles on the input-commit ack instead of burning the full 30s budget; response.created during the drain no longer flips the call back to speaking; a host-initiated start can no longer race deactivate() Robustness: - a late ASR final for an already-consumed input item is ignored as stale_input instead of dropping the call - raise the Host frame cap to cover a conformant CJK accessibility dump (64KiB frame cap contradicted the 32k-char field cap) - bin entry works through npm's bin symlink (realpath + pathToFileURL) - steered handoffs that join a running turn reuse the existing job instead of orphaning it; over-long steers fall back to a queued prompt; session_list unions created cwds, requests a full page, and maps hasActivePrompt for untracked sessions; created-session labels are applied via session metadata - injector: jobless throttle keys use the full context, dedup is symmetric, and barge-in drops clear the throttle stamp; sentence shapers handle CJK punctuation and mid-token periods - config loader: ENOENT-only swallow, BOM strip, ~ expansion, typed port validation, source-accurate errors; session log close() is terminal; transcript retention is capped Tests and docs: new config/daemon/sanitize suites, win32 guards restored on discovery mode assertions, protocol parity test now checks both type copies, qwen-live e2e suites join the no-AK PR gate, fake model gating in the inject e2e matches only the current turn and is consumed on match, FakeHost answers capture requests, lockfile diff reduced to qwen-live-only entries, README states the installer gap. * test(qwen-live): pin injector size caps and steered-message delivery - injector: 6000-char combined-context cap, whole-line spoken join at the 280-char boundary, single-line ellipsis truncation - steering e2e: assert the joined instruction actually reached the model endpoint, not just the receipt wording * fix(sdk): raise daemon browser bundle budget Co-authored-by: Qwen-Coder <[email protected]> * feat(qwen-live): scaffold standalone live daemon package with ported host stack New workspace package @qwen-code/qwen-live (M1+M2 of the Live split, issue #10118). This commit adds the package skeleton and the verbatim ports that need no behavioral changes: - host/: Live Host protocol v6 types, LiveHostCoordinator, and the discovery-file writer, ported from packages/cli/src/serve/live/ together with their test suites (51 tests green) - realtime/socket.ts, realtime/sanitize.ts: the two helper symbols the realtime client depends on, extracted from cli ui utils - registered in scripts/build.js buildOrder and root vitest projects * feat(qwen-live): generalize the realtime client tool surface Port qwen-realtime-session.ts and adapt it for the orchestration model: tools and instructions come from config, every declared tool dispatches through onFunctionCall with the raw argument payload, and outputs are receipt-style via submitFunctionOutput. The single-handoff machinery (multiple_handoffs error, steering acknowledgement, open-call complete) is removed; transport, response arbitration, pendingCalls timing, barge-in, and defensive validation are unchanged from the port. capturesTranscript marks handoff-style tools that capture the live transcript tail at call time. Test suite adapted: 27 tests green. * feat(qwen-live): add the BackendAdaptor seam and the qwen serve adaptor BackendAdaptor is the capability-bit-driven port between the live orchestrator and a coding-agent backend. QwenCodeAdaptor drives a running qwen serve daemon through the @qwen-code/sdk DaemonClient: 202-admission prompts, SSE event normalization (chunks and tool calls live under session_update.data.update), reject-if-idle mid-turn steering with a plain-prompt fallback, nested permission votes with voter attribution, and attachment-backed image blocks. 23 unit tests against an injected fake client. * feat(qwen-live): orchestrator, tools, injector, permissions, and daemon entry The M1+M2 core: - seven receipt-style dispatch tools plus remain_silent, with a dispatcher that answers every call (timeout fallback included) and session_N/job_N/asset_N handle registries - LiveSession wires Host audio, the realtime connection, tool handlers (appshot via the Host capture channel, handoff with voice-context packing and image attachments, busy-session steering), backend event pumps, and a bounded graceful-stop drain - Injector: safe-insertion window (VAD + response-in-flight + playback estimate at 48 kB/s with a quiet gap), batch aggregation, per-job progress throttling, barge-in drops progress, spoken/detail split - PermissionBroker: allow_always stays a one-shot protocol vote backed by a local TTL rule; remote resolutions retract queued asks - LiveDaemon: Host WS endpoint (Bearer + Origin-reject), discovery publish with fail-fast mutual exclusion and dead-owner handoff, appshot channel marked ready (the in-process capture path needs no separate verification), SIGINT/SIGTERM shutdown - SessionLog: JSONL with synchronous rotation (rename-before-reopen) 116 new unit tests; package total 167 green. * feat(qwen-live): end-to-end test suites against a real qwen serve daemon Test stack: a real qwen serve (spawned CLI bundle, fake-openai model endpoint), a scriptable fake DashScope realtime WebSocket server, the qwen-live daemon as a child process, and a protocol-v6 FakeHost client — every boundary is the real wire. Scenarios (11 tests): discovery publish/remove and hello/welcome, toggle brings up the realtime session (tool schema + instructions asserted on session.update), two-way audio byte fidelity, the full handoff loop against real serve with [COMPLETE] injection and spoken summary, injection-window hold/batch semantics, permission ask/vote relay, and busy-session steering tiers. The suites caught a real bug the unit layer could not: per-session calls must echo the daemon-issued clientId from the create/attach response (a self-made id is rejected by the client registration guard and broke handoff admission and vote attribution). QwenCodeAdaptor now tracks and echoes the issued id per session. * chore(release): keep @qwen-code/qwen-live versioned independently Same treatment as sdk/mobile-mcp/node-repl: the incubating package stays at its own 0.x version instead of lock-stepping with the CLI release train. The verbatim guard in package-scripts.test.js is updated alongside. * fix(qwen-live): harden review findings across dispatcher, injector, and permission relay Self-review fixes bundled after the adversarial pass: - adaptor: classify snake_case permission option ids (proceed_once) — the word-boundary regex never matched through "_", so an allow vote silently degraded to a cancelled outcome and serve dropped the turn - adaptor: detect the pending-prompt limit by error name as well as status, and fall image-bearing steers back to a regular prompt - dispatcher: a timed-out tool now answers with a pending receipt that tells the model not to retry (the handler keeps running) - injector: permission items flush ahead of the size-capped context join; spoken lines merge whole-line greedily under the 280-char cap - live-session: resubscribe the event pump with backoff until session_closed; a rejected permission chain can no longer become an unhandled rejection; receipts are clamped and never empty - permission broker: a failed auto-approval falls back to the spoken ask instead of losing the request - daemon/config/index: appshot readiness is declared at construction, numeric config ports parse, stray rejections are logged not fatal - e2e harness: QWEN_LIVE_E2E_KEEP=1 keeps the temp dirs for post-mortem * fix(qwen-live): address review round 1 — permission voting, shutdown, discovery reclaim, and hardening Permission trust boundary: - classify options by the wire's structured kind enum (allow_once/ allow_always/reject_*) instead of word regexes over snake_case ids, and vote the least-escalating match — a spoken "allow" now lands on proceed_once, never proceed_always_project - respond_permission's note is no longer discarded: it is logged and relayed to the backend session as a follow-up user instruction - track own votes per session so byUs attribution works on adopted sessions; narrow allow_always standing rules to the exact normalized command instead of a two-token prefix Lifecycle: - daemon stop() no longer hangs when SIGINT lands before listen() or when a Host redials during shutdown (stopping gate + client terminate) - dead-owner discovery reclaim now follows the cli contract (no-op commitOwner during handoff, republish after) — previously it could never succeed and a crash left the daemon unable to start - stop drain settles on the input-commit ack instead of burning the full 30s budget; response.created during the drain no longer flips the call back to speaking; a host-initiated start can no longer race deactivate() Robustness: - a late ASR final for an already-consumed input item is ignored as stale_input instead of dropping the call - raise the Host frame cap to cover a conformant CJK accessibility dump (64KiB frame cap contradicted the 32k-char field cap) - bin entry works through npm's bin symlink (realpath + pathToFileURL) - steered handoffs that join a running turn reuse the existing job instead of orphaning it; over-long steers fall back to a queued prompt; session_list unions created cwds, requests a full page, and maps hasActivePrompt for untracked sessions; created-session labels are applied via session metadata - injector: jobless throttle keys use the full context, dedup is symmetric, and barge-in drops clear the throttle stamp; sentence shapers handle CJK punctuation and mid-token periods - config loader: ENOENT-only swallow, BOM strip, ~ expansion, typed port validation, source-accurate errors; session log close() is terminal; transcript retention is capped Tests and docs: new config/daemon/sanitize suites, win32 guards restored on discovery mode assertions, protocol parity test now checks both type copies, qwen-live e2e suites join the no-AK PR gate, fake model gating in the inject e2e matches only the current turn and is consumed on match, FakeHost answers capture requests, lockfile diff reduced to qwen-live-only entries, README states the installer gap. * test(qwen-live): pin injector size caps and steered-message delivery - injector: 6000-char combined-context cap, whole-line spoken join at the 280-char boundary, single-line ellipsis truncation - steering e2e: assert the joined instruction actually reached the model endpoint, not just the receipt wording * feat(qwen-live): port the Live Host installer — M1 gap closed `live-host-installer.ts` moves verbatim from packages/cli/src/serve/live/ (same OSS/GitHub dual-source manifest fetch, sha256 + codesign + team-identifier verification, staging + backup install with rollback, and the retryable status machine), with its 11-test suite. The daemon gains the matching HTTP surface: - GET /live/setup — installed Host state - POST /live/setup/install — download + verify + install the latest POST /live/setup/launch — open the installed app All three sit behind the daemon's existing Bearer wall (a browser context sending Origin is always refused, wrong token 401) — pinned by a new daemon test; unknown paths stay 404 and GET /healthz answers plain 200. Bootstrap no longer depends on the built-in qwen-code Live integration having installed the Host first. * fix(qwen-live): attach the appshot-timeout rejection handler before advancing fake timers The timeout rejects synchronously inside advanceTimersByTimeAsync; a handler attached only afterwards leaves an unhandled rejection for an instant, which vitest counts as a run-level error (CI fails the package with Errors: 1 even though all 250 tests pass). Attach the expectation first, await it after the clock advances. * test(qwen-live): sync the no-AK gate pin with the qwen-live suites The no-ak-integration-ci pin (added upstream after our branch point) asserts the gate's file list verbatim; add the four qwen-live suites the gate has been running since they joined it. * fix(qwen-live): address review round 2 — trust-boundary keys, backpressure, session lifecycle Criticals (all probed and reproduced before fixing): - broker titleKeyOf: split on the FIRST colon and keep the entire remainder (commands routinely contain colons — a truncated key auto-approved a different -m message), and stop case-folding (/a vs /A are different paths on Linux). The whitespace-collapse pin is preserved - broker deny: an explicit spoken refusal now revokes the standing rule that matched the same request — the silent auto-answer's fallback path could otherwise be overridden by the invisible rule on the next identical request - adaptor activeJobRef: a busy-session fallback (image steer, over- long text 400) no longer overwrites the running turn's ref with the queued prompt's id — later steer receipts report the right job - adaptor byUs attribution: the daemon's originator stamp is now authoritative when present; ownVotes only covers anonymous resolutions. Another client resolving during our vote round-trip no longer reads as our own answer (R1-2 regression, fixed test-first) - adaptor permission titles: control sequences stripped before the title reaches speech or keys a standing rule (whole command kept — no first-line cut, unlike the progress path) - live-session pushAudio: the provider's backpressure signal is propagated (the port source fails the call) and onAudioDropped is wired to failCall — a socket over its buffer cap no longer silently garbles the utterance; the deliberate true for stale/connecting frames is untouched - handles: closed sessions stop resolving entirely (registry-level), and session_closed clears the call's default handle so a session- less handoff rebuilds instead of retrying a dead target forever - handles: read-site reconciliation — session_list and session_monitor transition a session's stale non-terminal jobs to done when the backend itself reports idle (a turn_complete emitted while no pump was subscribed no longer reports a running active_job forever) Suggestions on this PR's new surfaces: - live-host.yml: packages/qwen-live/src/host/** joins the path filter so the parity-test lane runs on qwen-live-only PRs instead of first failing in the merge queue - installer: the install route passes force=true (the recovery path for a corrupted-but-present app, matching the CLI mirror), and launch() gains the non-darwin guard in BOTH copies so the byte parity pin stays green - index: directory-form invocation (node packages/qwen-live) now runs main() instead of exiting 0 silently - realtime: the dead lastHandoffEntryCount machinery is removed (truncate-and-reset replaced the cursor it pretended to track) --------- Co-authored-by: Claude <[email protected]> Co-authored-by: 易良 <[email protected]> Co-authored-by: yiliang114 <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |