mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
f11bb31df2df9e63f68dc034e0be8b5b2fa0fd89
6751
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f11bb31df2 |
fix(cli): fix thought viewer truncation, layout gaps, and choppy scrolling in VP mode (#6002)
* fix(cli): wrap thought viewer text to visual lines The full-screen ThinkingViewer split `data.text` on '\n' and rendered each logical line with `wrap="truncate-end"`. A thought is usually a single long paragraph with no newlines, so it collapsed to one ellipsised row above an empty box and `maxScroll` stayed 0 (could not scroll). Pre-wrap the text to visual rows at the inner content width (border + paddingX = 4 cols) before slicing, reusing the existing `wrapToVisualLines` helper (now exported from ConversationMessages). Scrolling and rendering now operate on the same rows the user sees. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): collapse VP viewport to content and sink the composer In terminal-buffer (VP) mode the conversation wasted vertical space: - VirtualizedList pinned its root box to the full `containerHeight`, so short content left a tall blank gap and pushed the composer far down. Collapse the box to `min(containerHeight, totalHeight)` so it grows with its content like the legacy <Static> path; the scroll math still uses the full height, so overflow scrolling is unchanged. - `availableTerminalHeight` subtracted `staticExtraHeight` + `MAIN_CONTENT_HEIGHT_RESERVATION`, the <Static> overflow-flicker guards. VP clips natively and does not need them, so they stranded ~5 blank rows below the composer (input never reached the bottom). Drop the reservation in VP; non-VP keeps it unchanged. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * perf(cli): coalesce VP scroll events per frame Terminal mouse reporting emits one event per row crossed, so a brisk wheel spin or scrollbar drag fired a rapid burst, each applied synchronously with a full Ink reflow + terminal flush — the source of the choppy scroll. Accumulate wheel deltas / the latest drag row in refs and flush at most once per ~16ms frame. A press still applies instantly; under NODE_ENV==='test' updates apply synchronously so the existing timer-free tests keep passing. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): cancel queued scroll on scrollbar press A wheel burst schedules a 16ms coalescing flush. If the user clicked the scrollbar within that window, the press applied its row immediately but the still-armed timer then fired `scrollBy` with the leftover wheel delta, yanking the view off the clicked row. Clear the pending wheel/drag intent and cancel the timer when a scrollbar press takes over. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): keep a small height slack in VP mode Zeroing the VP layout reservation removed all slack, so when the composer grew (multi-line input) the one-frame-late controlsHeight measurement briefly oversized the list and overflowed the terminal — the exact jitter the layout change aimed to remove. Drop only the Static-specific staticExtraHeight (3) and keep MAIN_CONTENT_HEIGHT_RESERVATION (2) as a transient-measurement buffer; the composer still sits far closer to the bottom than before (was ~5 stranded rows). Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * refactor(cli): move wrapToVisualLines to textUtils It lived in the 1000-line ConversationMessages component and ThinkingViewer imported it across components. Co-locate it with its sibling visual-wrapping helper (sliceTextByVisualHeight) in utils/textUtils.ts and import from there. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * refactor(cli): share frame-coalesced scroll hook and test it Extract the per-frame scroll coalescing into useFrameCoalescedFlush and use it from both ScrollableList and ThinkingViewer (whose wheel handler was still un-batched, so a brisk spin in the expanded thought viewer stuttered). Drop the NODE_ENV==='test' escape hatch that made the production timer path unreachable in tests: the mouse-scroll tests now advance a real frame before asserting, exercising the batching/accumulation/precedence logic. Adds a regression test for a scrollbar press canceling a still-pending wheel flush. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> * fix(cli): match legacy bottom spacing in VP mode (no reservation) The static/<Static> path reserves no blank rows under the composer — the staticExtraHeight + MAIN_CONTENT_HEIGHT_RESERVATION budget only caps inline streaming-message height, while the composer flows to the very bottom of the terminal. Reserve nothing in VP so its composer reaches the bottom the same way, instead of leaving a 2-row gap. The one-frame controlsHeight measurement lag on composer growth mirrors legacy mode letting the terminal scroll. Generated with AI Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: 秦奇 <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
e2b85559a3 |
feat(cli): add --safe-mode flag to disable all customizations for troubleshooting (#4943)
* feat(cli): add safe mode * fix(test): add isSafeMode to test mocks and config stubs * fix(ui): hide memory toggles in safe mode * fix(cli,core): harden safe mode guards for permissions and extensions Add safeMode guards to all permission-related settings in loadCliConfig that previously only checked bareMode — mergedAllow, mergedAsk, mergedDeny, resolvedCoreTools, disabledSlashCommands, disabledTools, coreTools, allowedTools, autoMode, and approvalMode. Without these guards, a malicious repo settings.json could auto-approve all shell commands even in --safe-mode. Refactor the extension loading conditional to eliminate the empty if-body (safeMode branch) that was fragile against linter auto-fixes. Add diagnostic log line in Config constructor when safe mode activates, so daemon/headless/CI contexts have a log entry explaining which subsystems are disabled. Add comprehensive safe mode test coverage (7 tests) in CLI config tests. Co-Authored-By: Qwen Code <[email protected]> * fix(test): add isSafeMode to config stub in gemini.test.tsx The 'writes non-interactive warnings' test from main was missing isSafeMode() on its configStub, causing a TypeError after the merge. Co-Authored-By: Qwen Code <[email protected]> * fix(cli,core): close safe-mode bypass gaps in context loading and child agents Add isSafeMode() guards to code paths that previously only checked bareMode, allowing project/user customizations to leak through in safe mode: - AppContainer performMemoryRefresh: skip context file loading - /directory add: skip include-directory memory reload - CLI config: output-language.md, includeDirectories, sandbox, loadMemoryFromIncludeDirectories - Config.getProjectHooks/getUserHooks: return undefined in safe mode - Daemon workspace agents: pass real safe-mode state instead of hardcoded false Propagate safeMode through AgentPersistedCliFlags so child agents spawned via the agent tool inherit safe mode. Add debug logging when subagent-manager overrides a requested level in safe mode. Add unit tests for safe-mode branches in subagent-manager and skill-manager that were previously untested. Co-Authored-By: Qwen Code <[email protected]> * fix(test): add isSafeMode to directoryCommand mock config The new config.isSafeMode() call in directoryCommand.tsx threw TypeError in tests because the mock config did not stub it. The error was caught by the surrounding try/catch, flipping messageType from info to warning and failing 4 tests. Co-Authored-By: Qwen Code <[email protected]> * fix(safe-mode): address code review feedback and improve consistency - Fix type error in config test (approvalMode string literal) - Fix dead else-if branch in extension loading logic - Remove unnecessary definite assignment assertion on safeMode field - Add safe-mode guards for disabledSkillNamesProvider and getMcpServers() - Gate getPreventSystemSleepEnabled() with isSafeMode() check - Extract shared isSafeModeEnv() helper to deduplicate env var resolution - Refactor comma operator in subagent-manager ternary for readability - Use config.getWorkingDir() instead of process.cwd() in AppContainer - Add warning when --core-tools flag is used in safe mode - Add test coverage for getMcpServers() returning empty in safe mode * fix: rename safeMode.ts to safe-mode.ts for kebab-case convention * fix(safe-mode): close extension leak and restore bare-mode sleep prevention - Remove dead else-if branch: adding !isSafeMode() guard made it unreachable since the first if already covers !safeMode && !bareMode - Remove unused getExplicitExtensionNames() private method - Remove unrelated !getBareMode() from getPreventSystemSleepEnabled to preserve bare-mode sleep inhibition * fix(safe-mode): address review feedback on bare-mode extensions, env parsing, and test mock - Add missing Protocol export to contentGenerator mock in safe-mode test - Restore bare-mode explicit extension loading that was dropped in previous fix - Use shared isTruthy helper for consistent env var parsing in isSafeModeEnv * fix(core): add missing DEFAULT_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH to safe-mode test mock The telemetry mock in config.safe-mode.test.ts was missing the DEFAULT_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH export, causing all 14 tests to fail with: No "DEFAULT_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH" export is defined on the "../telemetry/index.js" mock Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen Code <[email protected]> Co-authored-by: 易良 <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
0af3cd8131 |
fix(core): filter thought parts from Stop hook last_assistant_message (#6009)
getLastModelMessageText() concatenated all text parts from the last model message without filtering out thought parts (part.thought === true). This caused the Stop hook's last_assistant_message to include the model's internal reasoning text, making structured output validation difficult. Added !part.thought to the filter condition in both GeminiChat and the client fallback implementation, consistent with existing thought filtering in copyCommand, partUtils, and stripThoughtPartsFromContent. Added two unit tests: - filters out thought parts from mixed model messages - returns undefined when all text parts are thoughts Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
35a8851a38 |
feat(core): add --insecure flag to skip TLS verification for self-signed endpoints (#3535) (#5962)
* feat(core): add --insecure flag to skip TLS verification for self-signed endpoints (#3535) Enable skipping TLS certificate verification for outbound model API connections via a new --insecure CLI flag, QWEN_TLS_INSECURE, or NODE_TLS_REJECT_UNAUTHORIZED=0. The setting is applied to the undici dispatcher Qwen Code installs: a direct connection uses connect TLS options, while a proxied connection disables verification for the upstream origin (requestTls) and a self-signed HTTPS proxy (proxyTls). Off by default; behavior is unchanged when not enabled. Fixes #3535 Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(core): address review on --insecure (security + simplification) (#3535) - Block project .env from enabling QWEN_TLS_INSECURE by adding it to PROJECT_ENV_HARDCODED_EXCLUSIONS, so an untrusted repo cannot silently disable TLS verification for all API connections. - Remove the unverifiable Bun fetch `tls` special-casing; Bun users can still opt out via NODE_TLS_REJECT_UNAUTHORIZED=0, which Bun honors natively. - Do not suggest `--insecure` in the TLS error hint when verification is already disabled; show a network/protocol-oriented message instead. - Add tests: env-flag regex/falsy branches, loadCliConfig env side-effect, fetch hint variants, and a security guard for the project .env exclusion. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(core): harden --insecure per review (#3535) - Block NODE_TLS_REJECT_UNAUTHORIZED from project .env too (initial load only consults PROJECT_ENV_HARDCODED_EXCLUSIONS), since isTlsVerificationDisabled() honors it. - When opting out, set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide in loadCliConfig and emit a stderr MITM warning. This makes the opt-out effective on the Bun runtime and the proxy-creation fallback path (which the undici dispatcher does not cover), and gives a user-visible signal. - Avoid evaluating isTlsVerificationDisabled() twice on the proxy path via a default parameter on getOrCreateSharedDispatcher. - Update tests accordingly. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(cli): broaden --insecure warning and log it; add env-path tests (#3535) - Broaden the TLS-disabled warning to state the process-wide blast radius (API, OAuth, MCP servers, child processes), since NODE_TLS_REJECT_UNAUTHORIZED=0 is set process-wide. - Also emit the warning via debugLogger so the state is discoverable in ~/.qwen/debug/ after terminal scrollback is gone. - Add tests for the env-var-only path (pre-set QWEN_TLS_INSECURE) and the already-disabled guard (no duplicate assignment/warning). Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> |
||
|
|
1af1bb8a55 |
fix(ci): cover release integration regressions (#5994)
* fix(ci): cover release integration regressions * fix(ci): retry linter archive downloads * fix(ci): keep release CI PR focused |
||
|
|
8babaa47e0 |
fix(web-shell): improve follow-up suggestion handling (#5996)
Co-authored-by: ytahdn <[email protected]> |
||
|
|
c90e6e7ba4 |
feat(channels): Add channel agent bridge abstraction (#5978)
* feat(channels): add channel agent bridge abstraction Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): handle bridge session lifecycle cleanup Co-authored-by: Qwen-Coder <[email protected]> * fix(channels): close bridge lifecycle review gaps Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <[email protected]> * fix: address channel bridge review feedback Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5978) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
dd077b9eeb | fix(daemon): filter specialized models from selectors (#5993) | ||
|
|
b737364892 |
fix(web-shell): prefer raw file diffs in tool output (#5992)
Co-authored-by: ytahdn <[email protected]> |
||
|
|
23830a180b |
fix(cli): avoid ACP runtime preload on serve fast path (#5989)
Route serve request helper imports through lightweight ACP bridge subpaths so runQwenServe can keep the ACP runtime behind its existing dynamic boundary. Add source graph and esbuild metafile guards that fail if the serve fast path statically reaches the ACP runtime modules again. Refs #4748 Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
9601d90b78 |
fix(ui): display output tokens instead of cumulative API throughput for subagents (#5972)
Multiple UI components displayed executionSummary.totalTokens (cumulative sum of total_tokens across all API rounds) as the subagent token count. For a subagent making 87 requests with ~51K prompt each, this inflated to 4.4M — misleading users into thinking it was context window usage. Switch all subagent token displays to use outputTokens (what the model actually generated), aligning with the loading indicator which already correctly shows output tokens only. Closes #5683 |
||
|
|
68348e236a |
feat(channels): qwen tag — RFC + Phase 0 (multiplayer channel-resident agent) (#5888)
* docs(channels): add "qwen tag" RFC — channel-resident multiplayer agent Design for a persistent, multiplayer agent that lives in a chat channel (DingTalk-first), built on the existing channel adapters (qwen channel start, packages/channels/*) and the qwen serve daemon rather than a new service. Covers the phased plan: Phase 0 multiplayer identity, Phase 1 proactive engine (scheduler + cold-group push, daemon migration), Phase 2 channel memory + governance, plus tradeoffs and resolved design decisions. Part of #5887 Co-Authored-By: Qwen-Coder <[email protected]> * feat(channels): qwen tag Phase 0 — multiplayer identity & group-safe commands In a thread-scoped group every member shares one session, which surfaces a few gaps in the channel layer. Phase 0 closes them as a backward-compatible increment on the existing AcpBridge channel path: - Inject a [sender] marker into the prompt for group turns so the agent can tell speakers apart; skipped for 1:1 chats and for already-prefixed re-entries. - Add Envelope.alreadyPrefixed so collect-mode coalescing does not double-prefix the already-tagged buffered text. - Require "/clear confirm" in groups — a bare /clear no longer wipes the shared session for the whole channel; DMs still clear directly. - Add a read-only /who reporting channel / workspace / session scope without creating a session. - Drop DingTalk group messages with no conversationId, so the shared session is never keyed on the expiring sessionWebhook. - Fix the stale ChannelConfig.dispatchMode JSDoc (runtime default is 'steer'). Adds unit tests for sender attribution, the collect double-prefix guard, the group /clear confirmation, and the read-only /who. Part of #5887 Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): scope group clear confirmation * fix(channels): address review on qwen tag Phase 0 - Restrict /clear in a shared (thread) group to config.allowedUsers (when set) and require an explicit "confirm"; only the shared case is gated — DMs and per-user groups clear directly. Reconcile the RFC (OD-4) with this approach (a hyphenated /clear-channel isn't parseable; per-member owner-gate waits on the identity model, OD-3/OD-11). - Sanitize the injected [sender] marker (strip brackets/CR/LF, cap length) so a crafted nick can't break out of or spoof the attribution tag. - Surface to stderr when a collect-mode coalesced re-entry drops buffered turns instead of swallowing the error. - Narrow Envelope.alreadyPrefixed to the literal `true` (internal-only flag). - Expose DingtalkChannel.isUnroutableGroupMessage and test the group-without-conversationId drop; replace flaky setTimeout sequencing in the collect test with vi.waitFor; add tests for /who (active + private scope) and /clear authorization in a shared group. Part of #5887 Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): preserve group command routing * fix(channels): harden group routing (map leak, injection, slash attribution) Address the surviving Phase-0 review criticals: - /clear now purges every per-session map (sessionQueues, activePrompts, collectBuffers), not just instructedSessions, so a long-running gateway doesn't leak dead session entries. - QQ group adapter sanitizes the self-prefixed sender name. It sets alreadyPrefixed, which bypasses ChannelBase's [..]/newline/length guard, so a crafted QQ nickname could otherwise inject brackets/newlines. - Unrecognized slash commands in a shared group keep their [sender] attribution; recognized commands (a local handler or a forwarded agent command) still reach the agent verbatim. - Sanitize quoted referencedText (strip control chars, cap at 500) so it can't inject newlines/instructions or balloon the prompt. - /who reports only the workspace basename, not the absolute cwd. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): close Phase-0 review criticals (quote injection, /clear cancel, drain log) - referencedText: strip the wrapper's own delimiters (" [ ]) in addition to control chars, so a quoted message can't break out of [Replying to: "..."] and inject its own top-level instructions. - /clear: cancel any in-flight bridge.prompt() for the cleared session(s) and drop their buffered follow-ups before purging the maps, so a running turn can't deliver a stale response into — or resurrect — the cleared session. - collect-drain failure log now includes the sessionId and last sender so concurrent sessions are diagnosable. - Extract the duplicated sender-name sanitizer into a shared sanitizeSenderName helper used by both ChannelBase and QQChannel; reuse isSharedGroupSession for the /who scope note. - Tests: quote-breakout payload, /clear cancels in-flight, /who in a DM, single-scope hasSession/removeSession, and a sanitizeSenderName unit test. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): make group slash-command pass-through race-free and invalidate queued turns on /clear CRITICAL 1 (sender attribution race): availableCommands is populated asynchronously by the ACP available_commands_update notification, so a registration check could see an empty list on a fresh session and wrongly [sender]-prefix the first real /command — corrupting it into plain text. Stop inline-prefixing ANY slash-shaped message: pass it through verbatim so it always parses as a command regardless of load/registration state. Also widen the command token to include - and : so /compress-fast and /git:commit parse as commands. Tradeoff: an UNREGISTERED group slash command no longer carries [sender] attribution (inverse of the earlier R2 ask) — not breaking real commands is the safer default; flagged for maintainers. CRITICAL 2 (/clear vs queued followups): a teammate turn that entered handleInbound() before /clear confirm had already captured the prev.then() chain and would still run bridge.prompt() against the just-cleared session. Add a per-session generation counter bumped up-front by /clear; a queued turn snapshots it at enqueue and bails if it was bumped before the turn dequeued. Tests: fresh-session/empty-availableCommands pass-through, hyphenated command, unrecognized command (flagged behavior), non-slash still prefixed; /clear genuinely awaits a PENDING in-flight turn before confirming; /clear confirm invalidates an already-queued followup (no resurrection); collect-drain failure logs the lost count with sessionId + sender to stderr. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): only skip group [sender] tag for real slash commands The group attribution check keyed off the lenient parseCommand(), which accepts slash-prefixed paths (e.g. /tmp/foo) as commands, so that prose reached the model without the [sender] tag. Decide command-vs-prose with a dedicated isSlashCommand() that mirrors the CLI's classifier (cli ui/utils/commandUtils.ts isSlashCommand): reject //, /*, a bare /, and any path separator in the first token. Slash-prefixed paths and comments now keep their speaker attribution, while real commands (/compress, /git:commit) still pass through verbatim. Purely lexical, so it stays race-free against the async command list. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): bound /clear's wait on a wedged in-flight turn doClear awaited active.done after cancelSession, but active.done only resolves in the prompt() finally. If the ACP child is wedged (stuck tool call, not reading stdin, or crashed without closing), cancelSession may throw (swallowed) or succeed yet the prompt never returns, so active.done never resolves and /clear — and the whole channel — hangs forever. Race the wait against a CLEAR_CANCEL_TIMEOUT_MS (3s) timeout and purge anyway when it wins: the per-session maps are still purged and the generation already bumped, so a turn that settles later is invalidated. Cancellation stays best-effort. Tests: a wedged session whose active.done never resolves still completes /clear (purges every map, replies "Session cleared") within the timeout via fake timers — no real wait(ms); plus /clear Confirm / CONFIRM (mixed-case) is accepted, guarding the handler's .toLowerCase(). Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): test QQ group slash branch, log dropped queued turns, reclaim cleared generations Address review feedback on the Phase-0 qwen-tag /clear + group-slash work: - QQChannel: cover the previously-untested `isSlash` branch of handleGroup — a group `/clear` is forwarded verbatim (no `[sender]` tag) and must NOT set `alreadyPrefixed`. Catches a regression that always sets it. - ChannelBase: when a followup turn is dequeued after `/clear` bumped the session generation, log the drop (with sessionId) instead of returning silently, so an unanswered queued message is diagnosable. - ChannelBase: reclaim the bumped `sessionGenerations` entry once the cleared session's queue drains (or immediately when nothing was queued), so a long-running gateway no longer leaks one entry per `/clear`. Reclamation is deferred and guarded so it can't delete an entry a still-queued turn needs, and a wedged in-flight turn can't block `/clear`. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): harden qwen-tag group session gating, cancel bounds, and prompt sanitization Address the latest PR #5888 review batch (4 Criticals + suggestions): - Treat sessionScope 'single' as a SHARED group session: the shared-scope predicate now covers both 'thread' and 'single' (via SHARED_SCOPES), so a 'single'-scoped group can no longer bypass /clear confirm + allowlist gating and wipe the channel-wide __single__ session with a bare /clear. - Bound the steer-mode wind-down wait: race active.done against CLEAR_CANCEL_TIMEOUT_MS (and fire-and-forget the cancel) so a wedged ACP child can't pin the session queue forever, matching the /clear hardening. - Make /clear's cancelSession request fire-and-forget so a wedged cancel request can't hang /clear before the bounded active.done wait even starts. - Strip Unicode line/paragraph separators (U+2028/U+2029) and bidi overrides (U+202A-U+202E, U+2066-U+2069) from sender names and quoted text via shared sanitize helpers (sanitizeSenderName, new sanitizeQuotedText); route referencedText and attachment filenames through the shared quoted sanitizer. - Add sender/conversation context to the generation-bail drop log and the DingTalk unroutable-group-message drop log. - Tests: fix TS4111 bracket-access in ChannelBase tests (test-inclusive typecheck clean); add load-bearing coverage for single-scope sharing, wedged steer, wedged /clear cancel, Unicode/bidi sanitization, and the generation-reclamation guard fire paths. - Correct the isSlashCommand JSDoc to document the intentional bare-'/' divergence from the CLI classifier; note SessionRouter's by-sender scan does not match single-scoped keys (latent today). Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): stop block-streaming on /clear, not just cancel it doClear flipped active.cancelled = true but never called active.stopStreaming(), unlike the /cancel handler. In block-streaming channels, text already buffered in the BlockStreamer could still be emitted by the idle timer during/after /clear, leaking a stale response into the just-cleared session. Mirror /cancel: also tear down the streamer on the cancelled prompt in the clear path. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): gate /clear for single-scope DMs and unstick wedged steer queue Two follow-up fixes in ChannelBase, both on my recent fixes. CRITICAL 1: `single` scope maps EVERY sender — group OR DM — to the one `__single__` session, but the shared-session guard also required `isGroup`, so anyone who could DM the bot could bare-/clear the channel-wide session without the confirm + allowedUsers gate. The shared-session predicate now treats `single` as shared regardless of isGroup (`thread` stays group-only); /help and /who follow the same predicate. CRITICAL 2: the bounded steer wait stopped `await active.done` from hanging, but the replacement turn was still chained behind the wedged turn's never-resolving sessionQueues tail, so the follow-up (and every later message) hung forever. On the steer timeout we now re-seed the chain (prev = resolved) so the next turn starts a fresh chain; the wedged turn stays cancelled, so a late settle still can't deliver a stale response. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): sanitize attachment filePath and tighten isSlashCommand The attachment filePath was embedded raw in the prompt while the filename beside it was sanitized. Adapters build the path by basename()-ing the user-supplied filename, so its last segment carries the same attacker-controlled chars (brackets, newlines, U+2028, bidi overrides), letting a crafted filename inject prompt lines via the path. Route the rendered path through the same sanitizeQuotedText neutralization; att.filePath itself is left intact, and benign names render unchanged so the agent's read-file tool still resolves them. Also tighten isSlashCommand to require parseCommand()'s token charset ([a-zA-Z0-9_:-]+ plus an optional @botname). A non-command-shaped input like /cafe or a zero-width-laden token previously skipped the group [sender] tag yet was not a runnable command, reaching the shared session as unattributed prose. It now keeps its attribution. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): guard activePrompt cleanup against replacement-turn clobber The per-turn `finally` in ChannelBase deleted `activePrompts[sessionId]` unconditionally. With the steer-mode bounded wait, this corrupts steer protection: when turn A wedges (its `bridge.prompt()` never resolves), the `CLEAR_CANCEL_TIMEOUT_MS` race times out (`steerWedged`), and turn B starts a fresh chain and re-seeds `activePrompts` with its own entry. When A's prompt finally settles and reaches its `finally`, the unconditional delete removes B's entry — so a later turn C sees `activePrompts.get(sessionId) === undefined` and silently loses steer protection (it forwards verbatim instead of cancelling + re-prompting). Fix: capture this turn's own `ActivePrompt` and compare-and-delete — only clear the entry if it is still ours (`activePrompts.get(sessionId) === promptState`). Sibling-map audit (same later-settling-wedged-turn clobber): - collectBuffers: also gated on the same `stillCurrent` flag. A replaced wedged turn must not drain the buffer the live replacement turn now owns (reachable via mixed-mode single-scope, where a steer turn and collect follow-ups share one session). Behavior-preserving: in all serialized flows the turn is still current at its finally, so the drain runs exactly as before. - sessionQueues / sessionGenerations / instructedSessions: not mutated in the per-turn finally (only in /clear), so no per-turn clobber. Queued turns are already guarded against /clear by the sessionGenerations counter. - promptState.resolve(), bridge.off, streamer.stop, onPromptEnd: per-turn-owned cleanup, kept unconditional (a replaced wedged turn must still release any steer/clear waiter racing its done promise). Adds a deterministic test (manual deferred, no timers/sleeps) reproducing A wedged -> B replaces -> A settles late, asserting B's entry survives and a following turn C still engages steer protection. Reverting the guard fails it. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): guard onPromptEnd on superseded turns and strip control chars from sender names When a steer-replacement turn B re-seeds a session after the bounded wind-down wait times out, a wedged predecessor turn A can settle late and run its finally. onPromptEnd hides a session/chat-scoped working indicator (e.g. the Telegram/WeChat typing indicator keyed by chatId), so A's previously-unconditional onPromptEnd would stop B's indicator while B is still working. Guard onPromptEnd with the same stillCurrent identity check that already protects the per-session map cleanup, so a superseded turn can no longer clear the successor's indicator. A's own teardown (textChunk listener, streamer, promptState.resolve) stays unconditional. Bring sanitizeSenderName to parity with its sibling sanitizeQuotedText by stripping C0/DEL control chars: a crafted display name with \x07/\x1b otherwise reaches the [name] prompt tag. Mirror the same strip in the qqbot send.test.ts mock so a control-char regression in the real helper is caught. Add a diagnostic stderr line when a steer abandons a wedged turn, matching the existing queue-drop log. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): preserve path chars in attachment filePath; trim sender names Addresses ci-bot review on the channel prompt-sanitization paths. filePath over-sanitization (the main regression): a prior hardening pass routed the rendered attachment path through sanitizeQuotedText, which strips the `[`, `]`, and `"` characters. Those are valid, common filesystem path characters (e.g. a Next.js `app/[slug]/page.tsx` route, a quoted segment, or a space in a folder name), so stripping them advertised a `saved to:` path that does not exist on disk and broke the agent's read-file tool. A path rendered alone on its own line cannot use brackets/quotes/spaces to break out of that line, so the only real injection risk is characters that break or reorder the line. Add a path-safe neutralizer, sanitizePromptPath, that strips ONLY C0/DEL controls (incl. CR/LF), the Unicode line/paragraph separators, and bidi overrides (the PROMPT_UNSAFE_INVISIBLES set), preserving everything else byte-intact and without capping length. The human-readable fileName label keeps sanitizeQuotedText (bracket-stripping is fine for a quoted label). steer generation-capture ordering: in steer mode the session generation was snapshotted AFTER the bounded wind-down wait. A concurrent /clear (e.g. another sender's clear-confirm on a shared session) that bumps the generation DURING that wait was therefore invisible: the snapshot read the post-clear value, the dequeue equality guard still held, and the turn ran against the just-cleared session. Capture preSteerGeneration BEFORE the wait and use it for the guard so a /clear during the wait is detected and the turn bails. sender-name fallback: sanitizeSenderName now trims after the length cap and returns an 'unknown' default, so a name made entirely of strippable chars (brackets/newlines) no longer renders an anonymous bracket tag. Both call sites embed the result as a bracket tag with no fallback of their own, so the default lives in the shared helper; the qqbot test mock mirrors the new contract. Adds load-bearing, mutation-checked tests for each change (deterministic, fake-timer driven for the steer/clear race; no real-time waits). Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): stop streamer on steer-cancel and align slash-command trim Two ci-bot review findings on the steer/slash-command paths in ChannelBase. 1. steer-cancel must stop the BlockStreamer, not just cancel. The steer handler flipped active.cancelled but never called stopStreaming(), unlike the sibling doClear path. cancelled alone only suppresses NEW chunks: text already buffered in a wedged turn's streamer is still flushed by the idle timer (~1500ms), which fires before the 3000ms steer wind-down bound, so it leaks into the chat after the replacement turn has begun. Mirror doClear and stop the streamer immediately after cancelling. 2. parseCommand now trims its input so it agrees with isSlashCommand (which already trims). Before, " /help" (leading space, common from IME/copy-paste) made isSlashCommand return true — suppressing the group [sender] tag — while parseCommand returned null, so the command reached the agent unattributed. Trimming closes that attribution gap; it changes nothing else (the no-whitespace path is unaffected, and /tmp/foo still has no handler). Also document the forward-looking late-cancel concern at the steer cancel site: cancelSession is keyed only by sessionId and the replacement prompt reuses it. The shipping AcpBridge sends cancel+prompt over one in-order stdin stream with cancel enqueued first, so the child always processes the cancel before the new prompt — the replacement turn is safe today. A future network/daemon bridge with cancel latency could reorder them; the proper fix is turn-scoped cancellation (a Bridge-contract change), deferred to avoid an API break here. Tests: a steered wedged turn with buffered streamer text invokes stopStreaming so the idle-timer flush can't deliver stale text after the replacement turn begins; " /help" in a group is handled as a command (no [sender] tag, no forward) while /help and /git:commit still parse. Both are mutation-checked. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): block shell commands in shared sessions; dedupe cancel path Phase 0 ships shared/group sessions (sessionScope 'thread' and 'single') but has no per-sender trust model — the [sender] marker is explicitly not a trust boundary. The pre-existing bang (`!`) handler in ChannelBase runs arbitrary host shell commands, so in a shared session ANY participant could run `!rm -rf /`. Gate it with the same isSharedSession predicate used for the destructive-/clear confirm gate: refuse with a one-line notice in shared sessions, keep direct execution in a 1:1 session (the lone user is the operator). isSharedSession is promoted from a closure to a private method so both gates share one source of truth. Extract the duplicated cancel + bounded-wait sequence from doClear and the steer branch into a private cancelAndAwaitActive() helper (the duplication previously caused steer to miss stopStreaming()). steer keeps its pre-wait generation snapshot and uses the boolean result; doClear ignores it. Also: trim the over-verbose steer/clear comment blocks to the load-bearing why; cap sanitizePromptPath at 1024 chars (defense-in-depth) like its siblings; and make the qqbot send.test mock use the real sanitizeSenderName via vi.importActual instead of an inline re-implementation that can drift. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): run onPromptEnd after /clear; only skip it for superseded turns The per-turn `finally` guarded `onPromptEnd` behind `stillCurrent` alone. When `/clear` cancels an in-flight turn with NO replacement it deletes that turn's `activePrompts` entry, so if the cancelled prompt settles late `stillCurrent` is false and `onPromptEnd` was skipped — leaking the platform cleanup several adapters do there (Telegram clears its typing interval, DingTalk recalls the working reaction, Weixin clears typing, Feishu finalizes card state). The `stillCurrent`-only guard was only meant to stop a STEER-superseded turn from clobbering its replacement's indicator, but it wrongly also caught the no-replacement `/clear` case. Distinguish the two: add `ActivePrompt.superseded`, set on the OLD turn only when a steer replacement actually takes over the session slot. The `finally` now runs `onPromptEnd` UNLESS the turn was superseded; the `activePrompts.delete` and the collect-drain stay `stillCurrent`-gated (a superseded turn must not drain the replacement's buffer). Regression + collect-drain tests added with mutation checks. Also addresses review nits: extract a shared command-token regex constant so parseCommand and isSlashCommand can't drift; report `single` scope as "shared channel-wide" in /who (it is shared across all DMs and groups, not just one group); append a truncation ellipsis in sanitizeQuotedText so a cut quote/filename is detectable; and include the message text in the generation-bail drop log so an ignored message is diagnosable. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): require the slash-command token to immediately follow the slash isSlashCommand used trimmed.slice(1).trimStart() before taking the first token, so a space after the slash (`/ foo`) still classified as a command. parseCommand's regex requires the token immediately after `/`, so it returned null for the same input. In a shared group session that divergence suppressed the [sender] attribution (isSlashCommand true) while running no command (parseCommand null), letting `/ foo` reach the agent unattributed — the exact failure the file's comment warns about. Remove the .trimStart() so the token must immediately follow the slash; `/ foo` now splits to an empty first token and is treated as prose, agreeing with parseCommand. Normal commands (/help, /git:commit, /compress-fast, /cmd@bot) are unaffected. Adds an invariant test that isSlashCommand and parseCommand agree on `/ foo` (both false) and /help (both true), plus a behavioral test that `/ foo` keeps its [sender] tag. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): don't clobber a post-/clear turn's indicator; tidy refusal + diagnostics Addresses wenshao's review on PR #5888: - Refusal text: `!` shell commands are "disabled in shared sessions" (drop "group" — isSharedSession is also true for single-scope DMs, which are not groups). - /clear diagnostic: capture cancelAndAwaitActive's result and log on timeout, mirroring the steer "abandoned a wedged turn" message, so a wedged /clear is observable instead of silently "worked". - /clear indicator clobber: a /clear that evicts a wedged turn never set `superseded`, so the turn's late-settling finally still ran onPromptEnd and killed the indicator a turn started AFTER the /clear now owns. Add a `clearEvicted` flag: /clear runs the wedged turn's own onPromptEnd at eviction time (no replacement exists yet) and marks it clearEvicted; the late finally then skips onPromptEnd. onPromptEnd now fires exactly once for the evicted turn, at clear-time. The finally guard becomes `stillCurrent || (!superseded && !clearEvicted)`. ActivePrompt carries the originating chatId/messageId so the eviction can target the right indicator. - Dropped-queued-turn log: sanitize the attacker-controlled message text (render newline visibly, strip C0/DEL incl. CR/ESC) before it reaches an operator's terminal, matching every other embed path. Tests: complete the single-scope DM `!` refusal test (not-called + length + not-forwarded); update the group/DM/1:1 refusal assertions to the new text; re-point the "/clear settles late" test to clear-time cleanup; add a replacement-after-/clear test asserting a late settle does not end a later turn's indicator; strengthen the dropped-turn-log test with control-char input. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): fix root tsc errors in channel tests; gate /who; sanitize dingtalk nick log Root `tsc --noEmit` failed on 9 type errors in two channel test files that per-package builds and `--workspace` typechecks miss (both exclude test files). Fix the test types only — no production type changes: - qqbot/dingtalk: `QQChannel`/`DingtalkChannel` come from `await import()` as values, so annotate instances with `InstanceType<typeof X>`. - dingtalk test config was missing the required `token` field of `ChannelConfig`. - qqbot mock: bracket-access the index-signature `router` and type the assignment so `{}` is assignable to `Record<string, unknown>`. /who: gate it to authorized senders in shared sessions, mirroring /clear — /who leaks the workspace basename, so non-members shouldn't see it. dingtalk: sanitize `senderNick` with the shared `sanitizeSenderName` before writing it to stderr, so a crafted nick with CR/LF/control chars can't fragment or inject log lines. !-shell gate: refuse `!` shell commands in ALL groups (gate on `envelope.isGroup`, not just shared sessions) — a user-scope group is not a shared session yet is still multi-operator, so members could reach the host shell. The refusal now runs BEFORE router.resolve, so a refused command never creates a session. Single-scope DMs stay refused. tests: the qqbot/dingtalk suites mock `@qwen-code/channel-base` and pull the real `sanitizeSenderName` via the package export, which resolves to base/dist and broke clean package-local runs. Alias the specifier to base's source in each vitest config (mirrors cli) so the suites run without a prior `tsc --build` of base. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): bump generation and release indicator on steer-abandon, mirroring /clear When a steer abandons a wedged turn it re-seeds a fresh queue chain but, unlike doClear, did neither of the two protections doClear runs on a wedged-turn eviction. Both bugs surface when mixed dispatch modes collapse onto one session (single/user scope). FIX 1 (generation bump): a followup queued behind the wedged turn stays on the now-orphaned chain. Without bumping sessionGenerations, when the wedged turn late-settles the followup PASSES the dequeue guard and runs the unguarded activePrompts.set, clobbering the live steer replacement -> two concurrent bridge.prompts on one session (duplicated responses + double tool execution). Bump the generation up-front (skipping if a /clear raced the wind-down wait) and advance the replacement's captured snapshot so it proceeds while the orphaned followup bails. FIX 2 (messageId-scoped indicator leak): the supersede path never released the abandoned turn's OWN indicator. A CHAT-scoped indicator is re-seeded by the replacement, but a MESSAGEID-scoped one (per-message reaction/card keyed on the inbound messageId) is keyed on the abandoned turn's messageId and leaks until disconnect. Run the abandoned turn's onPromptEnd at steer-time and mark it superseded so its late finally still skips onPromptEnd (released once, no double-fire, replacement untouched). Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): steer waits for the running turn instead of racing a concurrent replacement In steer dispatch, when a new message arrived while a prompt was running, ChannelBase cancelled the running turn (best-effort), did a BOUNDED wait, and on TIMEOUT (the old turn wedged, not finished) PROCEEDED to start a replacement bridge.prompt() on the SAME sessionId while the old prompt was still active. As wenshao flagged, this is bridge-unsafe: - DaemonChannelBridge.prompt() REJECTS while the prior prompt is still marked active, so the replacement is silently dropped. - Both bridges collect/emit chunks keyed by sessionId ONLY, so the abandoned turn's late chunks mix into the replacement's stream (duplicated/stale output). Fix (wenshao option (a)): steer now best-effort cancels the running turn and CHAINS the new turn onto the existing session queue tail, so it runs only AFTER the old turn's finally has actually run (onChunk detached, activePrompts cleared, indicator released). The cancel stays — it makes the old turn wind down sooner — but steer never proceeds while a turn is still active, eliminating the unsafe concurrency entirely. Steer now differs from followup only by that best-effort cancel. Removed the steer-only concurrency machinery that only existed to host a concurrent replacement: the steer-path bounded wait (cancelAndAwaitActive call), the steerWedged flag and fresh-chain re-seed, the preSteerGeneration capture, the steer-time sessionGenerations bump, the steer-time onPromptEnd, and the superseded flag (plus its finally guard term). /clear's OWN protections — its up-front generation bump, eviction-time onPromptEnd, and the clearEvicted finally guard — are SEPARATE and preserved; /clear genuinely evicts and still needs them. cancelAndAwaitActive is retained, now used only by /clear. BEHAVIORAL CHANGE: steer no longer force-interrupts a genuinely wedged turn; it cancels it and waits for it to finish before the new turn runs. Turn-scoped cancellation/routing (so a new turn can run without waiting for a wedged predecessor) is wenshao option (b) — it needs an API change across every adapter and is the deferred enhancement, out of scope here. Folded in two further #5888 review items: - [Critical] QQ slash-command audit-log injection (qqbot/src/QQChannel.ts): the audit process.stderr.write interpolated the RAW, attacker-controlled senderName (event.author.username) and cleanText BEFORE sanitization, so a crafted nick or message with CR/LF/ANSI escapes could forge or corrupt operator audit logs. Hoisted `safeName = sanitizeSenderName(senderName)` above the audit log and now log a neutralized command string (cap 80, render \n visibly, strip C0/DEL), mirroring ChannelBase's dropped-turn log and the DingTalk hardening already in this PR. The prompt-path sanitizeSenderName usage is unchanged. - [Suggestion] single-scope DMs were not attributed (base/src/ChannelBase.ts): the [sender] prefix was gated on envelope.isGroup alone, but sessionScope:'single' collapses every sender's DM into one __single__ session (already treated as shared by the !-gate, /clear confirm and /who), so different people merged into one unattributed conversation (the RFC-R4 gap Phase 0 closes). The gate is now (envelope.isGroup || sessionScope === 'single') — deliberately NOT isSharedSession, which is false for user-scope groups that must keep attribution. Tests: removed the obsolete steer-concurrency cases and added cases for the new steer behavior (new turn starts only after the old completes; the abandoned turn's late chunks cannot reach the new turn). Added an audit-log sanitization test (QQ) and single-scope-DM / user-scope-group / 1:1-DM attribution tests (base). /clear's eviction protections are unchanged and still pass. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): neutralize NEL/C1 in prompt text; dedupe shared-session auth gate Address wenshao's review on the Phase-0 qwen-tag channel base. - sanitize: PROMPT_UNSAFE_INVISIBLES now also neutralizes the C1 control block (U+0080-U+009F), which includes NEL (U+0085), a Unicode line break (UAX#14 BK) that renders as a new line. Without it a display name like "Alice<NEL>system: ..." or a crafted reply quote could inject a prompt line. Applies to both sanitizeSenderName and sanitizeQuotedText (and the shared path sanitizer) since they share the set. Tests cover NEL + a second C1 (CSI U+009B) for sender names and quoted text; mutation-checked. - ChannelBase: the two best-effort cancelSession() calls (the /clear wind-down wait and the steer pre-cancel) no longer swallow the IPC failure with an empty catch. They now log channel name + sessionId + reason to stderr, matching the existing /cancel log style, so a wedged turn is diagnosable. Still fire-and-forget (not awaited). - ChannelBase: extract the verbatim shared-session authorization gate shared by /clear and /who into a private isAuthorizedForSharedSession() predicate (isSharedSession + allowedUsers check). Behavior is unchanged; each caller keeps its own rejection wording. - tests: add a GROUP-path case that /help@mybot is treated as a command (no [sender] prefix, not forwarded to the agent); the existing @botname test only covered a DM. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): strip C1/NEL from audit-log text too, matching the prompt path The prompt path now neutralizes the C1 control block (U+0080-U+009F, incl. NEL U+0085, a Unicode line break) via PROMPT_UNSAFE_INVISIBLES, but the inline stderr audit-log sanitizers still stripped only C0/DEL. A crafted message carrying NEL/C1 therefore survived into the dropped-/queued-turn log (ChannelBase) and the QQ slash-command audit log, where many terminals render NEL as a newline, forging an extra [channel] log line. Extend both inline strips to also cover the C1 block (C0 + DEL + C1), matching the prompt path's union, and refresh the now-stale comments. Tests feed a NEL (U+0085) and a C1 char (U+009B) through both log paths and assert neither survives; reverting the strip to C0/DEL fails the new NEL assertions. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): gate /status and /cancel in shared sessions; code-point-safe truncation Three follow-ups to the shared-session authorization work: - /status: add the same isAuthorizedForSharedSession gate /who uses, so a non-member of a shared session with a non-empty allowedUsers list can no longer read its session/access state. Non-shared (DM/per-user) is unchanged. - /cancel: gate the destructive abort behind isAuthorizedForSharedSession like /clear (auth gate only, no confirm), so a group member can't cancel another user's in-flight turn on a shared session. 1:1 and authorized users unchanged. - sanitize: truncate sanitizeSenderName/sanitizeQuotedText/sanitizePromptPath on Unicode code-point boundaries (Array.from) instead of UTF-16 code units, so a cap landing mid-surrogate-pair (e.g. an emoji) can't leave a lone surrogate that renders as the replacement character downstream. The ellipsis logic in sanitizeQuotedText still keeps the result within maxLen code points. Adds tests for each (incl. mutation-checked gates and an emoji-at-cap case). Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): audit-log shared /clear and blocked ! shell; test steer stopStreaming Address ci-bot review on PR #5888: - [Observability] Emit a stderr audit line on a successful clear of a SHARED session (channel, sessionId, sanitized sender + stable senderId). A 1:1 DM clear is single-participant and is not logged. - [Observability] Emit a stderr audit line when a group/shared member's `!` host-shell command is refused, so operators can detect blocked attempts. Sender name is sanitized; the command payload is not echoed. - [Test gap] Add a steer test asserting the best-effort cancel calls active.stopStreaming (spying on the running turn's prompt), plus tests for both new audit lines and the DM no-log case. The /cancel and /status shared-session auth gates flagged by ci-bot were already present on this branch and are left unchanged. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): gate steer-cancel by authorization; close audit-log + attribution injection gaps Address review on PR #5888 (qwen-tag-phase0): - [Critical] The /cancel auth gate was bypassable via the default `steer` dispatch mode: any normal message cancels the running turn, so an unauthorized member of a shared session - blocked from /cancel - could abort another user's active turn just by sending a message. The steer branch now checks isAuthorizedForSharedSession FIRST and, when unauthorized, breaks to normal queuing (the message chains onto the session queue tail and runs AFTER the active turn instead of aborting it). - [Suggestion] Audit-log sanitizers missed U+2028/U+2029 and the bidi overrides (log-line spoofing / trojan-source). Exported PROMPT_UNSAFE_INVISIBLES and added a shared sanitizeLogText(text, maxLen) helper that applies BOTH that set AND the C0/DEL strip (and caps length), used at both audit-log sites (ChannelBase dropped-turn log + QQ slash-command audit log) so the defense can't drift apart. - [Suggestion] The [sender] attribution prefix was suppressed for any command-SHAPED text, so unrecognized "/x\n[SYSTEM]: ..." reached the agent unattributed. Added a synchronous isRecognizedCommand() (locally registered commands + the bridge.availableCommands snapshot) and now suppress the prefix only when the text is BOTH a command shape AND a recognized command; unrecognized command-like text keeps its [sender] tag. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): recognize command aliases per-session so attribution doesn't break them isRecognizedCommand decides whether to suppress the group [sender] attribution prefix, but matched only against availableCommands[].name. The ACP parser also accepts command ALIASES (parseSlashCommand altNames), so a valid alias like /summarize (alias of /compress) or /login (alias of /auth) was classified as unrecognized, rewritten to "[Alice] /summarize", and then run as PLAIN CHAT (the leading / is gone) instead of executing. It also read the bridge's GLOBAL availableCommands snapshot, which in DaemonChannelBridge can belong to another session. - Carry aliases through the available-command contract: add an optional altNames?: string[] to AvailableCommand, emit them on the wire in _meta.altNames (ACP's extension point; a top-level altNames would be an excess-property error against the SDK type), and lift them in both bridges via readAvailableCommandAltNames. Omitted when absent, so alias-free entries stay byte-identical. - Match name AND altNames, against THIS session's command list: isRecognizedCommand now takes sessionId and reads getAvailableCommands(sessionId) when the bridge exposes it (DaemonChannelBridge), falling back to the global getter (AcpBridge, single-agent, inherently session-correct). Stays synchronous: a real command sent before the snapshot loads keeps its tag (safe default). - Security intent intact: genuinely-unrecognized command-shaped text (e.g. /x\n[SYSTEM]: ...) still keeps its [sender] tag. Also fold in two #5888 review items in ChannelBase.ts: - Steer auth gate: audit the silent steer->queue downgrade to stderr for an unauthorized member (operator-visible only; no per-message reply), matching the /cancel,/clear,/who,/status gates' observability. - /clear eviction: a clear-time onPromptEnd that throws would abort the purge, leaving the evicted turn in activePrompts so its late finally (stillCurrent || !clearEvicted) re-runs onPromptEnd and clobbers a newer turn. Set clearEvicted first and catch+audit the throw so the purge always runs (turn becomes non-current) and the late finally skips. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): match agent commands case-sensitively; add steer wedge watchdog isRecognizedCommand lowercased the token before comparing it to agent command names/aliases, but the CLI's parseSlashCommand matches agent commands CASE-SENSITIVELY (`cmd.name === part`, `cmd.altNames?.includes(part)`). So a wrong-case token like `/SUMMARIZE` was "recognized" here — suppressing the `[sender]` attribution tag — yet ran NO command in ACP, which then forwarded the raw text unattributed, reopening the injection where a crafted `/SUMMARIZE\n[SYSTEM]: …` second line reaches a shared group as an apparent system directive. parseCommand now also returns the typed-case `raw` token; isRecognizedCommand matches AGENT commands on `raw` (case-sensitive, mirroring the CLI) while LOCAL commands keep their existing case-INSENSITIVE match (registerCommand lowercases the stored name; handleInbound dispatches by the lowercased token). The steer chain-and-wait path lost the old "abandoned wedged turn" log, so a hung predecessor bridge.prompt() could silently deadlock the session with no observability. Add a diagnostic-only watchdog: the steer branch arms a timer (unref'd) that, if the predecessor is still the active prompt after CLEAR_CANCEL_TIMEOUT_MS, emits a stderr line pointing at /clear for recovery; the chained `.then()` disarms it as its first statement once the predecessor's tail resolves. No concurrency change — chain-and-wait is untouched. Also simplify the late-finally onPromptEnd guard from `stillCurrent || !clearEvicted` to `!clearEvicted`: clearEvicted is set ONLY by /clear's eviction, which then unconditionally deletes activePrompts (its try/catch around the clear-time onPromptEnd guarantees the purge even if it throws) and never re-inserts the same promptState, so `clearEvicted` implies `!stillCurrent` and the dropped term was unreachable. Tests: wrong-case `/SUMMARIZE` keeps `[sender]` (with a mutation note); wrong- case local `/HELP` still dispatches locally; fake-timer watchdog tests for the wedged (logs) and settled (timer cleared, no log) paths. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): align agent command recognition with parseSlashCommand; guard finally onPromptEnd Address maintainer review on the qwen-tag Phase 0 channel adapter. - [Critical] Guard the normal-completion onPromptEnd in the per-turn finally. onPromptEnd runs platform-adapter cleanup (typing interval, working-reaction recall, card finalize) - network/IO that can throw. An uncaught throw skipped activePrompts.delete (session leak), promptState.resolve (active.done never settled, so a later /clear falsely logged "abandoned a wedged turn"), and the collect-buffer drain - and the rejection, swallowed by the queue tail's .catch(() => {}), silently dropped every later turn. Wrap it in try/catch with a stderr log, matching the eviction-path treatment. - [Critical] End the channel/agent command-recognition divergence by matching the AGENT branch of isRecognizedCommand EXACTLY as the CLI's parseSlashCommand does: the FIRST whitespace token after the leading '/', case-SENSITIVELY, WITHOUT stripping an @suffix. parseCommand's '@'-stripped, lowercased token diverged from the agent (PARSE_COMMAND_RE drops '(?:@\S+)?'), so /compress@x or /Compress were "recognized" here (tag suppressed) yet ran no command there and reached the model unattributed. Matching the exact token leaves wrong-case / @suffix / injection- shaped tokens UNRECOGNIZED -> they keep their [sender] tag (attributed), exactly as the agent treats them. No prompt rewrite: a /compress@otherbot aimed at another bot must not run here. LOCAL commands keep case-insensitive dispatch. - [Suggestion] Include the originating chatId/messageId (sanitized) in the "/clear abandoned a wedged turn" log so oncall can correlate the stuck turn. - [Suggestion] Clarify the alreadyPrefixed JSDoc: it is also set by the QQ adapter on real self-prefixed inbounds, which sanitize the embedded name at the source - so the flag does not bypass sanitization (verified; no behavior change needed). - [Suggestion] Replace the blind `as unknown as {...}` bridge cast on the recognition path with a typed AgentCommandsProvider interface (optional members), so a future rename/return-type change is type-checked instead of breaking at runtime. - [Suggestion] Validate altNames' shape: isAvailableCommand now rejects a non-array altNames, and the recognition site guards the alias check with Array.isArray, so a malformed wire payload can't throw at the `.includes` call. Tests: throwing-onPromptEnd cleanup + collect-drain + log; exact-token recognition (verbatim /compress and /summarize alias; tag kept for /SUMMARIZE, /COMPRESS, /compress@x, and /compress@x + [SYSTEM] line); wedged-turn log carries chat/message; malformed altNames does not throw; isAvailableCommand drops a malformed-altNames entry. Restored stderr spies in the two steer-watchdog tests' finally. Co-Authored-By: Qwen-Coder <[email protected]> * fix(channels): address qwen tag review followups --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
aa8f9bb993 |
fix(standalone): Route serve shim through cli-entry (#5977)
* fix(standalone): route serve shim through cli-entry Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5977) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
481ea7b1b1 |
docs(qc-helper): add daemon mode docs and fix system settings path (#5981)
Add qwen-serve.md and qwen-serve-deploy-local.md to the /qc-helper documentation index so the skill can locate daemon-mode docs at runtime. Also expand the Config File Locations quick reference to include the Linux and Windows system settings paths, matching what settings.md already documents. Co-authored-by: Claude <[email protected]> |
||
|
|
98dec4fa71 | fix(release): use relative postinstall patch dir (#5973) | ||
|
|
68a9de4752 |
fix(core): only spawn memory recall when auto-memory is enabled (#5963)
Co-authored-by: Minerest <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
d5a9027213 |
fix(cli): auto-select custom input on Enter in multi-select questions (#5791)
In ask_user_question multi-select mode, pressing Enter on the custom
input ("Type something...") did nothing — the Enter handler returned
early and handleCustomInputSubmit only toggled the checkbox without
submitting. Now Enter on custom input auto-selects the typed value
(if non-empty) and advances to the next question or submits. Empty
custom input is guarded to prevent accidental submission.
TextInput's onSubmit prop is the actual submission path when the
custom-input row is focused (the Enter handler in useKeypress is
short-circuited by the isCustomInputSelected guard). handleCustomInputSubmit
now calls handleMultiSelectSubmit() after toggling, but only when the
typed value is non-empty to avoid submitting pre-toggled options
unexpectedly.
Signed-off-by: Alex <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 易良 <[email protected]>
|
||
|
|
ee375c6a41 |
feat(loop): inject a .qwen/loop.md task file at fire time via sentinels (#5890)
* feat(loop): inject a .qwen/loop.md task file at fire time via sentinels A long-running /loop had no durable place to keep its task list — the model re-stated the work every tick. This adds a .qwen/loop.md file the loop re-reads and injects at fire time, driven by a sentinel prompt (<<loop.md-dynamic>> for self-paced LoopWakeup, <<loop.md>> for fixed-interval CronCreate). At fire time the sentinel expands into the full task block (first delivery, after the file changes, or after a compaction) or a short reminder (unchanged), so the list is paid for once into the cached prefix and later ticks stay cheap. Change-detection is content equality, and the cache is committed only after the tick is delivered so an aborted tick can't leave a dangling reminder. The reader is workspace-confined (realpath boundary), 25 KB capped, and skips empty/missing/symlinked candidates. Interactive (Session) wiring covers self-paced and fixed-interval loops; headless skips a bare sentinel (full headless expansion is a follow-up). The loop.md-absent branch is a minimal no-op tick. Closes #5889 Co-Authored-By: Qwen-Coder <[email protected]> * fix(cli): remove unused loop mode import * fix(loop): rename loop task-file/tick-resolver modules to kebab-case The repo's eslint.config.js enforces `check-file/filename-naming-convention` with `{ '**/*.ts': 'KEBAB_CASE' }` over `packages/core/src/**/*.ts`, so the camelCase module names (loopTaskFile.ts, loopTickResolver.ts and their tests) failed the lint gate and blocked merge. Rename them to kebab-case via git mv (history preserved) and update the relative import/export paths in the package barrel and the renamed files. Exported symbol identifiers are unchanged, so consumers importing from @qwen-code/qwen-code-core need no update. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): address re-review suggestions on loop.md injection - loop-task-file: log a debug trail when a project loop.md is skipped for escaping the workspace via a symlink (control flow unchanged). - loop-task-file.test: cover the non-whitelisted fs-error rethrow (EACCES) by mocking readFile so the assertion stays cross-platform stable. - loop-tick-resolver: SHORT_ABSENT now names both the project (.qwen/loop.md) and home (~/.qwen/loop.md) candidates that are checked. - loop-tick-resolver.test: assert tick.sourcePath on the unchanged branch. - index: move the two loop barrel exports from Services into the Skills section to restore grouping/ordering (consumers import via the barrel). Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): address loop.md re-review — bounded read, cached realpath, single H1, reset cache on absence - read at most LOOP_TASK_FILE_MAX_BYTES+1 via fs.open so a huge/malicious .qwen/loop.md is no longer fully read+decoded each tick (DoS hardening) - cache the project-root realpath once per resolver instead of per tick - emit a single H1 and avoid leaking the absolute path into the model prompt - clear lastContent on the absent path so delete/recreate re-expands the block Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): address re-review — heading test, absent heading, source label - Session.test.ts: fix the stale assertion to the current "# /loop tick — loop.md tasks from" heading (was the pre-rename "# /loop tick — tasks from") — the likely CI failure. - loop-tick-resolver: route the absent case through tickHeading() so every tick variant (full block, short reminder, absent) shares one heading and the dynamic-pacing suffix; emitted text is unchanged. - loop-task-file: return a semantic `source` ('project' | 'home') for a found loop.md; the resolver maps it to a label via an exhaustive Record, so a future candidate can't silently mislabel as "home loop.md". Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): keep loop.md confinement root internal; harden bounded-read test Address re-review on the loop.md injection change: - loop-task-file: drop `realProjectRoot` from the exported ReadLoopTaskFileOptions. The module is re-exported from @qwen-code/qwen-code-core, so a caller could pass a stale/broader root and widen the workspace-confinement boundary for project loop.md. The real project root is now derived from the trusted `projectRoot` inside readLoopTaskFile and cached per-root (kept off the public API), so callers can't supply a different confinement root. The resolver drops its private realpath cache accordingly. - loop-task-file.test: make the bounded-read test load-bearing. It now wraps the file handle to observe read() calls and asserts no read — and their sum — exceeds the cap budget (LOOP_TASK_FILE_MAX_BYTES + 1), plus a short-file EOF case. It fails on a "read the whole file, then slice" regression that the previous content-only assertion let pass. Co-Authored-By: Qwen-Coder <[email protected]> * test(core): cover loop.md project-root realpath cache eviction on transient failure Add a load-bearing test proving a transient fs.realpath(projectRoot) failure (EACCES/ENOENT) is not pinned in the per-process cache: the entry is evicted on rejection so the next tick re-resolves instead of replaying a permanently-cached rejection. Without that eviction, a single transient error would break loop.md resolution for that root forever. The test goes red if the `.catch(() => realProjectRootCache.delete(...))` eviction line is removed and green once restored; driven purely via the realpath mock, with no timing waits. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): harden loop.md against symlink exfiltration, FIFO hang, and untrusted reads Addresses the security review on PR #5890. - loop-task-file: the project `.qwen/loop.md` is now lstat'd BEFORE the blocking open. A symlinked project file is refused outright — a repo-controlled `.qwen/loop.md -> ../.env` resolves inside the workspace, so the realpath confinement alone would pass and exfiltrate that file to the model. A FIFO/socket/device/dir is refused too, so a named pipe can no longer wedge the tick on a blocking `open` that waits for a writer. The ancestor-symlink confinement (`.qwen -> /outside`) is preserved. The home `~/.qwen/loop.md` now follows symlinks (a legitimate dotfiles setup) but stat's the resolved target and requires a regular file, and gained the debug-skip trace the project path already had. - Session/resolver: gate the project loop.md on folder trust. An untrusted folder no longer reads the repo-controlled project file; the user-owned home file stays allowed. Threaded `allowProjectFile` (from `config.isTrustedFolder()`, mirroring getProjectHooks()) through LoopTickResolver into readLoopTaskFile. - nonInteractiveCli: headless cron leak. Skipping a loop sentinel now also deletes a recurring SESSION (non-durable) loop.md job so it stops re-firing and `sessionSize` can fall to zero — otherwise the hold-open never resolves and the run hangs. Durable jobs are left untouched (they persist and don't count toward sessionSize); one-shots are already removed before fire. - Session debug log: emit the non-absolute source label instead of the absolute `sourcePath`, so the resolved path isn't leaked to logs. Tests: project in-workspace-symlink exfiltration guard, FIFO-before-open (no hang, proven by open never being called on the project path), home symlink-to-regular-file, untrusted-folder gating (resolver + Session), absent-tick Session integration, and the headless recurring-session cleanup. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): bracket-access process.env in Session test setFakeHome `noPropertyAccessFromIndexSignature` (root tsconfig) rejects dot access on the `NodeJS.ProcessEnv` index signature, so the dot-notation `process.env.HOME` / `process.env.USERPROFILE` reads and writes in the `setFakeHome` helper failed `tsc --noEmit` with 4x TS4111, breaking CI (packages/cli includes *.test.ts in its typecheck). Switch them to bracket access to match the existing restore loop. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): fail-secure loop.md default + root confinement, with coverage Address qwen-code-ci-bot review on the loop.md injection change: - readLoopTaskFile's `allowProjectFile` now defaults to false. It is re-exported from the core barrel, so a caller that omits the option must not silently read an untrusted workspace's repo-controlled `.qwen/loop.md`. The real caller (Session via LoopTickResolver) already passes the trust-derived value explicitly, so trusted-folder behavior is unchanged. - Fix a workspace-confinement false positive at a filesystem root: when realRoot is `/` (or `C:\`), `realRoot + path.sep` became `//` (`C:\\`), which no descendant startsWith, so running from the root wrongly refused every project loop.md. Derive the prefix without double-appending the separator and allow `real === realRoot`. - Add coverage: the dynamic loop.md sentinel is cleaned up headlessly like the cron one (guards against a `=== LOOP_SENTINEL_CRON` regression); an aborted (undelivered) tick followed by an edit re-delivers the full new block (the #pendingContent vs #lastContent divergence); a project loop.md directly under a filesystem root is not falsely refused; and the new fail-secure default is locked in. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): harden loop.md reader/resolver per CI review Follow-up hardening on the loop.md task-file pipeline: - loop-task-file: add ELOOP/ENAMETOOLONG to the skip whitelist so a symlink-loop or over-long loop.md path is treated as a skippable candidate instead of crashing the tick (EACCES still surfaces). - loop-task-file: confine the home ~/.qwen/loop.md symlink target to $HOME via realpath, blocking out-of-home escapes (e.g. -> /etc/passwd) while still allowing in-home dotfile symlinks. Shared prefix-confinement helper now used by both the project and home candidates. - Session: echo the relative sourceLabel (not the absolute sourcePath) to the ACP client so the OS username / dir layout isn't leaked into the UI. - loop-tick-resolver: drop the contradictory "established earlier" reminder on the first full delivery (the block is in the same message); keep the mode-specific pacing suffix on both full and short ticks. - loop-tick-resolver: document the intentional cut > 0 boundary in cutToLastNewline (a position-0 newline keeps the body rather than emptying it). Tests: ELOOP skip, home-symlink escape vs in-home target, home non-regular-file skip, position-0 newline, first-delivery wording, and a Session-level assertion that an auto-compaction resets the resolver cache so the next unchanged tick re-expands. All new tests mutation-checked. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): address CI review on loop.md reader/resolver - loop-task-file: at the byte cap, drop a still-incomplete trailing UTF-8 sequence (walk to the last lead byte; cut before it when its declared width runs past the cap) so a mid-/malformed multi-byte char can't leave an orphan lead that decodes to a trailing U+FFFD. The byte-length re-clamp is now a pure safety net rather than the load-bearing step. - loop-task-file/resolver: scope the boundary realpath cache to the LoopTickResolver instance (fresh per /cd rebuild, cleared by resetCache) so a long-lived process can't pin a stale boundary after a symlink re-point; direct barrel callers keep the process-lifetime fallback. Eviction-on-failure is preserved. - nonInteractiveCli: log the headless cleanup of a recurring session loop.md cron, and guard the discarded delete() promise with .catch() so a future async delete can't become an unhandled rejection. - tests: cover the incomplete-trailing-char cap, the ENAMETOOLONG skip branch, and resetCache invalidating the boundary realpath cache. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): re-evaluate folder trust per tick; guard durable loop.md in headless Address review on the loop.md reader/resolver: - Session resolver takes a folder-trust getter and re-reads it on every tick instead of capturing it once at construction. isTrustedFolder() is not process-stable in IDE sessions (a workspace-trust update can flip it), so a trusted->untrusted flip now stops reading the repo-controlled .qwen/loop.md immediately rather than serving it from a stale snapshot. - Headless durable cron: a durable <<loop.md>> sentinel cannot be expanded in a headless run, so the scheduler skips such durable jobs before processJob stamps lastFiredAt (new setSkipDurableFire guard, honored by the tick loop and the catch-up/final delivery paths). Previously the tick was marked fired and persisted while the work was skipped downstream, silently consuming a tick the owning interactive session should run. - Drop the absolute sourcePath from LoopTickResult; sourceLabel already serves the UI label and the presence check without leaking an absolute path. - Size the loop.md read buffer to the file (stat.size + 1) instead of always allocating the 25 KB cap, keeping the bounded-read and truncated-flag semantics intact. - Add one happy-path debug log on a successful loop.md read (source label, byte count, truncated flag) so oncall can confirm pickup. - Add load-bearing tests for the 2-byte and 3-byte UTF-8 width branches. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): honor skipDurableFire in deliverPending missed branch The headless durable-cron guard skipped fire+persist for catch-up and final deliveries but not the missed branch: a missed one-shot <<loop.md>> sentinel was unconditionally notified AND removed from disk, permanently losing the task though no consumer ran the loop.md work. Partition the missed batch on the same predicate — skipped one-shots are neither notified nor deleted (left on disk for the owning interactive session), while co-missed runnable one-shots still fire (batched) and are removed. Tests: - cronScheduler.test.ts: cover deliverPending missed/catch-up/final with a skip predicate — sentinel not fired/persisted, sibling fired+persisted; the missed-branch test mutation-locks the fix above. - loop-task-file.test.ts: isWithin sibling-prefix confinement — root /ws/foo must refuse a realpath under sibling /ws/foobar; fails if isWithin loses the trailing path.sep (bare startsWith). Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): address loop.md injection review (durable guards, logs, QWEN_HOME) Address 5 review suggestions on the loop.md sentinel pipeline: - cronScheduler.deliverPending: align the catch-up and final skip guards with the tick loop by adding the `job.durable &&` predicate, so a non-durable job can never be skipped by skipDurableFire. - cronScheduler: add a debugLogger.debug at each of the 4 durable-skip points (tick/missed/catch-up/final) so a deferred durable loop.md tick is diagnosable. - Session: tag a loop.md sentinel-resolution failure distinctly from a model-call failure (sentinel mode + error code, no absolute path) before the shared cron catch surfaces it. - Session.test: add a tick test that delivers the full block then a SHORT REMINDER on an unchanged second tick (full:false + sourceLabel). - loop-task-file / Session: resolve the home/global loop.md from the QWEN_HOME-aware global dir (Storage.getGlobalQwenDir) instead of raw os.homedir(), keeping the home confinement from the earlier fix. Adds reader + Session coverage. Co-Authored-By: Qwen-Coder <[email protected]> * test(loop): cover all-filtered missed batch and working-dir resolver rebuild Address PR review on the loop.md injection work with two load-bearing test additions: - cronScheduler: a 'missed' batch where every durable task is a loop.md sentinel filtered by skipDurableFire fires nothing and never calls removeMissedFromDisk (the runnable.length > 0 guard), so the owning interactive session still surfaces the preserved tasks. - Session: changing getWorkingDir between ticks rebuilds the loop.md resolver for the new root, resolving the new root's loop.md and never re-serving the old root's content. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): report real home loop.md path in absent tick; cover resolve() error path The absent loop.md reminder hardcoded `~/.qwen/loop.md (home)`, which is wrong once $QWEN_HOME relocates the global dir — the resolver checks `<homeQwenDir>/loop.md`. Build the absent body from the resolver's real homeQwenDir (OS-home tilde-abbreviated so the unrelocated case still reads `~/.qwen/loop.md`); keep the project label relative. Also add a Session test that a `<<loop.md>>` sentinel whose resolve() throws (EACCES) propagates into the cron catch — surfacing as a cron error, never degrading to a default tick sent to the model — and logs the loop.md-tagged warn. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): sanitize loop.md resolve error and guard handle close The cron catch forwards a sentinel-resolution error's .message verbatim to the ACP client via emitAgentMessage. Re-throwing the raw Node fs error leaked its absolute loop.md path (OS username + dir layout) to the client/ API. Re-throw a sanitized error carrying only the relative candidate labels + errno code; keep the full detail (absolute path included) in the local debug warn. Also guard handle.close() in readBoundedTaskFile's finally: a close failure (e.g. EBADF) would otherwise replace an in-flight read/stat error (e.g. EIO) and mask the real cause. Swallow the close error (debug-log only) so the original propagates; the close is still attempted. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): align skip guards, resolve sentinel pendingRemoval limbo, QWEN_HOME-aware absent/error labels Follow-ups on the loop.md-injection review: - cronScheduler: add the `job.durable &&` guard to the missed-branch skip so all four skip sites (missed/catch-up/final/tick) read identically. - cronScheduler: a skipped sentinel stayed on disk yet lingered in pendingRemoval, stranding it out of both the job map and disk reconciliation. Clear it from pendingRemoval (missed + final branches) so the next load re-installs it — the intended "defer to the owning session" path. Tests assert the skipped sentinel is not left in pendingRemoval. - loop-tick-resolver: absentBody no longer claims the project candidate was checked when allowProjectFile is false (untrusted folder) — it names only the home path actually read. Tests updated. - Session: the sanitized resolve-error reused a hardcoded ~/.qwen/loop.md; reuse the resolver's QWEN_HOME-aware homeLoopLabel() (now public) so a relocated global dir is reported accurately, still leak-safe. Test added. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): never leak an absolute $QWEN_HOME path in the home loop.md label homeLoopLabel() built the model-facing home loop.md label via tildeifyPath(join(homeQwenDir, 'loop.md')). homeQwenDir honors $QWEN_HOME, which may point OUTSIDE $HOME (a supported relocation, common in containers/CI). tildeifyPath only abbreviates $HOME-prefixed paths, so for a $QWEN_HOME outside $HOME it was a no-op and the raw absolute path flowed through absentBody() and the sanitized resolve-error into model/API text, leaking the host's filesystem layout. Label the three cases without ever emitting an absolute path: - under $HOME -> tilde-abbreviated ~/.qwen/loop.md (unchanged); - relocated via $QWEN_HOME -> the literal $QWEN_HOME/loop.md (env-var name, not the resolved dir), by swapping the resolved prefix; - any other out-of-$HOME dir -> a generic placeholder. The real absolute path stays in LOCAL debug logs only. Two existing tests codified the leak (asserting the absolute path appeared); fixed and added a privacy test (label + absent-tick modelText use the placeholder, never the absolute path). Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): guard UTF-8 truncation boundary and scope resolve-error to checked candidates When a loop.md task file is byte-capped on a UTF-8 boundary, the trailing back-off only dropped a still-INCOMPLETE lead (`lead + width > end`). It missed the symmetric case where a complete character is followed by stray orphan bytes (`lead + width < end`), and it ran only once. Because `lead` is found by walking back over continuation bytes, an ASCII byte between an orphan lead and the cap (e.g. `c3 41 80 80 80 61`) stopped the walk early: the orphan `c3` and the stray `80` continuations survived and decoded to trailing U+FFFD that the byte-length re-clamp cannot remove. Replace the single check with a re-checking loop that keeps a trailing unit only when its declared width reaches `end` exactly, and re-checks the new tail since several malformed units can stack. Adds a mutation-checked test using the orphan-lead repro. Also make the sentinel resolve-error path trust-aware. The sanitized error always named `.qwen/loop.md (project)`, even for an untrusted folder where the project candidate is never read — the same false claim the absent reminder already avoids. Introduce LoopTickResolver.absentLocations(projectChecked) as the single source of truth for the checked-candidate "where" string (project named only when actually read, QWEN_HOME-aware never-absolute home label) and use it from both absentBody and the Session throw, so the two messages cannot drift. Adds a Session test asserting an untrusted folder omits `(project)` while still naming the home label. Co-Authored-By: Qwen-Coder <[email protected]> * test(loop): lock that one-shot sentinel jobs are not deleted by the headless skip The skipHeadlessLoopSentinel deletion guard is `job.recurring && !job.durable` — both conditions required. All existing tests use recurring: true, so a simplification to `!job.durable` would wrongly evict a one-shot sentinel job with nothing to catch it. Add a locking test: a non-recurring sentinel-prompt job still returns the skip result but is never passed to scheduler.delete. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): guard empty homedir confinement root; interpolate dynamic sentinel Two low-severity hardening fixes for the loop.md tick path. - Session.ts: when QWEN_HOME is unset and os.homedir() returns '' (minimal containers with no HOME), homeConfineRoot collapsed to ''. It is passed as the home-candidate symlink confinement root, and isWithin('', target) is trivially true — so a home ~/.qwen/loop.md symlink could resolve anywhere, bypassing the boundary. Fall back to path.dirname(homeQwenDir) (the parent of the always-non-empty Storage.getGlobalQwenDir()) so the root is never empty. Adds a focused test that drives a dynamic tick with os.homedir()==='' and asserts the resolver receives a non-empty homeDir (mutation-checked). - loop-tick-resolver.ts: PACING_SUFFIX.dynamic and ABSENT_TAIL.dynamic hardcoded the literal `<<loop.md-dynamic>>` in their user-facing instructions. Interpolate the exported LOOP_SENTINEL_DYNAMIC constant instead so a future rename can't silently drift the instruction. Adds one assertion referencing the constant on the absent-tick path (the full-block path was already covered). Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): reject hard-linked loop.md and keep dynamic loops alive on transient read errors Hardens the .qwen/loop.md fire-time resolution surfaced by wenshao's review. nlink hard-link exfiltration (Critical): the project and home loop.md candidate checks verified isSymbolicLink()/isFile() + workspace/home confinement but never inspected nlink. A hard link such as `ln .env .qwen/loop.md` (or `ln ~/.ssh/id_ed25519 ~/.qwen/loop.md`) is an indistinguishable regular file sharing the secret's inode; it passed every check and isWithin (same inode, same fs), so the secret was read and injected into the model prompt every tick. Both candidates now reject `nlink > 1`, mirroring canonicalizeKeytermsFile in voice-keyterms.ts. dynamic loop silent death (correctness): a transient, non-whitelisted fs error (EACCES/EIO, or a Windows editor/AV briefly locking the file) made resolve() throw, the cron catch re-throw, and the turn unwind before the model ran. For a dynamic (self-paced) loop this is fatal and permanent: the firing wakeup was already consumed and the loop only survives because the model re-arms LoopWakeup at end-of-turn, so no turn means no re-arm. The catch now degrades a dynamic tick to a model-facing no-op that mirrors the absent path (heading + ABSENT_TAIL.dynamic re-arm + errno note) so the loop survives the hiccup; cron keeps throwing (it re-fires on its own next interval). The real errno stays in the LOCAL debug log, never leaking the absolute path to the client. Also: capture folder-trust once per tick and thread it into both resolve() and the sanitized error's absentLocations(), so a mid-tick trust flip can't make the error name a different candidate set than was probed; and log the previously silent whitespace-only skip so a present-but-empty loop.md is distinguishable from an absent one in debug logs. Co-Authored-By: Qwen-Coder <[email protected]> * test(loop): hoist expects out of conditional branches to satisfy vitest/no-conditional-expect The loop.md test additions placed expect() calls inside if-narrowing branches (if (result.status === 'found') { ... }) and an if (typeof text === 'string') guard inside a for-loop, which the vitest/no-conditional-expect rule (an error in the recommended preset) flags. Hoist each assertion to the top level: assert result.status first, then a plain throw-guard to narrow the discriminated union, then the assertions unconditionally. In Session.test.ts, move the string type-filter into a .filter() so the expect runs in a plain for-of (loops are not conditionals). Pure lint-compliance refactor: assertions and coverage are unchanged. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): normalize home label trailing slash; distinguish transient-error echo homeLoopLabel() built the $QWEN_HOME label by slicing homeLoopPath at homeQwenDir.length, but Storage.getGlobalQwenDir() does not strip a trailing slash, so QWEN_HOME=/x/.qwen/ over-counted the separator and produced the garbled $QWEN_HOMEloop.md. Slice past path.dirname(homeLoopPath) instead, which is always trailing-slash-free; the no-trailing-slash case is unchanged. The loop-tick echo said "loop.md not present" whenever sourceLabel was absent, which also fired for buildTransientErrorTick's dynamic-mode survival tick (a file that exists but failed to read this tick). Add a transientError flag on LoopTickResult, set only by buildTransientErrorTick, and branch the echo to "loop.md temporarily unavailable" for that case; genuinely-absent stays "not present". No errno/path leaks into the echo. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): only degrade transient fs errors in dynamic mode; log transient flag In dynamic (self-paced) loop mode, the loop.md sentinel-resolution catch degraded EVERY resolve() error to a no-op re-arm tick. A non-fs error (a TypeError / assertion → code 'unknown') therefore entered an infinite silent no-op cycle: the loop never died and the real bug never surfaced. Gate the degradation on a known-transient fs code set (TRANSIENT_FS_CODES: EACCES/EIO/EBUSY/EPERM/ENOENT); any other (unexpected) error falls through to the existing sanitized throw so it surfaces instead of looping forever. Also distinguish a transient-error tick from a genuinely-absent loop.md in the debug log: both produce full:false + sourceLabel:undefined, so the line printed delivery=absent for both. Add transient=${transientError ?? false} so an oncall engineer can tell 'file missing' from 'file unreadable'. Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): treat EISDIR/ENOTDIR as transient in dynamic mode readLoopTaskFile's lstat→open sequence can throw EISDIR/ENOTDIR if the loop.md path is swapped to a directory (or non-directory) between the pre-open lstat and fs.open (a narrow TOCTOU race). Add both codes to TRANSIENT_FS_CODES as defense-in-depth so a dynamic (self-paced) loop degrades to a no-op re-arm tick and survives, rather than re-throwing and silently terminating, should readLoopTaskFile's internal skip ever narrow. Drop ENOENT from the set: readLoopTaskFile resolves a missing file to its own missing→no-op path, so ENOENT can never reach the Session resolve() catch where TRANSIENT_FS_CODES is checked — listing it was dead and misleading ("absent" is not a transient read failure). Add dynamic-mode tests asserting an EISDIR and an ENOTDIR resolve error each degrade to a no-op re-arm tick (loop survives), mirroring the EACCES case. Co-Authored-By: Qwen-Coder <[email protected]> * test(loop): lock the setSkipDurableFire sentinel-predicate wiring in runNonInteractive Both halves of the headless durable-loop.md guard are tested alone — detectLoopSentinel via the skipHeadlessLoopSentinel tests, and the setSkipDurableFire filter in cronScheduler tests — but nothing pinned that runNonInteractive actually installs a predicate connecting them. Add a locking test that enables cron, injects a real CronScheduler, spies on setSkipDurableFire, and asserts the captured predicate classifies both loop.md sentinels (true) and a regular cron prompt (false). Co-Authored-By: Qwen-Coder <[email protected]> * fix(loop): keep separator for root QWEN_HOME; distinct heading for transient-error tick Two maintainer-suggested refinements to the loop.md tick resolver: - homeLoopLabel() dropped the leading separator when $QWEN_HOME is the filesystem root: path.join('/', 'loop.md') = '/loop.md', whose path.dirname is '/' (length 1), so slicing past it yielded the garbled '$QWEN_HOMEloop.md'. The home candidate is always <homeQwenDir>/loop.md, so build the label as '$QWEN_HOME' + path.sep + 'loop.md' directly -- byte-identical for the under-$HOME, non-root, and trailing-slash cases, fixed for root. - The transient-error tick (file exists but unreadable this tick, e.g. EIO) reused the ABSENT heading ('loop.md absent'), contradicting its own body ('could not be read this tick'). Give it a distinct 'loop.md unavailable' heading via a new tickHeading({ unavailable }) variant, keeping the dynamic re-arm tail and the sanitized (errno) note unchanged. Tests: add a root-$QWEN_HOME label case and assert the transient tick's heading conveys 'unavailable' (never 'absent'/'not present'); update the Session transient-tick heading assertions to match. The genuinely-absent tick still reads 'loop.md absent' / 'not present'. Co-Authored-By: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
30c35781b3 |
ci(autofix): loosen issue candidate filters so the agent finds work (#5860)
* ci(autofix): loosen issue candidate filters so the agent finds work The scheduled issue phase had been finding 0 candidates on every run. Tier-1 requires the rarely-applied status/ready-for-agent label, and tier-2's "unattended = no human comment at all" rule fought its ">2 days old" age gate: on a busy repo every aged bug already has a community reply, so the funnel collapsed (128 aged bugs -> 0 survivors) purely on the comment rule. - Unattended now keys off maintainer engagement (OWNER/MEMBER/COLLABORATOR comments, excluding our own bots), not any human comment. Community "+1"/"me too" no longer disqualifies an issue. - Tier-1 and tier-2 are always both gathered (tier-1 prioritized, then de-duped by number) so a single weak ready-for-agent issue can't starve the run. - Tier-2 scans a bounded age window via MIN_ISSUE_AGE_DAYS..MAX_ISSUE_AGE_DAYS (1..15) to focus on fresh, settled bugs instead of an unbounded set. Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): preserve autofix issue tier priority * fix(ci): keep ready autofix issues in tier one * fix(ci): reserve autofix tier two slots * fix(ci): tolerate tier two autofix scan failures * fix(ci): tolerate tier one autofix scan failures * fix(ci): harden autofix candidate merging * fix(ci): harden autofix issue scanning * fix(ci): harden autofix diagnostics * fix(ci): skip unsafe autofix comment fallbacks * fix(ci): drop unused autofix comments payload * test(ci): strengthen autofix comment refresh guard * test(ci): satisfy autofix workflow lint * test(ci): tighten autofix tier two assertion * test(ci): cover autofix tier merge guards * test(ci): cover autofix unattended filter * fix(ci): summarize autofix comment refresh drops * fix(ci): bound autofix tier-2 comment-refresh API calls Cap the tier-2 issues fed into the comment-refresh loop to 15 (a margin above the ~10 ever selected, since filter_unattended_candidates drops attended issues afterward) and request full pages with per_page=100, to avoid GitHub secondary rate limits. Add workflow tests asserting the forced-issue skip/in-progress exclusion and the tier-2 ready-for-agent label exclusion stay in place. Co-Authored-By: Qwen-Coder <[email protected]> * fix(ci): refresh all scanned tier-2 autofix issues, not just first 15 The previous change capped comment-refresh to the first 15 tier-2 issues before filter_unattended_candidates runs. The scan still fetches 30, so valid unattended bugs at positions 16-30 were never refreshed nor filterable: if the first 15 were all maintainer-attended/dropped, the run could end with zero candidates again — the failure this PR set out to fix. Drop the .[0:15] refresh cap and refresh the full scanned set (already bounded by --limit 30 on the scan). per_page=100 stays as the pages-per-issue rate-limit mitigation, and the drop summary total/counters reflect the full refreshed set again. Top-N selection after filtering is unchanged. Co-Authored-By: Qwen-Coder <[email protected]> * fix(ci): order tier priority before recency in autofix assess prompt The assess prompt gave two conflicting selection rules: the tier note says not to pick a tier-2 issue over a comparably actionable tier-1 one, while the recency tiebreaker says to prefer the most recently reported. Since tier-1 has no age bound and tier-2 is the recent (1-15 day) tier, these point in opposite directions for comparable confidence. Make recency apply only after the tier preference and only within the same tier, removing the contradiction. Prompt text only; no shell/jq change. Co-Authored-By: Qwen-Coder <[email protected]> * fix(ci): guard forced-issue jq and derive autofix comment scratch path Address two review suggestions on the autofix candidate scan step. - Wrap the forced-issue candidate jq in the same error guard the other jq calls use (tier-1/tier-2 stamp, tier merge): on malformed forced-issue JSON it now emits a ::warning:: and falls back to an empty candidate list instead of silently producing an empty candidates.json with no diagnostic. - Make refresh_issue_comments reuse-safe by deriving its NDJSON scratch path from the output_file parameter (${output_file%.json}.ndjson) instead of hardcoding tier2-with-comments.ndjson, so a second call or a different tier can't clobber a shared scratch file. Behavior for the current single call is unchanged. Co-Authored-By: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
988a5021e8 |
docs(telemetry): comprehensive documentation update to match current implementation (#5960)
* docs(daemon): update developer docs for recent daemon PRs - Add Last-Event-ID client reconnect guide (10-event-bus, 13-sdk-daemon-client) - Add cross-connection vote routing section (04-permission-mediation) - Add new capability tags: daemon_status, workspace_permissions, workspace_trust, workspace_github_setup, workspace_voice, workspace_voice_transcription, voice_transcribe (11-capabilities-versioning) - Add new event types: trust_change_requested, github_setup_completed, extensions_changed, mid_turn_message_injected (09-event-schema) - Fix _meta.serverTimestamp source description (09-event-schema, 10-event-bus) - Fix async function* syntax in SDK example (13-sdk-daemon-client) - Sync event/capability counts across all docs (43->47 events, 67->75 tags) * docs(daemon): add workspace remember design doc (PR #5884) Design document for the sessionless workspace remember API proposed in PR #5884. Covers API endpoints, task lifecycle, implementation details, events, error handling, and SDK integration. Status: Proposed (not yet merged). * docs(telemetry): comprehensive update to match current implementation Added 34 undocumented events, 17 metrics, 11 daemon metrics, 2 spans. Fixed diff_stat attribute schema (was documented as JSON string, actually individual attributes). Added Performance Monitoring reserved section. Standardized attribute annotations with type and optionality markers. * fix(telemetry): extract EVENT_TOOL_OUTPUT_TRUNCATED constant and add qwen-code prefix Extracted hardcoded event name to constant for consistency with other telemetry events. Added standard qwen-code. namespace prefix. Updated test assertion to match the new prefixed event name. * qwen: address PR review feedback (#5960) * qwen: address PR review feedback (#5960) * qwen: address PR review feedback (#5960) * qwen: address PR review feedback (#5960) * qwen: address PR review feedback (#5960) * qwen: address PR review feedback (#5960) * docs: address PR review feedback (#5960) * docs: address PR review feedback (#5960) |
||
|
|
5581424b6b |
feat(browser-ext): revive Chrome extension via daemon-direct architecture (#5777)
* feat(chrome-qwen-bridge): 🔥 init chrome qwen code bridge * chore(chrome-qwen-bridge): connect * chore(chrome-qwen-bridge): connect & them * chore(chrome-qwen-bridge): connect & them * chore(chrome-qwen-bridge): wip use chat ui * chore(chrome-qwen-bridge): wip use chat ui * wip * refactor(chrome-extension): rename chrome-qwen-bridge package to chrome-extension * feat(chrome-extension): enhance network monitoring with webRequest API Replace the existing network monitoring implementation with a more comprehensive solution that combines both webRequest and debugger APIs for broader coverage. The new implementation: - Uses chrome.webRequest.onBeforeRequest to capture all outgoing requests - Uses chrome.webRequest.onCompleted to capture completed responses - Uses chrome.webRequest.onErrorOccurred to capture failed requests - Retains debugger API integration for detailed network information - Implements memory management with maximum 1000 logs per tab - Adds proper initialization and cleanup for each tab - Ensures graceful handling of debugger attachment failures - Provides more reliable network activity capture across all tabs This enhancement significantly improves the reliability and coverage of network monitoring functionality in the Chrome extension. * refactor(chrome-extension): reorganize directory structure for better maintainability This commit reorganizes the entire Chrome extension package structure for improved maintainability and clarity: - Move all source files to `src/` directory (background, content, sidepanel) - Move build configurations to `config/` directory - Move documentation to `docs/` directory with proper categorization - Move all script files to `scripts/` directory - Move native-host specific files to appropriate subdirectories (`src`, `scripts`, `config`) - Update package.json scripts to reflect new file locations - Add comprehensive documentation files (debugging, development, architecture, API reference) - Maintain all functionality while improving project organization The reorganization separates source code from build output, centralizes documentation, and creates a clear separation of concerns making the project more maintainable and easier for developers to navigate. * feat(chrome-extension): enhance native host communication and network logging - Add troubleshooting documentation for native host setup issues - Improve native host logging to home directory with fallback to tmp - Enhance network logging in service worker with response body capture - Update scripts to properly reference host.js from correct path - Increase timeout for MCP session creation and long prompts from 3 to 5 minutes - Add getConsoleLogs functionality to sidepanel for content script capture - Improve browser-mcp-server network logs aggregation by request ID - Update icon assets and improve manifest configuration refactor(chrome-extension): consolidate host.js entry point and improve path resolution - Create unified host.js entry point that delegates to src/host.js - Improve path resolution for host scripts in installer and runner scripts - Add proper path existence checks for browser-mcp-server.js - Support running from different directory structures style(chrome-extension): improve TypeScript type safety and error handling - Add proper type definitions for message handling in side panel - Add null checks and error handling for message parsing - Improve React component callback implementations * refactor(chrome-extension): redesign build workflow * fix(chrome-extension): resolve ESLint errors in native host, service worker, and content script - Fix 'Unexpected lexical declaration in case block' by wrapping switch cases in blocks - Fix 'Unexpected constant truthiness on the left-hand side of a || expression' by using conditional patterns - Fix unused variable errors by properly using catch error parameters or adding logging - Fix 'document' and 'window' not defined errors in service worker with proper global declarations - Add eslint-disable comments where appropriate for globals used in specific contexts * feat(chrome-extension): enhance native host with browser MCP tools and event streaming - Add new browser MCP tools: browser_click, browser_click_text, browser_run_js, browser_fill_form_auto - Implement SSE (Server-Sent Events) for improved event streaming instead of long-polling - Add daemon script for running the bridge host in background - Enhance documentation with MCP notes and updated README - Add multiple executable binaries to package.json: chrome-browser-mcp, qwen-bridge-host - Improve error handling and event processing in the native host - Add debouncing mechanism for stream end events in service worker - Update timeout for MCP discovery to accommodate slower startup Co-authored-by: Claude Opus 4.5 <[email protected]> * fix(chrome-extension): use trusted cwd for MCP tool discovery The root cause of MCP tools not being recognized by Qwen CLI was that the cwd (current working directory) was defaulting to '/' (root directory). In Qwen CLI's MCP discovery logic, there's a security check: if (!cliConfig.isTrustedFolder()) { return; // Skip MCP tool discovery } The root directory '/' is not a trusted folder, so MCP tools were silently not being discovered at all. Changes: - host.js: Default to $HOME instead of process.cwd() for start_qwen - service-worker.js: Remove '/' fallback, let host.js handle default This ensures browser MCP tools (browser_read_page, browser_click, etc.) are properly discovered and available to the model. * fix(core): validate MCP entry script existence before connection Add pre-flight check to verify that stdio-based MCP server entry scripts exist on disk before attempting connection. This prevents silent failures and provides clear error messages for misconfigured MCP servers. * fix(chrome-extension): enhance native host path resolution and port config - Add QWEN_BROWSER_MCP_SERVER_PATH env override for custom installations - Expand candidate search paths for browser-mcp-server.js discovery - Add detailed logging when MCP server script is not found - Support BRIDGE_PORT env variable for HTTP API server configuration * fix(chrome-extension): improve browser MCP server reliability and debugging - Add comprehensive debug logging for bridge health checks and host spawn - Support BROWSER_MCP_NO_SPAWN env to disable automatic host.js spawning - Handle bridge unavailability gracefully with clear error messages - Add raw JSON mode fallback for clients without Content-Length framing - Capture and log host.js stdout/stderr instead of inheriting stdio - Add pre-flight bridge check at startup for better diagnostics - Handle each bridge call failure with proper error responses * chore(chrome-extension): add debug wrapper script for MCP server Add cbmcp-wrapper.sh to help diagnose MCP server invocation issues. The wrapper logs invocation details and stderr to /tmp/cbmcp.log, making it easier to debug when Qwen CLI spawns the MCP server. * docs(chrome-extension): add MCP/Bridge troubleshooting guide Document common failure scenarios when MCP bridge shows as Disconnected: - EPERM errors when spawning host.js cannot bind to port - Content-Length framing issues during MCP handshake - Step-by-step troubleshooting with flow diagrams - Manual bridge setup with BROWSER_MCP_NO_SPAWN workaround * build(chrome-extension): 浏览器插件 mcp 构建优化 * docs(chrome-extension): update docs * fix(chrome-extension): 解决CDP响应体获取和权限请求处理问题 * feat(mcp-chrome-integration): add MCP Chrome browser extension integration Add complete Chrome extension with native messaging host for MCP integration: - Chrome extension with sidepanel UI, service worker, and content script - Native server with agent engines (Claude/Codex), session management, and tool bridge - Shared packages for types, tools, and node specifications - Documentation and build scripts Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore(mcp-chrome-integration): refactor * feat(chrome-extension): 切换到HTTP后端代理并更新构建配置 * refactor(mcp-chrome-integration): build * wip * wip chrome extension * chore: ignore .worktrees * docs: plan sidepanel component removal * refactor(sidepanel): remove local components * feat(chrome-extension): add native messaging ACP client and protocol support - Add ACP client for native messaging communication - Add file handler for local file access via native host - Add protocol definitions for ACP communication - Archive old documentation files - Update integration status and protocol documentation * fix(chrome-extension): use GenericToolCall from @qwen-code/webui - Replace non-existent local ToolCallCard import with GenericToolCall - Import from @qwen-code/webui package instead of local path - Fix component props to match GenericToolCall interface - Remove unused ToolCallData import * refactor(mcp-chrome-integration): split large files and fix ESLint errors - Split tools.ts (1554 lines) into 8 schema files by functionality - Split native-messaging.ts (1533 lines) into 6 modules - Split doctor.ts (1099 lines) into 8 modules - Split content-script.ts (1055 lines) into 5 modules - Add Qwen Team license headers to all new files - Remove unused tool names (SEARCH_TABS_CONTENT, SEND_COMMAND_TO_INJECT_SCRIPT, USERSCRIPT, RECORD_REPLAY) - Fix ESLint errors: no-require-imports, no-explicit-any, no-unused-vars, prefer-const - Add archive/ directory to eslint ignore patterns * 调试 MCP 工具成功 * refactor: revert unnecessary formatting changes and clean up MCP chrome-integration - Revert Node version requirement from >=22 to >=20 - Revert code formatting changes (import statements and indentation) - Archive obsolete chrome-extension implementation - Clean up outdated documentation and scripts - Reorganize MCP chrome-integration docs * docs(mcp-chrome): 新增核心文档和更新 README - 新增 02-features-and-architecture.md (27个工具完整参考) - 新增 03-design-and-implementation.md (与 hangwin/mcp-chrome 对比) - 新增 04-test-cases.md (35个测试用例) - 更新 01-installation-guide.md (Extension ID 固定方案) - 重写 README.md (对齐新文档结构) * refactor(chrome-extension): 简化 sidepanel 并移除未使用代码 - 删除未使用的 Onboarding 组件 - 删除冗余样式文件 (App.css, timeline.css) - 删除未使用的工具函数 (diffStats, diffUtils, sessionGrouping, tempFileManager, webviewUtils) - 新增 MCP 工具状态横幅显示 - 隐藏不需要的 UI 按钮 (slash command, attach, edit mode) - 移除未使用的变量 (clearToolCalls) - 更新 manifest.json 和 native-messaging-host * chore: regenerate lockfile for mcp-chrome-integration workspace npm install reconciles the lock with the merged package.json: adds the new packages/mcp-chrome-integration/app/* workspace subtree (371 deps). No existing main dependencies were removed. * chore(chrome-integration): drop dead logger + unused pino deps native-server/src/util/logger.ts was 100% commented-out dead code (even a hardcoded /Users/hang/... path) with zero importers; pino/pino-pretty were declared but never imported (logging is console.error to stderr, correct for a native-messaging host). Remove the file and both deps. * feat(chrome-integration): WIP serve-backed agent client (Phase 1 backbone) Replace the hand-rolled ACP client by driving `qwen serve` (main's maintained HTTP daemon) through the SDK's DaemonClient. New serve-agent/client.ts spawns `qwen serve --no-web` where the host used to spawn `qwen --acp`, then uses DaemonClient (REST + SSE) for session create / prompt / cancel / permission / streaming. Same public surface as AcpClient so the native host can swap in place. Compiles + typechecks against the real SDK API. NOT yet wired into native-messaging-host.ts — wiring is gated on two items that need runtime validation in a real Chrome + qwen env: - OAuth: host's onAuthenticateUpdate (in-extension authUri) maps to serve's server-side device-flow events — needs design. - permission requestId becomes string (was number) — host's permissionRequests map + handler need to follow. acp/ kept in place until the serve path is validated. Packaging follow-up: @qwen-code/sdk (→ core) as a dep of the standalone host is fine in the monorepo but needs a publish-time story (bundle or resolve from the co-installed qwen). * feat(browser-ext): daemon-direct connection foundation (Phase 1, #5626) First brick of the daemon-direct architecture from #5626: the extension talks straight to a local `qwen serve` HTTP daemon instead of a native messaging host. - daemon/config.ts: resolve { baseUrl, token } — default loopback http://127.0.0.1:4170 (auth-free), overridable via chrome.storage.local. - daemon/discovery.ts: GET /health probe so the side panel can show a "start qwen serve" hint instead of a broken chat when no daemon is up. Both typecheck clean. (Pre-existing tsc errors live only in the orphaned legacy sidepanel hooks — useWebViewMessages/useToolCalls etc. — which the DaemonSessionProvider migration removes next.) * docs(browser-ext): daemon-direct architecture spec (Phase 1+2, #5626) Concrete implementation spec: Phase 1 (side panel as daemon client, no daemon changes) and Phase 2 (browser tools as a client-hosted MCP server over the daemon WS). Phase 2 reuses the existing SdkControlClientTransport / SdkControlServerTransport pattern but moves the wire from the SDK subprocess control plane onto qwen serve's WebSocket — a new public daemon-contract surface, gated behind a capability flag (the open question in #5626). Includes the daemon-lifecycle options for #5626 Q3. * feat(browser-ext): Phase 1 — side panel as a daemon-direct client (#5626) Side panel chat now talks straight to a local `qwen serve` HTTP daemon via @qwen-code/webui's DaemonSessionProvider, replacing the native-messaging relay (background/ + content/ untouched; nativeMessaging perm kept for now). - SidePanelRoot.tsx: health gate — checkDaemonHealth(getDaemonConfig()); shows a "run qwen serve" hint + Retry when unreachable, else mounts DaemonSessionProvider around App. - App.tsx: rewritten daemon-driven — transcript/streaming/permissions/ lifecycle from the webui daemon hooks (useTranscriptBlocks, useStreamingState, usePromptStatus, usePendingPermissions, useConnection, useActions); reuses the existing webui presentational components + ChromePlatformProvider. - sidepanel/daemon/{transcriptItems,permission}.ts: adapters from daemon transcript/permission shapes to the existing UI components. - Deleted the dead legacy native-messaging chat hooks/types. Verified: sidepanel/daemon tsc clean; `npm run build` (esbuild) passes, emits the side-panel bundle with the daemon wiring. (Pre-existing tsc errors remain only in background/ + content/, out of scope until Phase 2.) Daemon contract accepted live against the worktree `qwen serve`: /health, /capabilities (advertises session_create/prompt/events/workspace_mcp), POST /session runs end-to-end to the model-auth gate. * feat(browser-ext): Phase 2 — browser tools over the daemon WS (#5626) Reverse tool channel: a WS client (the extension) hosts an MCP server (its browser tools) that the daemon's agent can call, carrying mcp_message JSON-RPC frames over the daemon WS — reusing the SdkControlClientTransport / SDK-MCP-server control-plane pattern. Gated behind capability flag `client_mcp_over_ws` (opt-in; the public-contract piece flagged in #5626). Daemon (core + cli/serve): - core/tools/client-mcp-registrar.ts: ClientMcpRegistrar — id-correlation, pending/timeout, notifications fire-and-forget, exposes the sendSdkMcpMessage(server,msg) callback McpClientManager consumes. - cli/serve/acp-http/client-mcp-ws.ts: ClientMcpWsConnection — handles mcp_register/mcp_message/mcp_unregister frames; pushes mcp_message down the WS; disposes on close. Hookup to the live agent McpClientManager is a ClientMcpServerProvider injection point (returns structured `not_wired` until the child↔parent reverse-IPC lands — see below). - capability `client_mcp_over_ws` threaded through serve options/capabilities. Extension (chrome-extension/background): - browser-tools-server.ts: minimal hand-rolled MCP JSON-RPC (initialize/tools/list/tools/call) reusing the existing tool-catalog + router + executors (MVP 6 read-first tools); WS client to the daemon /acp with mcp_register + reconnect. Wired into the service worker behind a health probe; native messaging left intact. Self-accepted (no LLM needed): 10/10 tests pass — a headless ws client registers + answers the MCP handshake over mcp_message and the daemon lists+CALLS the client-hosted chrome_read_page tool end-to-end over a real socket. Builds: core + cli + extension esbuild all green; tsc clean. NOT yet wired (out of scope here; needs acp-bridge + acpAgent reverse IPC): the parent-process WS ↔ ACP-child McpClientManager hookup — the daemon's WS lives in the parent serve process but sendSdkMcpMessage binds in the ACP child. Single injection at the mountAcpHttp call site once that IPC exists. * feat(serve): wire client-MCP-over-WS to the ACP child agent (#5626) Closes the Phase 2 gap: a client-hosted (extension) MCP server's tool calls now reach the agent's McpClientManager in the ACP CHILD, routed back to the parent's ClientMcpRegistrar and out over the daemon WS. Opt-in via QWEN_SERVE_CLIENT_MCP_OVER_WS=1 (the contract is still settling — dormant by default; this is the public-daemon-contract piece flagged in #5626). Contract additions: - ACP ext-method `qwen/control/client_mcp/message` (child→parent, called UP): {server,payload} → {payload}; notifications resolve with a synthetic ack. - runtime-MCP config flag `__clientMcpOverWs`: parent stamps it on the SDK-type add config; child KEEPS type:'sdk' (instead of stripping) so it binds an SdkControlClientTransport instead of the SDK subprocess control plane. - BridgeOptions.clientMcpSender seam + ServeAppDeps.clientMcpSenderRegistry. Round-trip: mcp_register(WS) → serve registers the connection's ClientMcpRegistrar.sendSdkMcpMessage in a process ClientMcpSenderRegistry + bridge.addRuntimeMcpServer(type:'sdk',__clientMcpOverWs) → child adds the SDK server + runs initialize/tools/list, each frame child→parent via client_mcp/message → BridgeClient looks up the sender → registrar pushes mcp_message down the WS → extension answers → child discovers the tools. Self-accepted LIVE (no LLM): integration-tests/cli/qwen-serve-client-mcp.test.ts spawns a REAL qwen serve + REAL qwen --acp child under a mock OpenAI server, a headless ws client registers + answers the MCP handshake, and the child discovers the client-hosted chrome_read_page tool over the genuine child→parent→WS channel (GET /workspace/mcp/chrome-tools/tools lists it). Verified passing locally (24.6s). +5 bridge round-trip tests; 324 acp-bridge, the 10 prior Phase-2, acpAgent 133, server+acp-http 675 all pass; builds clean. Not exercised here: a real LLM turn driving tools/call (needs creds), and a real Chrome extension as the WS client (headless ws stands in). * fix(serve): session-scope runtime MCP servers so client tool calls resolve (#5626) The reverse tool channel registered the client-hosted MCP server only on the bootstrap/workspace Config, so discovery worked but a prompt — which runs against an independent per-session Config from newSessionConfig→loadCliConfig — couldn't resolve the tool ("not found in registry"), and the reverse WS channel was never reached. Spec (docs/05) intends per-session scope. Fix (minimal, additive, guarded; normal settings-based MCP servers unaffected): - core/config.ts: add Config.getRuntimeMcpServers() (shallow copy of the private runtimeMcpServers map). - acpAgent.ts newSessionConfig: copy the bootstrap Config's runtime MCP servers into a newly-created session Config before initialize() so its discovery binds that session's sendSdkMcpMessage (register-before-session). - acpAgent.ts workspaceMcpRuntimeAdd/Remove: fan the add/remove out to every active session's McpClientManager (register-after-session), best-effort. Test: the fake model now emits the fully-qualified registered name mcp__chrome-tools__chrome_read_page (what a real model is handed); the reverse channel still forwards the bare tool name to the client server. Verified LIVE (no LLM, no creds): integration test now drives the FULL loop — model→agent→session-registry resolution→reverse client_mcp/message over the daemon WS→headless ws client returns CallToolResult→agent consumes it (tool completed)→turn_complete. 2/2 integration tests pass (confirmed locally). Regression: config 248, acpAgent 133, mcp-client-manager 101, client-mcp-ws 5 pass; builds clean. (server.test.ts has 2 pre-existing Web-Shell flakes, identical on the unmodified baseline.) Still needs a real Chrome extension (vs the headless ws stand-in) + a real model turn for true browser behavior; the protocol round-trip is proven. * chore(browser-ext): remove the dead Native Messaging stack (#5626) Daemon-direct made Native Messaging obsolete — the extension talks to `qwen serve` directly (chat over HTTP+SSE, browser tools over the daemon WS), so the entire native-host stack is dead weight. Deletes ~15.5k lines: - packages/mcp-chrome-integration/app/native-server/ — the whole native host (MCP servers, ACP client, Fastify server, doctor/register/report/postinstall, the superseded serve-agent backbone). - extension background transport: native-messaging.ts, native-connection.ts, native-message-handler.ts, native-messaging-types.ts, ui-request-router.ts (+ its test). The still-used executor types (BrowserToolArgs / RawNetworkRequest / WebSocketSession / NetworkCaptureState) move to background/browser-tool-types.ts. - service-worker rewired to daemon-direct only (drops the NativeMessaging init + the onMessage→routeUiRequest relay; keeps the browser-tools server start). - esbuild.background.config.js drops the deleted native-messaging entry point. - manifest.json drops the `nativeMessaging` permission. - package.json scripts drop every native-server/native-host reference; dev-watch no longer spawns the native host. - obsolete native-messaging docs (01-04) + scripts (diagnose/install/update) removed; README rewritten for daemon-direct. Extension esbuild build green; tsc errors dropped (84 -> 69, all pre-existing node:test/content typings). Daemon-side (cli/serve/core) untouched. * chore(browser-ext): drop orphaned content-fetch-patch.ts (#5626) * fix(serve): let browser extensions open the daemon WS reverse channel The daemon-direct Chrome extension (#5626) connects to qwen serve's /acp WebSocket to register its browser tools as a client-hosted MCP server. Three gaps blocked the real-browser path. A node WS client in the integration tests carries no browser Origin and completes the ACP handshake, so none of these surfaced until a real Chrome connected: - The WS CSRF check hard-coded loopback origins, so the extension's chrome-extension://<id> Origin was rejected with 403. Wire the existing --allow-origin allowlist into the WS upgrade check (acp-http/index.ts, server.ts) with the same match semantics as the REST allowOriginCors. - parseAllowOriginPatterns rejected chrome-extension:// because its URL.origin is the opaque "null". Rebuild the canonical origin from scheme+host for opaque-origin schemes (auth.ts), with tests. - The extension skipped the ACP initialize handshake and sent mcp_register directly, tripping the daemon's 30s initialize timeout. Send ACP initialize first and register only after the ack (browser-tools-server.ts). Also allow console.* in the extension package (no stdio in the MV3 runtime) via the eslint no-console allowlist. Verified end-to-end against a real Chrome: the daemon agent calls mcp__chrome-tools__chrome_read_page and reads the live active tab. * docs(browser-ext): Plan C (CDP tunnel) feasibility + implementation design Assess routing chrome-devtools-mcp's ready-made DevTools toolset through the extension's chrome.debugger to drive the user's real browser, instead of re-implementing each tool in the extension (Plan A). Records, with source verification against [email protected] + [email protected]: - the createCDPSession wall (why zero-change reuse fails — Target.attachToTarget is "Not allowed" for chrome.debugger; cdp-mcp throws in McpContext.from); - the patch-package fork shape (pin 1.4.0 + a ~2-site patch, not vendor/submodule); - the daemon /cdp browser-level CDP emulation + sessionId routing design; - minimal browser-level command set, reusable prior art (playwright-mcp --extension), phased steps, risks, and the Plan A fallback. Refs #5626. * feat(serve): add CDP browser-level emulator for Plan C tunnel (#5626) First component of the Plan C "CDP tunnel": a synthesis layer that fakes the browser-level CDP topology so an external puppeteer client (chrome-devtools-mcp) can connect over a future /cdp endpoint while page-domain commands are forwarded to the one real tab via the extension's chrome.debugger. Implements the exact contract a Phase 0 spike proved necessary: a tab->page two-level target tree + recursive Target.setAutoAttach (browser attaches the tab session; the tab session attaches the page session), with page-session commands routed to forwardToTab and tab events re-tagged with the page session id. The spike connected real puppeteer to a pure synthesis layer and ran page.evaluate(() => 1 + 1) === 2. 7 unit tests cover the handshake + routing. Refs #5626. * feat(serve): add CDP tunnel reverse-link, /cdp glue, and bridge registry (#5626) Plan C Phase 1 daemon core. The reverse-link forwards page-domain CDP commands to the extension over cdp_command/cdp_result frames (id-correlated, timeout) and re-tags cdp_event onto the page session; cdp-ws wires a per- puppeteer-connection emulator to the reverse-link bound to the single active extension bridge in a process-scoped registry. The emulator gains setTabInfo so the synthetic targetInfo reflects the real tab after cdp_attach. * feat(serve): wire /cdp upgrade branch and cdpTunnelOverWs flag (#5626) Adds the /cdp WebSocket upgrade branch to acp-http (reusing the loopback / host-allowlist / auth / CSRF checks) and routes inbound cdp_* frames on the extension's /acp socket to the bound reverse-link. The extension connection registers as the active CDP bridge eagerly at ACP initialize so a /cdp puppeteer client can bind immediately (avoids the attach chicken-and-egg). Feature flag cdpTunnelOverWs is wired exactly like clientMcpOverWs (env QWEN_SERVE_CDP_TUNNEL_OVER_WS=1, capability cdp_tunnel_over_ws), DEFAULT OFF — existing behaviour is unchanged when off. * feat(browser-ext): add CDP bridge over the reverse /acp socket (#5626) The extension answers cdp_attach by attaching chrome.debugger to the active tab and cdp_command via chrome.debugger.sendCommand, replying cdp_result; chrome.debugger.onEvent -> cdp_event, onDetach -> cdp_detach. Reuses the existing browser-tools-server /acp socket (routes cdp_* frames; tears the bridge down on socket close) and mutually excludes with the chrome_network_debugger_* tools (one debugger per tab). * build(deps): pin chrome-devtools-mcp 1.4.0 + puppeteer-core 25.2.0, patch McpContext (#5626) Pins the two CDP-tunnel client deps (exact, to keep the version-specific patch and puppeteer's hardcoded ExtensionTransport topology stable) and adds patches/chrome-devtools-mcp+1.4.0.patch. The patch wraps McpContext.#init's devtoolsUniverseManager.init / serviceWorkerConsoleCollector.init in try/catch so the createCDPSession wall (Target.attachToTarget -> -32000 over chrome.debugger) no longer crashes the server on startup; only performance_* / service-worker console degrade. Applied via the existing postinstall patch-package hook (same form as patches/ink+7.0.3.patch). * test(serve): add Plan C /cdp end-to-end acceptance harness (#5626) Node script that starts the real daemon with the flag on, connects a mock extension over /acp (ACP initialize + mcp_register, answering cdp_command with page-domain CDP), then puppeteer.connect to /cdp and asserts page.evaluate(() => 1 + 1) === 2 through the real daemon + emulator + reverse-link. Allowlists the harness dir in eslint's node-script globals. * fix(serve): gate CDP page commands behind attach completion (#5626) Real-Chrome testing surfaced an ordering race the mock acceptance missed: the extension's chrome.debugger.attach is async (it pops the debugger banner), but forwardToTab forwarded page-domain commands immediately, so a fast puppeteer Network.enable reached the extension before attachedTabId was set and failed "CDP tunnel not attached to a tab". The mock extension acked attach synchronously, which hid the race. Add an attach gate in CdpReverseLink: forwardToTab awaits the in-flight cdp_attach to settle (success or failure) before forwarding. Verified end-to-end against REAL Chrome — puppeteer read the live active tab (a GitHub PR page) through the tunnel: pages=1, real url/title/body returned. Refs #5626. * refactor(chrome-extension): delete Plan A side panel, content scripts, and reverse tool channel Tears out the superseded Plan A surface so the extension can become a pure CDP-tunnel pipe (chat moves to the daemon web UI, browser tooling runs as chrome-devtools-mcp over the /cdp tunnel): - src/sidepanel/ (side-panel chat UI) - src/content/ (content scripts; the CDP tunnel drives DOM/Input via chrome.debugger, no injection needed) - background reverse tool channel: browser-tools-server, browser-network-tools, network-capture-utils, browser-tool-executors, tool-catalog, tool-router, mcp-tool-result, browser-tool-types, and their tests - public/sidepanel/sidepanel.html static asset Refs #5626 * refactor(chrome-extension): rewrite service worker as minimal daemon CDP client The service worker is now the entire extension logic: probe the daemon /health, open the /acp WebSocket, send the ACP initialize handshake (the daemon closes the socket on a 30s init timeout otherwise and binds this connection as the CDP bridge at that point), then route cdp_* frames into the CDP bridge with capped backoff reconnect. No more reverse MCP tool server (chrome-tools is gone). cdp-bridge: drop the browser-network-tools import and the network-capture mutual-exclusion branch in handleAttach (the network tools are deleted, nothing to exclude); remove the now-unused isCdpTunnelAttached export. Refs #5626 * build(chrome-extension): trim manifest, build config, and deps to the CDP pipe manifest: drop content_scripts, side_panel, and the sidePanel/webRequest/cookies/ scripting/webNavigation permissions; keep only debugger/tabs/activeTab/storage plus background, key, host_permissions, icons, and action. build: background esbuild now has a single service-worker entry point (content script gone); delete the UI esbuild/postcss/tailwind configs and drop the UI build step + build:ui scripts; sync-extension no longer special-cases the gone sidepanel assets; dev-watch no longer spawns the UI watcher. deps: remove the side-panel-only deps (@qwen-code/webui, react, react-dom, markdown-it, and the @types + the postcss/tailwind/autoprefixer CSS toolchain). Refs #5626 * chore(chrome-extension): drop dead externally_connectable hook (#5626) * test(serve): add real-Chrome /cdp local verification script (#5626) * test(serve): add cdp-mcp-over-tunnel layer-C smoke check * fix(extension): keep the CDP tunnel alive with chrome.alarms MV3 service workers idle out after ~30s, so the tunnel silently dropped whenever no puppeteer client was driving it and the user had to keep the Service Worker DevTools open to hold the worker awake. Register a 30s chrome.alarms keepalive: the recurring onAlarm dispatch holds the idle timer off, and each wake of a terminated worker re-runs the top level to reconnect. * feat(serve): auto-register chrome-devtools-mcp over the CDP tunnel Plan C (#5626) last mile: when `qwen serve` runs with the CDP-tunnel flag, the agent should be able to drive the user's real browser. Rather than hand-writing browser tools, auto-register the (patched) chrome-devtools-mcp as a session MCP server pointed at this daemon's /cdp endpoint, so its 29 ready-made DevTools tools flow through the tunnel. - run-qwen-serve: forward QWEN_SERVE_CDP_TUNNEL_OVER_WS + _PORT into the spawned ACP child via childEnvOverrides (same path as the MCP budget env). - acpAgent: buildCdpTunnelMcpServer() injects the server into the top-precedence sessionMcpServers tier when the flag + port are present and the package resolves; trust left unset so tools default to 'ask' (no silent auto-approval of browser control); best-effort skip otherwise. No settings.json edit and no hand-written tools required. * fix(serve): gate CDP bridge registration to the extension (#5626) Auto-registering every /acp initialize as the CDP bridge was last-writer- wins. Once an ACP agent connects over the same /acp endpoint (web UI, Zed), it would capture the bridge and receive cdp_* frames it can't answer, stealing the tunnel from the extension. Gate registration on clientInfo.name === 'qwen-cdp-bridge'; the extension (and the acceptance mock) now identify themselves that way, while agent clients are left alone. * feat(extension): open the web UI when the toolbar icon is clicked The extension has no UI of its own (pure CDP-tunnel pipe; chat lives in the daemon web UI), so clicking the toolbar icon did nothing after the side panel was removed. Wire action.onClicked to open the daemon baseUrl in a new tab so the icon is a useful entry point instead of a dead click. * feat: host the web UI in a Chrome side panel (#5626) The extension is a pure CDP-tunnel pipe with no UI of its own, so after the side panel chat was removed the toolbar icon did nothing. Bring the side panel back as a thin host that iframes the daemon web UI (chat + tools), so the sidebar is the everyday entry point and reuses the web UI's pages/components instead of shipping a second UI in the extension. - extension: side_panel + sidePanel permission; sidepanel.html/js frames the daemon baseUrl; toolbar icon opens the panel (openPanelOnActionClick). - daemon: the Web Shell sent frame-ancestors 'none' + X-Frame-Options: DENY, which blocked the iframe. Allow framing only for chrome-extension origins explicitly passed via --allow-origin; everything else still gets DENY. * fix: prefer chrome-devtools over computer-use under the CDP tunnel + keep MV3 worker alive during attach (#5626) Two issues surfaced driving the real agent: 1. The agent picked the OS-level computer-use tool (cua-driver) for browser tasks instead of the injected chrome-devtools-mcp — heavyweight screenshot/ click loop that pegged a CPU and stalled turns. Disable computerUse when the CDP tunnel flag is on so browser automation goes through the tunnel. 2. The extension's MV3 service worker idled out *between* CDP commands (the agent pauses to think), detaching chrome.debugger and hanging the next command. Add a sub-30s keepalive while attached; the 30s alarm only covered idle reconnects, not in-flight attachments. * chore(extension): stop tracking .extension-key.pem (signing private key) The extension signing private key was committed and pushed — anyone with repo access could impersonate the extension. Stop tracking it and gitignore *.pem. Load-unpacked debugging needs only the public "key" in manifest.json, so this doesn't affect dev on any machine; the .pem is kept locally for packaging. * refactor(chrome-extension): flatten to packages/chrome-extension Drop the mcp-chrome-integration wrapper + dead native-server (daemon-direct no longer uses native messaging). The extension is now a top-level workspace at packages/chrome-extension. Updated root workspaces, eslint globs, doc-path comments, and the two node scripts' global directives. cli + extension builds and eslint verified green. * test(serve): assert cdp_tunnel_over_ws in the capability registry The cdp_tunnel_over_ws capability was added to SERVE_CAPABILITY_REGISTRY + CONDITIONAL_SERVE_FEATURES but the test's EXPECTED_REGISTERED_FEATURES and the conditional drift-insurance branch weren't updated, failing 3 registry tests. Add it to the expected list (after client_mcp_over_ws, matching registry order) and add its assertion branch (predicate accepts/rejects the cdpTunnelOverWsEnabled toggle). * fix(serve): address review comments on CDP tunnel + client-MCP wiring (#5777) - guard post-async ws.send() with readyState OPEN (daemon crash on extension disconnect, sendClientMcpAck + cdp endpoint sends) - make client-mcp-sender-registry delete() ownership-aware (cross-connection server-name collision) - type the __clientMcpOverWs runtime config flag carrier - document the CDP tunnel trust model (loopback + bridge-gated dumb pipe) * chore(cdp-tunnel): set copyright year to 2026 on files created this year The Plan C / #5626 files were authored in 2026 but carried a 2025 header. service-worker.ts (created last year under #1432) keeps 2025. * feat(chrome-ext): add side-panel onboarding gate + fix packaging The side panel framed the daemon Web Shell unconditionally and showed a static "Connecting…" line. When no daemon was reachable, or the daemon wasn't started with --allow-origin (so frame-ancestors blocks the iframe), the user was stuck on "Connecting…" with no guidance, and discovery.ts's health check was never wired in. Wire a health/capabilities gate into the panel: - probe GET /health, then GET /capabilities - down → "Start qwen serve" + the exact command - up but no `allow_origin` feat → "Allow this extension" + the command - ready → frame the Web Shell The command is built from chrome.runtime.id at runtime, so it always names this extension's real origin (dev-unpacked or published) — no need to know or hardcode the id, and no publish-first chicken-and-egg. The pure decision helpers live in onboarding-logic.js. Also: - fix `npm run package` so manifest.json sits at the zip root (the old `zip ... extension/` nested it under extension/, which the Chrome Web Store rejects) - gitignore that packaging zip; add a package README Part of #5626. PR #5777. * docs(cdp-tunnel): trim over-long Plan C comments (ponytail) Comment-only: compress multi-paragraph design rationale to intent, keep the non-obvious why (trust model, bridge gate, attach ordering) + ponytail ceilings. ~-91 lines, no code touched, build + tests green. * fix(serve): address second-round review comments on the CDP tunnel (#5777) - close the bound /cdp puppeteer socket on extension disconnect (was hanging ~170s on CDP timeout) via an onExtensionGone hook - reject a 2nd concurrent /cdp client instead of silently clobbering routeInbound - narrow extension host_permissions from <all_urls> to localhost (chrome.debugger needs no host perm; only the /health fetch needs localhost) - log safeWsSend drops under serve debug mode so a dead tunnel is diagnosable * feat(chrome-ext): polish side-panel welcome into a terminal console Replace the bare welcome with a console that matches the product (a CLI daemon): the command is the hero, typed at a `$` prompt with a blinking cursor inside a titled terminal card. Warm-charcoal / electric-lime, light + dark aware (prefers-color-scheme), staggered load-in, a pulsing "listening" status, and a reduced-motion guard. No web fonts / no inline JS (the extension CSP allows neither). No behavior change: same /health + /capabilities gate and the chrome.runtime.id-derived command. Mechanics tidied alongside the markup: visibility toggles a .hidden class (CSS owns the flex layout), the copy button updates a label span, and the command is prefilled synchronously so first paint isn't an empty prompt. PR #5777. * feat(chrome-ext): click-to-copy command + centered copy button Make the onboarding command easier to grab: the whole command row is now click-to-copy (keyboard-reachable, Enter/Space), and a centered "Copy command" button sits at the foot of the terminal card. Both flash a check-mark "Copied" confirmation; the small top-bar button is gone. No gate-logic change. PR #5777. * test(serve): cover CDP-tunnel + client-MCP regression guards Add the four focused unit tests flagged in review for load-bearing reverse-channel paths that had no coverage: - ClientMcpSenderRegistry: ownership-scoped delete — a disconnecting connection must not remove an entry a peer re-registered under the same name. - CdpTunnelRegistry: register/supersede/unregister lifecycle, inbound routing delegation, onExtensionGone-on-disconnect, and the stale-unregister guard that must not evict a newer active bridge. - CdpReverseLink: a forwarded command rejects when its per-command timer expires (not just on bulk dispose). - safeWsSend: drops (no send, no throw) on a CLOSED/CLOSING socket. safeWsSend is extracted from acp-http/index.ts into its own module so it's unit-testable in isolation; behavior unchanged. PR #5777. * fix(serve): address bot code-review findings on the CDP tunnel (#5777) - cdp-bridge: tear down listeners before re-attach (was double-registering → duplicate cdp_event frames corrupting puppeteer) - emulator: return a CDP error for an unknown session instead of fake success - registry: notify the superseded bridge so the old /cdp closes (single-puppeteer) - deps: move chrome-devtools-mcp + puppeteer-core to optionalDependencies (~26MB) - tests: entry-script validation, deliverClientMcpMessage error branches, registry supersede * fix(chrome-ext): revert side panel to welcome when the daemon stops Once the panel framed the Web Shell it stopped probing, so if the daemon later went away the iframe was left showing Chrome's localhost connection-refused page with no way back. Keep probing after framing and, after a short tolerance (2 misses, ~5s, so a transient blip doesn't nuke a live chat), clear the iframe src and show the welcome screen again. PR #5777. * fix(chrome-ext,serve): address review comments on the CDP tunnel - service-worker: redact the bearer token from the connect log, and guard connect() against a still-CONNECTING socket so a rapid reconnect can't orphan an in-flight handshake. - acpAgent: don't clobber a user-configured `chrome-devtools` MCP server with the tunnel auto-wire. - cdp-reverse-link: log dropped/unexpected inbound frames via an optional diagnostic sink instead of swallowing them silently. - name the cross-package `qwen-cdp-bridge` client-name constant on both sides instead of repeating the bare string. PR #5777. * refactor(chrome-ext): inline onboarding helpers, drop dead pollTimer onboarding-logic.js was a 65-line file (mostly JSDoc) for three trivial helpers and a constant, split out "for testability" that was never used. Fold them into sidepanel.js (decideState collapses into probeState's return; resolveBaseUrl/allowOriginCommand become a one-liner each) and delete the file + its import. Also drop `pollTimer`: the welcome-fallback change made it write-only (the only clearInterval was removed), which trips no-unused-vars. PR #5777. * fix(serve,chrome-ext): more CDP-tunnel review fixes - sidepanel: pass the bearer token through the Web Shell URL fragment so a token-gated daemon doesn't 401 every framed request. - server: throw if deps.bridge is injected without deps.clientMcpSenderRegistry (the bridge is already wired to its own sender; a fresh one would be orphaned). - cdp-browser-emulator: surface unhandled browser-level CDP commands via an optional log sink (keep the empty-result ack, with a TODO). - cdp-bridge: only treat "already attached" as ours when attachedTabId === tabId (a foreign DevTools owner now errors), and detach the previous tab on switch so Chrome drops its debug banner. - build.js: add packages/chrome-extension to the build order so root build exercises the extension bundle. PR #5777. * fix(serve,chrome-ext): harden CDP-tunnel + client-MCP reverse channel - client-mcp-ws: cap registered servers per connection (max 10) and re-check `disposed` after the provider round-trip so a WS close mid-register can't leave a zombie server; add registrar serverCount(). - acp-http: rate-limit client-MCP frames (mcp_register/unregister at the mutation tier, mcp_message at read) and cap concurrent fire-and-forget register/unregister dispatch (max 8) to stop DoS amplification. - cdp tunnel: on /cdp puppeteer disconnect send a `cdp_release` frame so the extension detaches chrome.debugger instead of leaving the tab's debug banner up until /acp dies. - acpAgent: skip chrome-devtools auto-registration (with a diagnostic) when the /cdp tunnel requires bearer auth — the ACP child can't authenticate to it. PR #5777. * fix(chrome-ext,serve): address second-round CDP-tunnel review - service-worker: only the *active* socket's close tears down the bridge — a stale daemon-forced close must not detach the new connection's debugger. - daemon config + sidepanel: fail closed on a non-loopback baseUrl so a tampered chrome.storage value can't exfiltrate the bearer token off-host (background fetch/WS bypass host_permissions). - sidepanel: reentrancy guard on tick() so overlapping slow probes don't burn the framed-miss tolerance and flash the welcome screen mid-chat. - cdp-bridge: reentrancy guard on handleAttach so overlapping cdp_attach frames can't interleave teardown and corrupt attachedTabId. - add cdp-ws.test.ts: regression cover for no-bridge reject, second-client reject, onExtensionGone fail-fast, cdp_release on dispose, and the superseded-bridge cleanup guard. PR #5777. * fix(serve,webui): address third-round CDP-tunnel review - safe-ws-send: wrap the debug-mode writeStderrLine in try/catch so a broken stderr (EPIPE on a piped/closed log) can't break the "never throw on a dead socket" contract production callers rely on. - ChromeToolCall: index-access rawInput['name'] to satisfy noPropertyAccessFromIndexSignature. PR #5777. * fix(serve,chrome-ext): address #5777 review round 4 - acp-http: send mcp_error back on an unexpected client-MCP handler rejection (register/unregister callers otherwise hang); log when the inflight cap rejects. - cdp-ws: log the happy-path cdp_release dispatch for oncall tracing. - run-qwen-serve/acpAgent: don't pass a bogus "0" CDP-tunnel port for ephemeral --port 0, and emit a stderr diagnostic when the tunnel is disabled for a missing/invalid port instead of failing silently. - cdp-bridge: handle a cdp_release that races an in-flight handleAttach so a late attach can't leave a debugger attachment with no live /cdp client. - service-worker: close the WS on an ACP initialize error so the daemon doesn't keep holding a non-functional CDP bridge. * fix(serve,chrome-ext): address pr-review findings (#5777) - client-mcp-sender-registry: gate the child-side runtime-server teardown on ownership too (Config.removeRuntimeMcpServer is not owner-scoped), so a disconnecting connection can't kill a server a later connection re-registered under the same name. (P2) - service-worker: carry the bearer token via the `qwen-bearer.*` WS subprotocol (matching the web-shell + daemon decoder) instead of a `?token=` query the daemon never reads, so a token-gated daemon no longer 401-reconnect-loops. (P3) - run-qwen-serve: advertise client_mcp_over_ws / cdp_tunnel_over_ws in the bootstrap /capabilities too, matching the runtime path. (P3) * chore(webui): remove unused ChromeToolCall component (#5777) ChromeToolCall was added in a debugging commit but is never wired into the tool-call routing (getToolCallComponent) — there's no `chrome` tool kind and the only references were barrel re-exports. Dead code; remove it. * fix(serve,chrome-ext): address #5777 review round 5 (diagnostics) - service-worker: log the WS close code/reason so failure modes aren't indistinguishable (e.g. the daemon's 1011 "no extension connected"). - acpAgent: containment-check the resolved chrome-devtools-mcp bin path so a malformed `bin` field can't escape the package dir. - cdp-tunnel-registry: log when a new extension bridge supersedes a stale one. * fix(chrome-ext,serve): address #5777 review round 6 - cdp-bridge: ack the attach (as an error) before tearing down on a release-during-attach, so the daemon's reverse link doesn't hang ~170s waiting for a cdp_attached that never arrives. - acp-http: rename isFireAndForget -> dispatchOffQueue + clarify the comment; register/unregister are dispatched off-queue but still expect a response ack, so the name no longer reads as "no response." * fix(serve,cli): address #5777 review round 7 - config: warn (stderr) when QWEN_SERVE_CDP_TUNNEL_OVER_WS overrides an explicit tools.computerUse.enabled=true, so the effective config isn't a silent surprise. - client-mcp-ws: document the intentional idempotency of handleUnregister. - cdp-reverse-link: add tests for the attach gate — forwardToTab parks behind an in-flight attach, the cdp_attach timer rejects, and the gate opens on timeout so commands don't hang. * fix(serve,core): address #5777 review round 8 - mcp-client: only treat the first stdio arg as a local entry script when it is clearly a filesystem path. A bare includes('/') also matched scoped npm package names (npx @scope/pkg), wrongly resolving them under the workspace and throwing before the runner ran. Adds a regression test. - client-mcp-sender-registry: reject shadowedSettings so a browser-hosted WS client cannot shadow a user-configured MCP server name; roll back the child-side add. - cdp-ws: close the puppeteer socket when /cdp attach fails so dispose() clears cdpBound/routeInbound, instead of a stuck tunnel until restart. * fix(serve,chrome-ext): address #5777 review follow-ups * fix(serve): satisfy CDP inbound frame guard typing * fix(serve): address CDP tunnel review feedback --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
a1bcda6c1b |
feat(desktop): voice dictation in the desktop app (#5856)
* feat(desktop): voice dictation in the desktop app
Bring the `/voice` dictation feature to the desktop app (#5796), matching the
Web Shell (#5755). The renderer captures the microphone and streams raw 16 kHz
mono PCM to a loopback `/voice/stream` WebSocket in the Electron main process;
the server transcribes by reusing the CLI voice pipeline (batch + realtime) with
the qwen credentials, so provider keys never reach the renderer.
- server-core/voice: a standalone loopback WS server (separate from the RPC
server, shares its token via the `?token=` query) + ported batch/realtime
transcription pipeline + SSRF guard.
- Credentials resolve from the qwen CLI config: OAuth (`~/.qwen/oauth_creds.json`)
→ API key (`~/.qwen/settings.json` DashScope provider) → env.
- Renderer: a mic in the composer toolbar that swaps to a recording bar
(waveform + timer) while dictating; transcript is inserted for review.
- Voice on/off and model are configurable in Settings → Input and the composer
model dropdown, persisted via a `voiceEnabled` / `voiceModel` setting.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(desktop): harden voice dictation review gaps
* test(desktop): update voice ipc channel inventory
* fix(desktop): hide voice stream internals from errors
* test(desktop): cover voice websocket paths
* test(desktop): cover voice transcription paths
* fix(desktop): detect voice capture frame drops
* fix(desktop): address voice review blockers
* fix(desktop): address voice security blockers
* fix(desktop): address voice review blockers
* fix(desktop): address voice review blockers
* fix(desktop): address voice shutdown review
* fix(desktop): address voice review blockers
* fix(desktop): fix server-core voice typecheck and harden config resolution
Fix three type errors that left server-core's typecheck red:
- type the readQwenJson test mocks generically (`<T,>() => ... as T | undefined`)
to match the real `<T>(file) => Promise<T | undefined>` signature
- replace `HeadersInit` (undefined under lib ESNext/no DOM) with
`RequestInit['headers']`
- widen the fetch stub cast to `as unknown as typeof fetch` (the stub lacks
`preconnect`)
Also apply small hardening to resolve-voice-config:
- normalizeBaseUrl strips userinfo so a user:pass@host base URL can't leak
credentials into outbound requests or errors
- OPENAI_API_KEY without OPENAI_BASE_URL now throws a specific message instead
of a generic no-credentials error
- readQwenJsonFromDisk honors QWEN_HOME for the config base dir
Co-Authored-By: Qwen-Coder <[email protected]>
* fix(desktop): address voice review follow-ups (logging, dedupe, truncation)
- voice-ws-handler: tag connection logs with a short per-connection id so
concurrent sessions are distinguishable; log unrecognized text frames; add
bufferedBytes/pcmChunks context to the finalize "no session/audio" warnings.
- voice-frame-sender: fire onTooManyDroppedFrames once as the threshold is
first crossed instead of on every later dropped frame.
- qwen-asr-realtime-session: rename `failed` -> `settled` for parity with
voice-stream-session (the flag is set on both fail and the success path).
- voice-stream-session / qwen-asr-realtime-session: drop the redundant outer
slice in formatServerErrorMessage so sanitize's truncation `...` indicator
survives (credential redaction unchanged).
- voice-server: drop the dead `if (!isEnabled())` wrapper that the early
return above already makes unconditional.
- voice-stream-session test: cover the result-generated sentence_end commit
branch and the lastPartial reset (multi-sentence path).
Co-Authored-By: Qwen-Coder <[email protected]>
* fix(desktop): normalize QWEN_HOME and scope voice pending-op cap to control frames
Voice config resolution read the raw QWEN_HOME env value, so QWEN_HOME=~/x or
a relative value pointed desktop voice at a different directory than the rest
of Qwen (which normalizes via core's Storage.getGlobalQwenDir) and missed the
OAuth/settings credentials. Mirror core's normalization in a local helper:
expand a leading ~, resolve a relative path to absolute, and fall back to
~/.qwen when QWEN_HOME is unset/empty.
Also scope the per-connection pending-operation cap to control frames. PCM
frames buffered behind a slow upstream connect (up to the 8s connect timeout)
no longer inflate the count cap and falsely trip "Too many pending voice
messages." — buffered audio is already bounded by queuedBytes. The drain gate
(pendingOperations) still counts every message, so close/slot-release
semantics are unchanged.
Co-Authored-By: Qwen-Coder <[email protected]>
* fix(desktop): reject credentialed voice base URLs; scope batch byte cap to batch
Address qwen-code-ci-bot review on the desktop voice ASR handler.
- resolve-voice-config: normalizeBaseUrl now throws on embedded userinfo
instead of stripping it. For `https://[email protected]/...` the URL
parser already resolves `evil.com` as the host, so clearing userinfo
afterward would silently proceed with an attacker-controlled host. A
legitimate voice base URL never carries credentials.
- voice-ws-handler: MAX_AUDIO_BYTES (~5.5-min / 10 MB) is the batch WAV/file
cap. Streaming forwards frames immediately and is bounded by
MAX_CONNECTION_MS (the 6-min hard timer) + queuedBytes, so counting
already-forwarded frames toward the batch cap cut legit streams off ~30 s
early. Enforce the file cap for batch sessions only; the batch 10 MB cap
is unchanged.
Co-Authored-By: Qwen-Coder <[email protected]>
* fix(desktop): redact voice stream errors; drop unused qwen:// origin; re-fire voice URL retry
Address voice review on
|
||
|
|
f876f4a250 | ci: allow longer PR review timeout retries (#5961) | ||
|
|
de706203c0 |
feat(core,cli,sdk): resume an interrupted turn without a synthetic "continue" message (#5030)
* feat(core): add continueInterruptedTurn to resume unfinished turns from history Detect how a session's last turn ended from persisted chat history alone (no in-memory request refs, so it works across process restarts) and continue it without injecting any synthetic user message: - interrupted_prompt (orphaned trailing user entry): re-submit the same content via SendMessageType.Retry — strip + re-push, same logical turn. - interrupted_turn (dangling functionCall): close each pair with a synthesized error functionResponse submitted as a ToolResult, the same continuation signal a real tool completion would have sent. - none: idempotent no-op (null stream). Core layer for the SDK/ACP "continue previous turn" capability discussed in #4679; protocol exposure (_qwen/session/continue) comes separately. * feat(cli,sdk): expose continue-interrupted-turn through ACP and SDK control protocol ACP surface: - New vendor ext method `qwen/session/continue` (acpAgent) backed by Session.continueTurn(): classifies the last turn from history and re-drives the model loop — Retry semantics for an orphaned trailing user entry, synthesized error tool results for dangling tool calls. No new user message enters the transcript; the prompt id reuses the interrupted turn's number. 409 when a turn is already in flight. - The model-call ↔ tool-call loop is extracted verbatim from #executePrompt into #runModelTurnLoop (+ #finishTurn epilogue) and shared by both paths. - initialize advertises the capability via agentCapabilities._meta (qwen.sessionContinue) per ACP extensibility guidance. SDK surface (stream-json control protocol): - New control request subtype `continue_last_turn` routed to SystemController and delegated to the nonInteractive Session, which schedules a continuation turn ahead of queued user messages. - runNonInteractive gains `continueInterrupted` mode: detection + Retry/ToolResult first-turn override; reminder injection is skipped so tool_result parts stay at the head of the user message. - Query.continueLastTurn() sends the request; reply reports { accepted, interruption } and resumed output flows as regular stream messages. Part of the #4679 continue-previous-turn work; core detection landed in the previous commit. * fix(core): replay the orphaned user entry verbatim, reminders included The Retry send path does not re-inject per-turn system reminders, so filtering them out of the captured entry made the resumed request differ from the original (the date reminder vanished). A continuation is the same turn, not a fresh one — capture the trailing user entry byte-for- byte so the resumed wire request is identical to the interrupted one. Verified end-to-end against a request-capturing mock server. * fix(cli): re-trigger work loop for pending continue turns; cover continueInterrupted - ensureProcessingStarted's finally-block re-trigger condition omitted pendingContinueTurns, so a continue request arriving as the work loop wound down could be stranded until the next user message. Add it to the guard alongside the other queues. - Add runNonInteractive({continueInterrupted}) unit tests: Retry path, ToolResult (interrupted_turn) path, and the clean-turn no-op. * fix(core,cli): restore orphaned entries when a continued send is skipped When resuming an interrupted turn, the orphaned trailing user entries are stripped from history before the content is re-sent. If that send is then skipped (e.g. session token limit exceeded), the stripped content was lost from history entirely. Have strip return the removed entries and restore them on any non-cancelled skip across the core retry, non-interactive, and ACP continue paths. On the ACP path the turn loop already re-adds functionResponse parts of an unsent message via #preserveUnsentMessageHistory, so a tool_result orphan would be written twice (once by preserve, once by restore). Let the caller own restoring the initial message and skip the loop's functionResponse-only preservation for that one message when it isn't a cancellation. * fix(cli,sdk): address continuation review feedback * fix(cli): address continuation review feedback * fix(cli): preserve continuation constraints and drain state * Merge latest main into feature branch * fix(core,cli): harden continuation recovery * fix(cli): close continuation review gaps * chore(cli): restore acp bridge path mapping * fix(cli): reject continuation during background turns * refactor(cli): simplify continuation handling * fix(cli): harden interrupted turn continuation * refactor(cli): share continuation helper logic * fix: harden interrupted-turn continuation * chore: remove unrelated continuation diff churn * refactor(cli): drop ACP session-continue surface from PR #5030 Issue #4679 only needs the SDK/stream-json continuation path. The SDK's Query.continueLastTurn() rides the stdio control protocol handled by the nonInteractive session, not ACP, and the qwen/session/continue vendor method had no SDK caller. Revert the ACP continuation surface (Session.continueTurn, #executeContinue, the qwen/session/continue routing and capability advertisement, and their tests) to the merge-base so the PR ships only the path the issue requires. * refactor(core): rename turnInterruption to kebab-case turn-interruption check-file/filename-naming-convention requires kebab-case for new files (AGENTS.md), so turnInterruption.ts failed CI lint. Rename the module and its test and update the index re-export. * fix(core,cli): delegate interrupted-prompt continuation to the Retry path The continuation path duplicated the orphan strip/restore that the Retry send in client.ts already performs, and its restore guard flipped on pre-push stream events, so an abort before the history push could permanently drop the orphaned user entry. Delegate to the Retry strip/restore, whose guard flips only after the content is committed. Removing the local strip drops the now-dead maxEntries parameter from stripOrphanedUserEntriesFromHistory, and detection now reads the full history (getHistory) so its re-submitted parts match the unbounded strip. Also fire onResultEmitted only after a successful emit, so a throwing emitResult cannot lose both the result and the error. * test(cli): cover insertAfterFunctionResponses ordering Add direct unit coverage for the splice helper used by the continuation reminder insertion: mixed parts, all-functionResponse input, empty additions, and empty parts. * feat(cli,acp,sdk): expose continue-last-turn to daemon + ACP and fix continuation review findings Surface the interrupted-turn continuation through two more entry points, both routing into a new Session.continueLastTurn() that reuses the core detectTurnInterruption classification and the existing send/tool loop: - daemon: new control method qwen/control/session/continue (status -> bridgeTypes -> bridge -> POST /session/:id/continue -> acpAgent handler), returning { accepted, interruption }. - ACP: a `qwen.daemon.continueLastTurn` prompt _meta flag, mirroring the existing daemon retry meta. interrupted_prompt re-submits the stripped orphaned run; interrupted_turn closes dangling tool calls with synthesized error functionResponses. System reminders are now inserted after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (normal prompts unchanged). Also resolve the open continuation review findings: - core/client: history-length snapshot guard so a Retry send that fails before the first event never duplicates history (replaces the retrySendStarted flag that missed pre-event failures). - cli/nonInteractive/session: emit a terminal error result when an accepted continuation is abandoned by shutdown; emit a continue_turn_failed diagnostic when a continuation fails after a result was already flushed. - sdk/query: idempotent finishTransportRead with optional error, also invoked on the message-router error path, so a transport crash rejects pending control (continueLastTurn) and MCP requests instead of hanging. Adds regression tests across core/cli/sdk for each fix and the new surfaces. * test(sdk): cover finishTransportRead MCP-response rejection on teardown Addresses the review ask to validate that finishTransportRead rejects pending MCP responses when the transport read loop ends. White-box: injects a pending entry rather than standing up a full SDK MCP server handshake, since the rejection drain is map-internal regardless of how the entry was registered. * fix(acp): harden continueLastTurn — restore stripped orphan on send failure, fix ordering and error surfacing Review follow-ups on the ACP/daemon continuation path: - interrupted_prompt: preserve the full stripped orphan message back into history when a continuation send fails non-cancelled. Previously only functionResponse parts were kept, so a stripped text orphan was lost — the user's unanswered turn vanished from history. - worktree restore notice: insert after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (Anthropic ordering), matching the system-reminder insertion. - continueLastTurn fire-and-forget: emit a client diagnostic on failure instead of log-only, so a failed continuation is visible rather than silently stalled. - log at warn when the in-prompt re-detection finds no interrupted turn. Adds a Session regression test asserting the orphan is preserved on a non-cancelled continuation send failure. * perf(acp,core): classify continuation from a bounded tail; O(1) retry length read Review follow-ups (all non-blocking): - continueLastTurn classifies from a shallow bounded tail (getHistoryTailShallow) instead of structuredClone-ing the entire history. The authoritative re-detection inside the fired prompt() still reads full history for the strip, matching the nonInteractive two-tier pattern. - the Retry history-length guard reads getHistoryLength() (O(1)) instead of getHistory().length, which structuredClones the whole history just to read .length (the truncateHistory path already warns about this). Adds a Session test for the pendingPrompt re-entrancy guard (continueLastTurn rejects while a prompt is in flight). * fix(acp-bridge): strip daemon continue meta key from external prompts An external POST /session/:id/prompt could set _meta['qwen.daemon.continueLastTurn'] and trigger a continuation through the prompt FIFO, bypassing continueLastTurn()'s admission guards. sendPrompt now strips it alongside the retry meta key; the legitimate path is the agent's internal Session.prompt() and does not go through sendPrompt. * fix(cli): resolve continuation review comments - Session.ts: reuse the shared insertAfterFunctionResponses helper for both the reminder and worktree-notice phases (removes the duplicated findIndex pattern) and guard against duplicate reminders on an interrupted_prompt continuation, mirroring nonInteractiveCli.ts. Adds an ordering test. - systemController.ts: point the missing-callback error at the wiring (onContinueLastTurn on ControlContext) instead of 'not available'. - nonInteractive/session.ts: tag the continue logs with sessionId. * fix(core): gate retry restore on a push counter, not history length The Retry strip/restore guard compared history length to a post-strip baseline to decide whether the re-submitted user content landed. Auto- compression inside GeminiChat.sendMessageStream runs before the push and shrinks history independently of it, so the length could fall below the baseline even after a successful push, restoring the stripped entries and duplicating the user's prompt (reachable on success once compaction removes >=2 entries, common near the token limit). GeminiChat now exposes a monotonic userContentPushCount (incremented when the user content is pushed, decremented only if that push is rolled back on a setup error). client.ts snapshots it after the strip and restores only when it did not advance — invariant under compression. * fix(sdk): log pending requests when the transport finalizes finishTransportRead rejected pending control/MCP requests with no anchor log, so a CLI crash mid-continuation left only scattered rejections. Emit one correlatable line with the pending counts and error when work was in flight. * fix(serve): track daemon continuation through the prompt-admission path POST /session/:id/continue ran the continuation as an untracked internal agent prompt (continueLastTurn fired Session.prompt() fire-and-forget via the sessionContinue control method). The bridge never set pendingPromptCount / promptActive / activePromptOriginatorClientId for it, so the daemon reported the session idle during a continuation, a racing POST /prompt was admitted with no FIFO wait and aborted the in-flight continuation, and the continuation's events went out unattributed. Now continueLastTurn is the accept/reject pre-check only; when it accepts, bridge.continueSession drives the actual turn through sendPrompt with a trusted continue flag (re-arming the continue meta key that sendPrompt strips from untrusted callers), so the continuation gets full admission tracking, FIFO serialization, originator attribution, and turn-complete broadcasting. The /continue route forwards X-Qwen-Client-Id and continueSession validates it up-front, matching POST /session/:id/prompt. The {accepted, interruption} response contract is preserved. Addresses the continuation-lifecycle review finding. * test(serve): cover continuation admission tracking end-to-end - bridge: assert a continuation reports the session BUSY (hasActivePrompt + pendingPromptCount) and that a racing prompt queues behind it via the FIFO instead of aborting it — the two behaviors the lifecycle fix targets. - serve: assert POST /session/:id/continue forwards X-Qwen-Client-Id to continueSession so the continuation turn is attributed like a prompt. * test(serve): add real-daemon smoke for POST /session/:id/continue Exercises the full route → bridge → agent continuation path against a spawned daemon: a fresh session has no interrupted turn, so /continue returns {accepted:false, interruption:'none'} (model-free, like the prompt-admission checks). Also fixes the now-stale DAEMON_CONTINUE_META_KEY comment to reflect that continueSession (not continueLastTurn) re-arms the meta via sendPrompt. * refactor(acp-bridge): drop dead AbortController in continueSession; harden reject test - continueSession passes undefined instead of a never-aborted AbortController to sendPrompt (continuations cancel via the cancelSession route, not this handle). - The 'rejected continueSession does not dispatch' test waits long enough for a regression fire-and-forget to reach the agent before asserting none ran, instead of a single microtask flush. * fix(cli): restore orphaned turn when a continuation send throws The interrupted_prompt continuation strips the orphaned user run from history before re-sending it. #preserveUnsentMessageHistory only restores on a graceful null-stream result, not when the send throws (e.g. a hard-rescue compression error before the user-content push) — so the orphan was permanently lost (transcript gap; --resume loads history missing the user's turn). Capture the stripped entries + a GeminiChat push-count snapshot, and restore them in the send-loop catch when the count did not advance (mirrors the core Retry restore in client.ts) — so a later tool-loop failure, where the orphan already landed, does not double-restore. Also emit conversation_finished on the no-op continuation early-return, which sits before the send-loop try/finally. * fix(serve): surface continuation admission failures + mirror prompt replay contract continueSession previously swallowed post-accept admission failures (queue-full, pre-aborted, session reaped) yet returned accepted:true, leaving SDK callers waiting for output that never arrives. Now it dispatches sendPrompt synchronously so an admission throw propagates (the caller gets an error, not a false accept); only post-admission turn failures are logged (now via teeServeDebugLine, so they reach the SSE diagnostic stream). It also mirrors POST /session/:id/prompt's replay/correlation contract: capture lastEventId + accept a route-generated promptId and return both on accept, so a client attaching the SSE stream can replay missed events and correlate turn_complete/turn_error. Adds tests for the combined retry+continue meta strip, nonexistent-session and async-failure error paths, and the new contract fields. * chore(core): log retry-restore decision for diagnostics restoreStrippedRetryEntries mutates history in a finally with no trace; a single debugLogger.info on the restore branch makes a future push-counter regression (silent duplicate/loss) diagnosable. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
60cf2556df |
refactor(cli): Remove serve bridge re-export shims (#5955)
* codex: address PR review feedback (#5937) Co-authored-by: Qwen-Coder <[email protected]> * refactor(cli): remove serve bridge re-export shims Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5955) Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
3ce46949c2 |
fix(core): halt repeated shell inspection variants (#5944)
* fix(core): halt repeated shell inspection variants * test(core): cover shell-stagnation streak reset, fix guard-count doc Address review on #5944: the checkAlwaysOnSafeties JSDoc still said it enforces two guards after the shell inspection-command stagnation guard was added, so correct it to three. Add a regression test proving a non-inspection tool call resets the shell-stagnation streak to zero, which is the guard's main false-positive defense and was previously untested. * fix(core): exclude write-bearing git chains from stagnation bucket isGitInspectionCommand matched if any segment of a shell chain was a git status/diff/ls-files, so a productive chain like `git add . && git status && git commit` was classified as read-only inspection. Eight such commands would falsely trip the always-on shell-stagnation guard. Require every segment of the chain to be a read-only git inspection before bucketing. Mixed chains that also write fail open (non-inspection), the safe direction for an always-on halt. The #4695 loop is still caught: its compound case is status && diff, both read-only. * fix(core): halt repeated shell inspection variants * fix(core): treat path-specific git diffs as progress * test(core): cover shell stagnation guard boundaries * test(core): address shell stagnation review feedback * docs(core): sync loop guard comments |
||
|
|
680f523853 |
ci(review): increase PR review timeout from 90 to 120 minutes (#5959)
Large PRs with extensive diffs across multiple packages can exceed the 85-minute effective review window (90 - 5 buffer). The recent run #28318973107 timed out on PR #5777 which touched acp-bridge, serve, config, and chrome-extension packages. Bump both the job timeout-minutes and the validation cap to 120 so the review agent has enough headroom for complex multi-package PRs. |
||
|
|
8daeb5b1f9 |
feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025) (#5868)
* feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025) Add two features requested in issue #4025: 1. Configurable auto-compact threshold via settings.json - Add context.autoCompactThreshold setting (0-1, default 0.7) - Extend computeThresholds(window, pct?) to accept optional pct parameter - Wire all 4 call sites (chatCompressionService, geminiChat, contextCommand, useContextualTips) - Large windows (>110K) dominated by absolute branch, custom threshold mainly affects small windows 2. Stop hook stdin payload includes context usage data - Add ContextUsageData interface and buildContextUsage helper - Extend StopInput with context_usage, context_limit, input_tokens fields - Wire 3 callers (Session.ts, client.ts, config.ts) - Enables hook scripts to observe context usage and suggest compact strategies * fix(test): add getAutoCompactThreshold mock to geminiChat.test.ts, add NaN guard to buildContextUsage * fix(review): address round 4 findings — schema constraints, Partial<ContextUsageData>, buildContextUsage validation * docs: add context.autoCompactThreshold and Stop hook context usage fields documentation * fix(review): add contextWindowSize fallback, pct clamp, doc accuracy, threshold propagation test * fix: correct warn value in contextCommand test comment * test(chatCompressionService): fix misleading pct=1 test name and assertion * fix(chatCompressionService): prevent negative warn threshold for low pct values * docs(chatCompressionService): update JSDoc warn formula to include max(0, ...) floor * test(chatCompressionService): add pct clamping tests and fix NaN handling Add tests for out-of-range pct values (-0.5, 1.5, NaN) to verify computeThresholds clamping behavior. Fix implementation to use Number.isFinite() check so NaN falls back to DEFAULT_PCT instead of propagating through Math.max(0, NaN) which yields NaN. * test(config): add MCP Stop dispatch validation tests Add tests for buildContextUsage runtime validation in MCP Stop dispatch path: - Valid numeric inputs produce correct ContextUsageData - Missing/undefined fields return undefined - String values rejected by Number.isFinite validation - Negative values return undefined Also add Number.isFinite check for contextWindowSize in buildContextUsage to properly validate MCP input types at runtime. * fix(chatCompressionService): fix TypeScript type narrowing for pct parameter Use explicit undefined check before Number.isFinite to properly narrow the number | undefined type in the ternary expression. --------- Co-authored-by: 俊良 <[email protected]> |
||
|
|
7f71a8512d |
docs(daemon): refresh daemon docs for recent PRs (wave 2) (#5954)
* docs(daemon): update developer docs for recent daemon PRs - Add Last-Event-ID client reconnect guide (10-event-bus, 13-sdk-daemon-client) - Add cross-connection vote routing section (04-permission-mediation) - Add new capability tags: daemon_status, workspace_permissions, workspace_trust, workspace_github_setup, workspace_voice, workspace_voice_transcription, voice_transcribe (11-capabilities-versioning) - Add new event types: trust_change_requested, github_setup_completed, extensions_changed, mid_turn_message_injected (09-event-schema) - Fix _meta.serverTimestamp source description (09-event-schema, 10-event-bus) - Fix async function* syntax in SDK example (13-sdk-daemon-client) - Sync event/capability counts across all docs (43->47 events, 67->75 tags) * docs(daemon): add workspace remember design doc (PR #5884) Design document for the sessionless workspace remember API proposed in PR #5884. Covers API endpoints, task lifecycle, implementation details, events, error handling, and SDK integration. Status: Proposed (not yet merged). |
||
|
|
8b65a555a0 |
chore(release): v0.19.3 (#5952)
* chore(release): v0.19.3 * docs(changelog): sync for v0.19.3 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
877e8ab777 |
fix(desktop): normalize source slug validation errors (#5911)
* fix(desktop): normalize source slug validation errors * fix(desktop): address source slug review feedback --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
277673794a |
fix(core): isolate Anthropic SDK abort listener leak with per-request child controllers (#5946)
The Anthropic SDK's fetchWithTimeout (core.mjs) registers an 'abort' listener on the caller-provided signal and never removes it, the same leak #4810 isolated for the OpenAI generator. Both generateContent and generateContentStream passed request.config?.abortSignal, typically a long-lived session/turn signal, straight to client.messages.create (and the streaming path also to the empty-stream fallback probe), so every request left a dead listener behind and they piled up over a session. Wrap the caller's signal in a per-request child (createChildAbortController) on both paths, hand the child to the SDK and the fallback probe, and abort it once the request completes or the stream is drained or abandoned. The leaked listener then lands on a short-lived child instead of the caller's signal, and the child's own parent listener is removed on cleanup. This mirrors the OpenAI pipeline fix (execute and executeStream). Adds regression tests that drive five requests over one caller signal (streaming and non-streaming) and assert no abort listeners remain on it, plus a test that a caller abort still propagates to the per-request child. |
||
|
|
8f1118a179 |
feat(web-shell): add 'voice' to ComposerToolbarAction for external visibility control (#5947)
Co-authored-by: ytahdn <[email protected]> |
||
|
|
c458830db4 |
feat(web-shell): add mobile responsive view for TodoPanel (#5948)
- Desktop (>=600px): show full text 'Step 3 / 8' or '第 3 / 8 步' - Mobile (<600px): show compact text '3 / 8' with progress ring only - Add todo.stepFraction i18n key for both EN and ZH - Reduce summary padding on mobile for better space usage Co-authored-by: ytahdn <[email protected]> |
||
|
|
0dc631b631 |
feat(web-shell): add error boundaries so a render crash can't white-screen the embed (#5943)
web-shell ships as an embeddable React component, so an uncaught render error anywhere in the tree (Markdown / KaTeX / Mermaid / a tool panel) unmounts the whole React root and blanks the host page. Add two layers of isolation plus a generic boundary: - ErrorBoundary (components/ErrorBoundary.tsx): generic class boundary with reset-on-key recovery, so a streamed/edited/retried subtree recovers on its own while a stably-broken one stays on the fallback without looping. - Message level: each MessageItem body is wrapped, so one bad message degrades to an inline notice instead of taking down the transcript. The notice is role-aware (right-aligned for user messages, left for assistant) so it still reads as the right turn and doesn't blur turn boundaries. - Top level: the WebShell lib entry and the standalone main.tsx wrap <App> in a self-contained RootErrorFallback (no provider/theme/i18n dependency, inherits host text color) offering a retry, so an App-level crash shows a recoverable surface instead of a blank page. Adds message.renderError strings (en + zh-CN) and unit tests covering the boundary mechanism and the root fallback + retry path. |
||
|
|
a6ea33f203 |
fix(serve): reject non-positive sessionRecapAwayThresholdMinutes values (#5945)
* fix(serve): reject non-positive sessionRecapAwayThresholdMinutes values Fixes #5680. The away threshold is a duration in minutes, so 0 or a negative value is meaningless and is effectively ignored at runtime, leaving the persisted value out of sync with what takes effect. Bound it to minimum 1 in the settings schema (the ACP schema already uses min: 1), mirroring #5906 which added minimum: 0 to cleanupPeriodDays. Add workspace-settings API tests covering reject (0/-1) and accept (1/5). Signed-off-by: Yufeng He <[email protected]> * chore(schema): regenerate settings.schema.json for sessionRecapAwayThresholdMinutes minimum --------- Signed-off-by: Yufeng He <[email protected]> |
||
|
|
c7020907c1 |
fix(core): preserve the selected model when re-applying a provider install plan (#5835)
* fix(providers): preserve user-selected model on re-install/re-auth (#5819) * test(cli): add getValue stub to settings mocks broken by install-plan fix * test(cli): add getValue stub to settings mocks broken by install-plan fix * fix(providers): address review feedback on preserve-model PR - fix stale model.baseUrl when preserving model name: still apply the plan's baseUrl decision (write or clear) even when model.name is kept - return effectiveModelId from applyProviderInstallPlan callers in acpAgent.ts and run-qwen-serve.ts by reading the adapter's post-apply model.name value instead of plan.modelSelection?.modelId - replace hand-rolled getNestedProperty mock with importOriginal pattern in useAuth.test.ts and useProviderUpdates.test.ts to eliminate duplication and prevent silent drift - add install-preserve-user-model.test.ts to the diff so reviewers can run automated confirmation without dropping an external file * fix(providers): address review feedback on preserve-model PR - fix stale model.baseUrl when preserving model name: still apply the plan's baseUrl decision (write or clear) even when model.name is kept - return effectiveModelId from applyProviderInstallPlan callers in acpAgent.ts and run-qwen-serve.ts by reading the adapter's post-apply model.name value instead of plan.modelSelection?.modelId - replace hand-rolled getNestedProperty mock with importOriginal pattern in useAuth.test.ts and useProviderUpdates.test.ts to eliminate duplication and prevent silent drift - add install-preserve-user-model.test.ts to the diff so reviewers can run automated confirmation without dropping an external file * style: fix indentation in run-qwen-serve.ts installAuthProvider * fix(providers): use isSameModelIdentity for model retention check and fix acpAgent test mock * fix: remove duplicate baseUrl in acpAgent providers/connect response * fix(test): preserve settings reference in acpAgent createLoadedSettingsAdapter mock * test: add coverage for planOffersCurrentModel and effectiveModelId * fix(test): fix TypeScript cast in acpAgent preserved model test * fix(serve): read effective model/baseUrl from adapter in installAuthProvider * style: fix prettier formatting |
||
|
|
714513df20 | feat(channels): register Telegram bot command menu (#5919) | ||
|
|
34678cfbd9 |
Upgrade GitHub Actions to latest versions (#3683)
* Upgrade GitHub Actions to latest versions Signed-off-by: Salman Muin Kayser Chishti <[email protected]> * ci: restore ratchet annotations on upgraded actions The action version bumps had replaced '# ratchet:owner/repo@<ver>' annotations with bare '# vX.Y.Z' comments, which silently breaks ratchet's ability to track and update these SHA pins. Restore the annotations on every changed line (updated to the new major), matching each line's pre-existing convention on main. --------- Signed-off-by: Salman Muin Kayser Chishti <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: yiliang114 <[email protected]> |
||
|
|
fa8b2ad87a |
feat(acp): support /cd command in ACP sessions (#5903)
* feat(acp): support /cd command in ACP sessions Implements the server-side /cd command for ACP multi-session architecture: - Infrastructure: ext-method constant, bridge types, CdWhilePromptActiveError - Core: skipProcessChdir option in Config.relocateWorkingDirectory() - ACP child handler: trust check, fs validation, sandbox guard - Bridge: promptQueue-serialized changeSessionCwd with event publishing - HTTP route: POST /session/:id/cd with error-to-status mapping - directory_not_found → 400 - directory_not_trusted → 403 - restrictive_sandbox → 403 - session_busy (CdWhilePromptActiveError) → 409 SDK client wrappers and capability registration are deferred to a follow-up PR once the server-side implementation is validated end-to-end. Closes #5677 * qwen: address PR review feedback (#5903) - [Critical] Thread skipProcessChdir through prepareSessionArtifactMigration to prevent corrupting shared daemon process.cwd() on rollback in ACP mode - [Suggestion] Raise cd timeout to Math.max(initTimeoutMs, 30_000) for large-project fs ops and memory refresh - [Suggestion] Move errorKind-to-HTTP mapping (restrictive_sandbox, directory_not_found, directory_not_trusted) into sendBridgeErrorImpl; cd route catch block now matches /branch and /fork pattern * fix(acp): address review feedback on /cd implementation (#5903) Critical fixes: - Do not overwrite entry.workspaceCwd — session stays listed under the daemon's bound workspace; logical cwd lives in agent Config only - Queue tail tied to raw extMethod settlement, not timeout — subsequent prompts cannot run against unknown cwd state after caller timeout - Add realpath equality check for skipProcessChdir path to guard against TOCTOU symlink swaps between trust check and state update - Skip artifact migration for ACP /cd (skipArtifactMigration option) — storage stays at bound workspace so branch/load/lifecycle remain consistent Suggestions addressed: - Add response shape validation for extMethod result - Rename CdWhilePromptActiveError HTTP code from 'session_busy' to 'cd_while_prompt_active' (avoids collision with SessionBusyError) - Add null-byte path validation in ACP sessionCd handler - Add operator logging on successful cwd change (warnings included) * chore: revert unrelated cua-driver formatting, add to .prettierignore Reverts 23 cua-driver files that picked up Prettier formatting noise during merge conflict resolution. Adds packages/cua-driver/ to .prettierignore since it's a vendored package whose content should not be auto-formatted by local hooks. --------- Co-authored-by: qwen-code-dev-bot <[email protected]> |
||
|
|
8b5cb01a33 |
docs: add provider preset governance policy to CONTRIBUTING.md (#5631)
Define Tier 1 qualification criteria (affiliation disclosure, operational maturity, organic user demand, data/security transparency, maintenance commitment) and the custom-provider default path for providers that don't qualify. Also document the technical checklist for approved preset PRs. This formalizes the governance stance applied to recent third-party gateway PRs (#5060 TrustedRouter, #5625 PleumRouter). |
||
|
|
a93ec674e9 |
perf(cli): enable compile cache and defer getCliVersion for serve (#5938)
Enable Node.js compile cache (module.enableCompileCache) in the serve fast-path so V8 bytecode is cached across daemon restarts. Start getCliVersion() as a non-blocking promise and resolve it in parallel with the runtime module loads inside buildRuntime(), removing it from the pre-listen critical path. |
||
|
|
865581b559 | feat(cli): show scheduled task count in footer (#5921) | ||
|
|
2583b5c7dd |
feat(cli): add /model --vision for a fallback vision model (#5778)
* feat(cli): add /model --vision for a fallback vision model
Add `/model --vision <id>` and an interactive vision-model picker to
configure an image-capable model the vision bridge borrows when a
text-only main model receives an image, mirroring the `/model --fast`
pattern (flat `visionModel` setting + runtime `setVisionModel`). An
explicit vision model takes precedence over same-provider auto-select and
may cross providers; an unreachable pin falls back to auto-select rather
than firing the bridge at a model it can't reach.
Also fixes two vision-bridge issues surfaced while verifying #5126:
- The gate read stale modalities after a runtime model switch, so an
@image sent right after switching to a multimodal model could still
trigger the bridge. Refresh model-derived modalities on the qwen-oauth
hot-update paths (setModel default-model + handleModelChange) so the
gate reflects the current model.
- The transcription was appended after a now-empty "Content from <file>:"
header, leading the primary model to re-read the image file and ignore
the transcript. Stand the transcription in the image's slot and tell the
model the image cannot be read by a tool.
Closes #5597
* fix(cli): add vision model description translations
* feat(cli): warn on non-image-capable /model --vision pin; cover modality refresh
Self-review follow-ups on the /model --vision work:
- Warn (but still honor) when /model --vision or the picker pins a model that
isn't image-capable, reusing the vision bridge's isImageCapable so the
explicit and auto-select paths agree on what counts as vision-capable.
isImageCapable is now exported for the CLI to share.
- Exclude fastOnly models from --vision validation so it matches the
completion filter.
- Add a Config-level test asserting handleModelChange refreshes modalities on
the qwen-oauth hot path (the vision-bridge gate fix that lacked direct
coverage).
* fix(cli): translate the non-image-capable vision model warning
Add zh/zh-TW/en entries for the /model --vision picker warning shown when a pinned model isn't image-capable, matching the existing vision-string translations.
* fix(vision-bridge): stop showing the image transcription twice
The vision bridge printed the full transcription in its notice AND fed it to
the primary model, which then re-stated it — so the user read the same image
description twice (once in the notice box, once in the answer).
- The notice now shows only the egress-disclosure header; the transcription is
fed to the primary model and surfaced in its answer, so it is no longer
duplicated. Removes the now-unused transcript truncation / control-char
stripping helpers.
- The bridge model is instructed to transcribe/describe, not answer the user
request (the user's intent is carried as a focus hint, not a question), so its
output is context for the primary model rather than a second competing answer.
* fix(cli): register vision model i18n strings and cover /model --vision
- Register 'Vision Model' / 'Select Vision Model' in en/zh/zh-TW so the
vision picker and confirmation aren't English-only for non-English users
(mirrors the existing Voice Model entries).
- formatNonVisionModelWarning reuses the dialog's already-registered t()
key instead of a plain English literal, keeping both paths i18n-consistent.
- Add the missing bare '/model --vision' tests (interactive dialog +
non-interactive current-model report), mirroring the --voice coverage.
* refactor(vision-bridge): remove dead VisionBridgeResult.transcript field
The inline transcript display was dropped in
|
||
|
|
22389247d5 |
feat(web-shell): add optional session sidebar with navigation (#5931)
Co-authored-by: ytahdn <[email protected]> |
||
|
|
673454839e |
feat(memory): add a git-shared team memory tier (#5886)
* feat(memory): add a git-shared team memory tier Add an opt-in third auto-memory tier — TEAM — stored in the repository at `.qwen/team-memory/` and shared with collaborators through git. The private USER and PROJECT tiers are unchanged. - Enabled via the `memory.enableTeamMemory` setting (default off) or the `QWEN_CODE_MEMORY_TEAM` env override; inactive otherwise. - The model is taught a three-tier prompt and when to route saves to the shared tier (project-wide conventions, team-wide references). - Writes to the team directory are scanned for secrets (a curated gitleaks-style ruleset) and hard-blocked, since the directory is committed to the repo and visible to everyone with repo access. - Team writes default to 'ask' permission — not auto-allowed like the private tiers — so they are proposed for confirmation in the default approval mode and surface in the git diff for review. Co-authored-by: Qwen-Coder <[email protected]> * fix(vscode): update settings schema for team memory * fix(memory): harden team memory review gaps * fix(memory): address team-memory PR review feedback - secret-scanner: AWS key suffix is base62 ([A-Z0-9]), not base32 ([A-Z2-7]) — old class missed IDs with digits 0/1/8/9; add a positive test for such a key. - paths: document that isAnyAutoMemPath deliberately excludes team memory (security-load-bearing — team writes must stay 'ask'). - extractionAgentPlanner: name the team-memory dir in the scoped deny message so a denied team write is debuggable. - prompt: spell the tier count via a number-word lookup so a future 4th tier never silently reads "two". - team-memory-secret-guard: debug-log matched rule IDs only (never the secret content) when a write is blocked. - edit: when a blocked team write's secret already exists in the on-disk file, tell the user to delete the committed secret. - docs: caveat that a directory-form gitignore (.qwen/) makes the !-reinclude a no-op; use the file-glob form. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): harden team-memory secret guard on blocked writes and notebooks - write-file: blocked team-memory secret write now returns an `error` field (INVALID_TOOL_PARAMS) so the framework treats it as a failure instead of a silent success, mirroring edit.ts. - notebook-edit: scan the serialized .ipynb that hits disk through the team-memory secret guard before writing, blocking with the same `error` field; behavior unchanged for non-team-memory paths. - store: log non-ENOENT errors in readTeamAutoMemoryIndex before re-throwing, so EACCES/EIO/ELOOP no longer vanish via config.ts's best-effort `.catch(() => null)`. - config: emit a debug diagnostic when team memory is enabled but suppressed by the untrusted-workspace trust gate. Co-Authored-By: Qwen-Coder <[email protected]> * feat(memory): auto-generate the team memory index Generate the team `MEMORY.md` index by scanning the saved memory files instead of having the model hand-maintain it. The index is derived and ordered by path, so the committed file is deterministic across machines. This removes the git merge-conflict surface a hand-edited shared index would have, keeps the index consistent with the files, and frees the model from the two-step save for the team tier. Co-authored-by: Qwen-Coder <[email protected]> * feat(memory): opt-in git sync for team memory Add an opt-in, best-effort git sync of team memory at session start so a daemon can share team memory through the remote without manual git. Off by default; enabled with `QWEN_CODE_MEMORY_TEAM_SYNC=1`. It commits the team directory (only that path), pulls fast-forward-only, then pushes. `--ff-only` is deliberate: it never creates a merge commit or a conflict — a diverged branch is skipped rather than touching the working tree. All git is best-effort and never throws, so a failure cannot break a session. Commits can be attributed to the acting user via an optional author, for cooperative per-user attribution on a shared daemon. Co-authored-by: Qwen-Coder <[email protected]> * fix(core): close team-memory secret-guard regex + test gaps - secret-scanner: add `_` to the two T3BlbkFJ-marker char classes so legacy base64url OpenAI keys with underscores in the body no longer bypass the scanner; extend the OpenAI positive samples with an underscore key body. - edit: cover the pre-existing-secret branch — seed the on-disk file with a full detectable token, assert the distinct "already exists" message and that the blocked edit leaves the file untouched. - team-paths: cover the first-ever write before .qwen/team-memory exists, exercising realpathNearestExisting walking up to an existing ancestor. Co-Authored-By: Qwen-Coder <[email protected]> * feat(core): add Google OAuth client secret and SendGrid secret-scanner rules Extend the team-memory secret scanner with two more high-confidence, distinctive-prefix credential patterns (both gitleaks rules): - gcp-oauth-client-secret: GOCSPX- prefix + 24 base64url chars, placed next to the existing GCP AIza API-key rule. - sendgrid-api-token: SG. + 22-char id + . + 43-char secret. Both have a near-zero false-positive bar, matching the curated set's existing criteria. Add a load-bearing positive sample per rule. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): warn when team memory is enabled but not git-shareable When the team tier is active, emit a one-time startup warning if its directory is git-ignored (e.g. a directory-form `.qwen/` ignore that a `!`-reinclude cannot escape) or there is no git root — so the tier never silently shares nothing. Gated on the active (trusted + enabled) tier; probes a representative file path with `git check-ignore`, and is latched so refreshHierarchicalMemory re-runs do not re-emit it. Closes the last open review thread (wenshao + qwen-code-ci-bot both flagged the silent-inert case); the docs caveat already shipped. Co-Authored-By: Qwen-Coder <[email protected]> * feat(memory): dedup team index by description; fix empty-frontmatter parse The team index now collapses entries that share a (normalized) description into one line — listing the other files via "(also: …)" — so two people saving the same shared fact don't surface as duplicates. Nothing is dropped; every file path is preserved and the grouping is deterministic (input is path-sorted). Also fix a pre-existing frontmatter bug: parseFrontmatterValue used `\s*`, which crosses newlines, so an empty value (`description:`) captured the next frontmatter line. Bound it to horizontal whitespace. Co-Authored-By: Qwen-Coder <[email protected]> * feat(memory): add memory.enableTeamMemorySync setting Team-memory git sync was env-only (QWEN_CODE_MEMORY_TEAM_SYNC). Add a first-class `memory.enableTeamMemorySync` setting mirroring the enableTeamMemory plumbing (settingsSchema, cli/core config, acpAgent type/schema/default/keys/normalizer, desktop mirror), with the env var kept as a 0/1 override. Off by default. Docs updated. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): audit hardening — resilient scan, deterministic team index From a multi-round adversarial audit of the team-memory tier: - scan: a single unreadable memory file (EACCES, or a TOCTOU delete during `git pull`) no longer rejects the whole scan and wipe every memory from the index — per-file failures are caught and skipped (debug-logged). - indexer: order the team index by code unit, not `localeCompare()`, so the committed MEMORY.md is byte-identical across machines/locales and the ff-only sync can't wedge on index churn. - scan: escape the frontmatter key before building its RegExp (latent hardening). - config: surface a skipped/failed team-memory sync at debug level instead of silently swallowing it. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): deterministic team index cap + surface diverged sync Round-2 audit follow-ups: - scan: the >200-file cap selected its subset by mtime + locale basename, which differs per machine/locale — so a large team's committed index churned and wedged the ff-only sync. Team scans now cap by code-unit path (deterministic); private tiers keep mtime-recency (never shared, so it's fine). - sync: a diverged branch made `pull --ff-only` (and the push) fail with no `skippedReason` — a silent no-op for a user who opted into sync. Now reported as `pull-failed` / `push-failed` and logged. Docs note pull/push are whole-branch, not path-scoped. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): regenerate settings schema for enableTeamMemorySync Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): harden team-memory git sync (reconcile-first, scoped push, safe failure) The opt-in team-memory git sync could publish unrelated local commits, wedge a two-writer branch, leave team files staged on commit failure, and hang session start on a credential prompt. Harden all four maintainer-flagged criticals: - Reconcile first: run `git pull --ff-only` BEFORE committing so the local team commit lands on top of upstream instead of diverging; a diverged branch is skipped cleanly as `pull-failed` rather than committing then wedging `--ff-only`. - Scoped push: push only when THIS sync created the commit and the branch was not already ahead of `@{u}` (pre-sync), via an explicit single-branch refspec (`HEAD:<merge-ref>`) — never an unqualified `git push`. New `local-ahead` skip. - Safe failure: unstage the team path when the commit fails (hook/GPG/identity) so a user's next manual `git commit` cannot sweep team files in. - Non-interactive git: force `GIT_TERMINAL_PROMPT=0` + batch-mode SSH, SIGKILL on timeout, and a shorter timeout so sync cannot block session start on a prompt. config refresh: rebuild the team index BEFORE syncing (so a fresh MEMORY.md is committed/pushed, with a re-build after a pull that brought new files) and key the shareability latch by projectRoot so a newly entered repo is re-checked. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): harden team-memory index/scan/path security Address the security/correctness criticals from PR #5886 review. Team memory is committed to the repo and loaded into every collaborator's system prompt, so its write/scan/classification paths are trust boundaries. - indexer: pass noFollow on the team MEMORY.md write and reject a symlinked team root, so a committed `.qwen/team-memory` (or index) symlink can't redirect the generated index outside the repo. - indexer: sanitize attacker-controlled frontmatter title/description (strip control/zero-width/bidi chars, collapse newlines, defang code/link markdown, cap length) before embedding into MEMORY.md, to prevent prompt injection into the shared context. - indexer: skip the rewrite when the generated index is byte-identical, avoiding mtime churn and no-op commit cycles. - scan: normalize CRLF before parsing so a Windows checkout's team files don't silently drop out of the shared index. - paths: resolve a dangling final-component symlink via lstat/readlink in realpathNearestExisting, so a `decoy -> team-memory/leak.md` (missing target) is classified as team and the secret scanner can't be bypassed. - secret-scanner: bound both quantifiers in the openai-api-key T3BlbkFJ rule ({20,512}) to remove O(n^2) ReDoS backtracking; real keys still match. - team-memory-git-status: also probe a representative topic path so a repo that re-includes the index but ignores topic files is flagged as non-shareable. - notebook-edit: run the team-memory secret scan in validateToolParamValues too, for parity with edit/write-file. Adds load-bearing tests for each fix. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): sanitize team index paths and verify whole-path containment Refines the earlier team-memory security hardening with two fixes the first pass left open: - sanitizeIndexPath now defangs the attacker-controlled relativePath rendered into the committed MEMORY.md — at BOTH the `](path)` link target and the "(also: ...)" dedup suffix. A git filename may legally contain newlines and markdown delimiters, so a raw path could inject a second physical line (e.g. "- SYSTEM:") and reopen the prompt-injection structure the patch was closing for title/description. - rebuildTeamAutoMemoryIndex now realpath-resolves the whole team root and requires it to equal repoRoot/.qwen/team-memory. The prior lstat only inspected the leaf, so a symlinked PARENT component (e.g. `.qwen -> /tmp/out`) let scan/write escape the repo undetected while the guard believed it had rejected symlinks. Also adds a regression test pinning the security-load-bearing invariant that isAnyAutoMemPath excludes team paths (so team writes stay ask-gated). Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): keep team index link targets addressable, not just safe The earlier injection fix rewrote legal filename chars (`(` `)` `[` `]` and backticks) to `_` and used that destroyed value as the ACTUAL MEMORY.md link target. A real file `feedback/a(b).md` was emitted as `](feedback/a_b_.md)`, which does not exist (or, worse, collides with a different real file) — the link was injection-safe but no longer addressable. Replace `sanitizeIndexPath` with `encodeIndexPathTarget`, a reversible percent-encoder: every char outside an addressable allowlist (alphanumerics and `/ . - _ ~`) is percent-encoded, so breakout chars become inert ASCII (newline->%0A, `(`->%28, `)`->%29, space->%20, backtick->%60, ...). The target is a single line with no `](`/`)` breakout, yet `decodeURIComponent` recovers the exact original path so the link resolves to the real file. `/` is kept literal so the path still works as a relative reference. Applied at both index sites (the main `](path)` link and the `(also: ...)` suffix). The display label (title) stays non-reversibly sanitized as before — it is not addressable. Update the path tests to assert the emitted target percent-decodes back to the real relative path, and add a `feedback/a(b).md` case. The newline+`]`/`)` injection assertions remain (mutation-verified: they fail if the encoding is dropped). Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): gate team-memory sync on the index safety check rebuildTeamAutoMemoryIndex throws when the team root is a symlink that could redirect the committed index outside the repo, but the surrounding .catch swallowed the error and syncTeamMemory ran independently — so a refused symlink/escape still got git add/commit/push'd on the out-of-repo dir, defeating the indexer's refusal. Capture whether the rebuild succeeded and only sync when it did, so a failed safety check skips sync entirely. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): harden team-memory git sync and cover execute-time secret scan Several robustness fixes to the team-memory git sync, plus a missing test: - Respect a user's GIT_SSH_COMMAND: append our non-interactive batch guards (-oBatchMode=yes -oConnectTimeout=5) onto it instead of clobbering it, so a custom identity (-i), proxy jump (-J), or non-standard port is preserved. - Skip cleanly on a detached HEAD (new `detached-head` skippedReason) instead of creating an orphaned commit that can never be pushed. A branch with no upstream still commits locally as before. - Use SIGTERM (not SIGKILL) as the git kill signal so a timeout mid-commit/-pull lets git release index.lock and clean up; the non-interactive ssh guards already bound the network-hang case. - docs(memory): correct the sync description to match the implementation (rebuild index, ff-pull BEFORE commit, push only the scoped sync commit, skip on divergence/no-upstream). - test(notebook-edit): cover the execute-time team-memory secret scan (the full-notebook backstop), distinct from the existing validate-time test. Co-Authored-By: Qwen-Coder <[email protected]> * fix(memory): split security vs operational team-sync failures; per-op git kill signal Refine the recent team-memory sync changes per review: - indexer: throw a typed TeamMemoryRootSecurityError for the symlink/escape rejection so the sync gate can tell a SECURITY refusal apart from an OPERATIONAL IO failure. config now blocks sync ONLY on the security class; an operational rebuild failure (EACCES/ENOSPC/EPERM) is logged but self-corrects on the next rebuild and no longer permanently gates legitimate team sync. The security invariant (never add/commit/push an escaping root) is unchanged. - team-memory-sync: pass a per-op kill signal to tryGit — SIGKILL for the hang-prone network/ref ops (pull/push/rev-parse/symbolic-ref; no index.lock to corrupt), SIGTERM for mutating ops (add/commit) so git can release locks and clean up. Node's execFile timeout does not escalate to SIGKILL on its own, so a child trapping SIGTERM could hang past the timeout. - team-memory-sync: thread the resolved branch into resolvePushTarget so it no longer re-spawns `git symbolic-ref` after the detached-HEAD check. - docs: fix a broken sentence in the team-memory git-sync section. - tests: cover both rebuild failure classes (security -> sync skipped; operational -> sync still runs) and a positive gate (rebuild ok + sync enabled -> sync runs); assert the typed error on the indexer symlink tests. Co-Authored-By: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Shaojin Wen <[email protected]> |
||
|
|
94ea0884bb |
refactor(cli): split serve server assembly (#5937)
Co-authored-by: Qwen-Coder <[email protected]> |
||
|
|
f33dd61f8a |
fix(core): stop repeated truncated write_file/edit retries from looping (#5934)
* fix(core): stop repeated truncated edit retries * fix(core): default output tokens to the model limit instead of the 8K cap The 8K CAPPED_DEFAULT_MAX_TOKENS made normal large responses (esp. file writes) truncate, forcing a truncate->escalate round-trip and, worst case, a retry loop. Default to the model's declared output limit instead; the existing escalation + multi-turn recovery stay as the truncation backstop. The 8K cap was a slot-reservation optimization. Claude Code keeps the same cap but gates it behind a feature flag that defaults OFF for third-party providers; qwen-code's providers are all third-party / OpenAI-compatible / self-hosted, so matching that default-off behavior is the safe choice. The capacity tradeoff stays opt-in via QWEN_CODE_MAX_OUTPUT_TOKENS. Refs #5756 * fix(core): use a truncation-specific stop directive for repeated truncated writes * docs: update max tokens configuration wording |
||
|
|
a1f7846cd1 |
feat(web-shell): add manual toggle for enhanced markdown tables (#5917)
* feat(web-shell): add manual toggle for enhanced markdown tables - Add toggleable table component with SVG icon buttons - Default to plain table, click icon to switch to enhanced table - Show "Copied!" feedback with checkmark for 2s after copy operations - Add padding-right to last column to prevent icon overlap - Update tests to verify toggle and copy feedback behavior * fix(web-shell): update table toggle tests * fix(web-shell): address table toggle review * fix(web-shell): make table copy feedback strict-mode safe * fix(web-shell): ignore stale table copy feedback * test(web-shell): cover stale table selection copy --------- Co-authored-by: ytahdn <[email protected]> |
||
|
|
91ac3c200c |
feat(core): warn before foreground shell timeout (#5918)
* feat(core): warn before foreground shell timeout * fix(core): guard shell timeout promotion hint |