qwen-code:基于 Qwen3-Coder 的终端 AI 代理项目

An open-source AI agent that lives in your terminal.

分支1457Tags468
文件最后提交记录最后更新时间
ci: split Aliyun OSS sync into a separate post-release workflow (#4492) * ci: split Aliyun OSS sync into a separate post-release workflow The OSS upload and verification steps were adding significant time to the release workflow's critical path. Move them into a new `sync-release-to-oss.yml` workflow that triggers on `release: published`, running asynchronously after the release completes. Key changes: - Extract all OSS steps (ossutil install, credential config, asset upload, verification, hosted installation sync, latest VERSION pointer) into `sync-release-to-oss.yml` - Switch `gh release create` to use CI_BOT_PAT so the release event can trigger the new downstream workflow (GITHUB_TOKEN events don't trigger other workflows) - Add `workflow_dispatch` input for manual re-runs on failure - New workflow downloads release assets from GitHub Release instead of rebuilding them This decouples publishing from CDN distribution: the release finishes as soon as npm publish + GitHub Release are done, and China CDN sync happens in parallel without blocking. * fix(test): update install-script test to check sync-release-to-oss.yml The test asserts OSS sync steps exist in the workflow. Now that these steps live in sync-release-to-oss.yml instead of release.yml, update the test to read from the correct file and add assertions that release.yml no longer contains OSS logic. * fix(ci): address review feedback for OSS sync split - Add 'Verify Standalone Archives' step before gh release create in release.yml as a pre-publish safety gate (wenshao) - Add concurrency group to sync-release-to-oss.yml to prevent race conditions when multiple releases publish close together (wenshao) - Update test to assert verify step exists in release.yml * chore: add comment explaining CI_BOT_PAT requirement [skip ci]2 天前
Sync upstream Gemini-CLI v0.8.2 (#838) 6 个月前
docs(agents,pr-template): add Working Principles and restructure PR template (#4496) * docs(agents): add Working Principles and file/comment conventions Add a "Working Principles" section at the top of AGENTS.md, with Simplicity First (adapted from Andrej Karpathy's CLAUDE.md) as the lead principle. Extend Code Conventions with two new entries: - File naming: PascalCase for React components, kebab-case preferred for new non-component files, existing camelCase stays as-is. - Comments: default to none; explain why, not what. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(agents): link Karpathy's CLAUDE.md in attribution Per review feedback, make the source attribution clickable so reviewers can reach the original document in one hop. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(agents): align comments guidance — "default to none" Raise the bar for code comments from "add sparingly" to "default to none" in the runtime prompt, matching the AGENTS.md convention. Add a preservation clause to AGENTS.md so agents do not strip existing high-value comments during cleanup passes. Update snapshots. Co-Authored-By: Qwen Code <noreply@alibaba-inc.com> * docs(pr-template): restructure for reviewer test plan clarity - Reorganize PR template around a Reviewer Test Plan section with How to verify, Before/After, and Tested on - Add collapsible Chinese description section for bilingual PRs - Simplify create-pr command guidance to match the new template - Tighten AGENTS.md file naming and comments conventions; align PR submission guide with the new template This makes PRs easier to review by focusing contributors on the evidence reviewers need most. * docs(pr-template): merge Before/After into Evidence and require full Chinese translation - Consolidate Before and After sections into a single Evidence (Before & After) section - Update Chinese summary comment to require full paragraph-by-paragraph translation instead of abbreviated bullets This reduces template redundancy for non-UI changes and ensures the Chinese block is a proper translation, not a summary. --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Qwen Code <noreply@alibaba-inc.com>2 天前
Merge branch 'main' into feat/sandbox-config-improvements 2 个月前
feat: update docs 5 个月前
feat(cli): default auto-dream/auto-skill to on and add /memory toggle (#4547) * feat(cli): default auto-dream/auto-skill to on and add /memory toggle Bring the managed memory pipeline closer to its intended out-of-the-box experience: auto-dream and auto-skill now default to enabled (matching the existing auto-memory default), so users get summarized memories and reusable project skills without having to opt in. The /memory dialog previously only exposed Auto-memory and Auto-dream toggles. With auto-skill now on by default, users need an equally discoverable way to opt out, so this adds an Auto-skill row alongside the existing two with the same focus/Enter toggle semantics and workspace-scoped persistence (memory.enableAutoSkill). Default-value updates are kept consistent across all three sources of truth (settings schema, CLI loader, core Config), and the generated vscode settings.schema.json is regenerated to match. * test(cli): add getAutoSkillEnabled to MemoryDialog test mock The new Auto-skill toggle row reads config.getAutoSkillEnabled() at render time; without it on the mocked config the component throws and the existing list-navigation tests assert against an empty frame. * fix(cli): guard managed auto-dream in bare mode, sync tests and docs - enableManagedAutoDream in loadCliConfig was missing the bareMode guard that its two siblings already had; once the default flipped to true, this caused a raw-field inconsistency in bare-mode sessions (the getter still returned false via its own !getBareMode() guard, but the Config.enableManagedAutoDream field itself was now true). - docs/users/configuration/settings.md still listed enableManagedAutoDream's default as false, and was missing the new enableAutoSkill row entirely. Both fixed. - MemoryDialog.test.tsx now covers the autoSkill row render, the new focus chain (list ↑ autoSkill ↑ autoDream and back down), and the Enter-toggle path that writes memory.enableAutoSkill to workspace settings. - config.test.ts gains a non-bare default test asserting all three getManaged*Enabled() / getAutoSkillEnabled() return true, and the bare-mode test now asserts auto-dream/auto-skill also resolve to false in bare mode.8 小时前
pre-release commit 9 个月前
feat(core): add NotebookEdit tool for Jupyter notebooks Adds NotebookEdit as the structured write counterpart to existing notebook read support. Summary: - Add `notebook_edit` for safe cell-level `.ipynb` replace/insert/delete operations. - Integrate notebook editing with tool registration, permissions, Claude conversion, prior-read enforcement, IDE/inline modify flow, commit attribution, docs, and SDK permission docs. - Harden notebook read/edit behavior for truncated notebook renders, ambiguous fallback cell IDs, internal modify metadata, compact JSON, UTF-8 BOM notebooks, and cache behavior after structural edits. - Add unit and integration coverage for notebook read/edit behavior. Follow-up work remains for tab-indented notebook formatting preservation, a few low-risk unit-test additions, and non-blocking hardening suggestions from review.7 天前
fix(telemetry): improve LogToSpan bridge error info and TUI handling (#4482) * fix(telemetry): improve LogToSpan bridge error info and TUI handling The OTel `LogToSpanProcessor` bridge (used when traces+metrics are over OTLP but logs aren't, e.g. Alibaba Cloud ARMS) had two diagnostic issues: 1. Empty error messages. When the OTLP HTTP exporter callback returned `{ code: FAILED, error }`, `error.message` is the HTTP reason-phrase — always empty on HTTP/2. The bridge printed literally `[LogToSpan] export failed: code=1 error=` with zero actionable info. Now we surface `name`, `httpCode` (only when numeric), and a 200-byte `data` snippet from the underlying OTLPExporterError, with JSON-escape on user content so embedded newlines can't tear the log line. 2. TUI pollution. The processor wrote diagnostics to `process.stderr` directly. Ink only manages stdout, so those writes punched through into the rendered terminal area. The processor now accepts an injectable `diagnosticsSink`; in interactive mode `sdk.ts` injects a sink that routes through `debugLogger.warn` (file-backed). Non- interactive runs (CI/scripts) keep the default stderr sink so export failures remain visible on the canonical batch-diagnostic channel. Backward compatibility is preserved: the legacy numeric-arg constructor keeps stderr behavior; the options-object overload gains the new field. Other raw `process.stderr.write` sites in the CLI (errors.ts, startupProfiler.ts, useGeminiStream.ts, etc.) have the same TUI-leak pattern but are intentionally left out of this PR. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): address PR #4482 review comments - Fix TS2353 compile error in keeps-processing-after-sink-throw test: the mock callback type was narrowed to `{ code: number }` and rejected the `error?: Error` field that `ExportResult` actually carries. Widen the type. (wenshao Critical — this was the root cause of CI lint/test failures across all 3 OS.) - JSON.stringify the payload of the `export threw` diagnostic so a synchronously-thrown error with embedded newlines stays on one line, same single-line invariant enforced by `formatExportError`. Add coverage for both the newline case and the non-Error throw branch. (wenshao Suggestion) - Remove the dead `makeFailingProcessor(err)` call in the JSON-escape test that was immediately overwritten — the orphaned processor retained a live `setInterval` timer with no cleanup. (wenshao Suggestion) - Rename the "200 bytes" test name and comment to "200 characters" to match the actual `string.slice(0, 200)` (UTF-16 code units) behavior; add a note on the cap being a leak/noise budget, not a hard byte limit. (Copilot 2x) - Strengthen the non-interactive test to actually trigger a failed export against the real `LogToSpanProcessor` and assert the default sink writes to stderr, not just that `diagnosticsSink === undefined`. (github-actions High #2) - Reword the "shell-active bytes" comment to "characters that would break log parsing" — the actual concern is log-line tearing, not shell semantics. (github-actions Medium) - Update class JSDoc to mention the diagnostics-sink responsibility alongside the bridge purpose. (github-actions Low) - Minor JSDoc wording fix on `LogToSpanDiagnosticsSink` type for clarity around the no-trailing-newline contract. (github-actions Low) 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * test(telemetry): cover two unreachable formatExportError branches Add coverage for two paths flagged by the DEEP-tier review on #4482: - `err.message || err.name || 'unknown'` chain: the third branch (both message and name empty) was never exercised. Scenario: minified environments that strip `Error.name`. Test constructs `Object.assign(new Error(''), { name: '' })` and asserts the output contains `error="unknown"`. - `typeof extra.data === 'string' && extra.data.length > 0` guard: the empty-string case (HTTP response with empty body) was never tested, so a future loosening to `!== undefined` would silently start emitting `data=""`. Test asserts `data=` is absent. Both branches are real and reachable in production failure modes; the tests are guards for the documented intent. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): tighten LogToSpan diagnostics per wenshao review - Quote `error="unknown"` in the err-missing early return so it matches the JSON.stringify output produced when message+name fall back to 'unknown'. Two paths now emit identical greppable output for semantically identical "unknown error" states. - Widen the duck-typed cast to `code?: number | string` and add a load-bearing comment on the `typeof === 'number'` guard. The type now matches reality (Node networking errors surface string codes like ECONNREFUSED), preventing a future simplification to `if (extra.code)` that would mislabel networking errors as HTTP statuses. - Reuse `formatExportError` in the sync-throw path so a synchronously- thrown OTLPExporterError surfaces its httpCode and data, matching the callback-failure path. Non-Error throws still fall back to JSON.stringify on String(err) to preserve the single-line invariant. - Include batch span count in the timeout diagnostic ("(N span(s))") — lets an operator distinguish slow network from oversized batch when troubleshooting timeouts. - Add a test for non-string truthy err.data (Buffer) — the `typeof === 'string'` guard's false branch was only covered for undefined and empty string, so a future refactor relaxing the guard would silently start emitting binary garbage with no test to catch it. - Document the QWEN_DEBUG_LOG_FILE=0 trade-off at the sink wiring site: interactive mode plus disabled debug log = full diagnostic silence. This is an accepted user opt-in trade-off; falling back to stderr would re-introduce the TUI pollution this injection prevents. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)5 小时前
ci: split Aliyun OSS sync into a separate post-release workflow (#4492) * ci: split Aliyun OSS sync into a separate post-release workflow The OSS upload and verification steps were adding significant time to the release workflow's critical path. Move them into a new `sync-release-to-oss.yml` workflow that triggers on `release: published`, running asynchronously after the release completes. Key changes: - Extract all OSS steps (ossutil install, credential config, asset upload, verification, hosted installation sync, latest VERSION pointer) into `sync-release-to-oss.yml` - Switch `gh release create` to use CI_BOT_PAT so the release event can trigger the new downstream workflow (GITHUB_TOKEN events don't trigger other workflows) - Add `workflow_dispatch` input for manual re-runs on failure - New workflow downloads release assets from GitHub Release instead of rebuilding them This decouples publishing from CDN distribution: the release finishes as soon as npm publish + GitHub Release are done, and China CDN sync happens in parallel without blocking. * fix(test): update install-script test to check sync-release-to-oss.yml The test asserts OSS sync steps exist in the workflow. Now that these steps live in sync-release-to-oss.yml instead of release.yml, update the test to read from the correct file and add assertions that release.yml no longer contains OSS logic. * fix(ci): address review feedback for OSS sync split - Add 'Verify Standalone Archives' step before gh release create in release.yml as a pre-publish safety gate (wenshao) - Add concurrency group to sync-release-to-oss.yml to prevent race conditions when multiple releases publish close together (wenshao) - Update test to assert verify step exists in release.yml * chore: add comment explaining CI_BOT_PAT requirement [skip ci]2 天前
fix(cli): skip stdin read for ACP mode - Extend stdin handling to skip reading for ACP mode, similar to stream-json - Remove duplicate line in .dockerignore ACP mode passes protocol data via stdin that should be forwarded to the sandbox intact, not consumed as a user prompt. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> 1 个月前
pre-release commit 9 个月前
feat(installer): add standalone hosted install and uninstall flow (#3828) * feat(installer): add standalone archive installation * fix(installer): harden standalone archive installs * fix(installer): address standalone review findings * chore(installer): clarify review followups * fix(installer): stabilize standalone script checks * chore(installer): remove internal planning docs * chore(installer): simplify standalone release review fixes * test(installer): add Windows batch install smoke * test(installer): fix Windows batch smoke quoting * test(installer): preserve Windows cmd quotes * fix(installer): use robust Windows checksum hashing * ci: narrow installer debug matrix * fix(installer): address standalone review hardening * fix(installer): avoid Windows validation parse errors * fix(installer): simplify Windows option validation * fix(installer): harden standalone review fixes * feat(installer): publish release installer assets * fix(installer): address release asset review feedback * fix(installer): avoid prerelease installer asset links * test(installer): isolate standalone dist fixture * feat(installer): add hosted install release alias * chore: no changes - code review requested Agent-Logs-Url: https://github.com/QwenLM/qwen-code/sessions/38467aec-15b9-4b76-9139-0b2cfe40477a * fix(installer): pin versioned installer assets * fix: parallelize Node.js binary downloads in standalone release build Use Promise.all instead of sequential for...of+await for the 5 independent Node.js runtime downloads, reducing CI release build time by ~4-5x. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(installer): address release asset review followups * refactor(installer): share release CLI parsing * fix(installer): address release asset review followups - sh: reject CR/LF in archive entry names before the literal `..` glob so a `..\r` entry cannot bypass path validation. - bat: prefer Tls12+Tls13 in PowerShell helpers, fall back to Tls12 alone on older .NET Framework where the Tls13 enum is missing. - bat: document the implicit `:ValidateOptions` dependency next to the qwen.cmd wrapper writer so loosening the validator stays a conscious choice. - build-standalone-release: surface the `xz-utils` host requirement for Linux Node downloads in `--help`. - release-script-utils: support `--key=value` form in `parseCliArgs`. - tests: cover the new CRLF message, TLS string, and `--key=value` parsing; register process-level signal/exit handlers in `ensureMinimalDist` so a crashed test still restores `dist/`. * fix(installer): unblock Windows CI for standalone install path Three CI failures and a few review followups in one pass. - ensureMinimalDist places its dist/ backup beside dist/ instead of under os.tmpdir(). On Windows GitHub runners the workspace lives on D: while os.tmpdir() is on C:, so renameSync raised EXDEV for every test that needed to swap dist/ in. - create-standalone-package.js and the matching test fixture build win-x64 zips with [IO.Compression.ZipFile]::CreateFromDirectory. Compress-Archive emits backslash entry names that the .bat installer's path-traversal guard then rejected, so every freshly built archive failed the standalone install path on Windows. - :ValidateArchiveContents normalizes entry separators to '/' before checking for '..', absolute paths, and drive prefixes - archives from any Windows zip tool still install while real traversal entries remain rejected. - createWindowsTraversalStandaloneArchive runs PowerShell via -File instead of a single -Command line; the joined-with-'; ' form had a function definition the runner's PowerShell refused to parse. Drive-by review followups: - replaceRequired uses replaceAll so a future duplicate placeholder cannot silently keep the trailing copy as 'latest'. - :ValidateOptions runs the unsafe-character check on SOURCE alongside the other variables. - build-installation-assets.js drops a dead INSTALLATION_ASSETS re-export; consumers already import from release-asset-config.js. - .gitignore covers the new sibling .qwen-dist-backup-* directory. * fix(installer): address release asset review findings * fix(installer): keep installer entrypoint hosted * fix(installer): reject stale hosted assets * fix(installer): refine hosted asset staging * fix(installer): tighten hosted default-version check, flag legacy URL - Replace the loose `latest` fragment check with per-format regex patterns in HOSTED_INSTALLER_DEFAULT_VERSION_PATTERNS so an unrelated occurrence of `latest` (comment, help text) cannot satisfy the staging guard. The patterns still tolerate whitespace variation, only the default-version assignment itself must be intact. - Add a "Hosted endpoint status" callout in INSTALLATION_GUIDE.md before the curl examples. The documented `--version` flow does not work against the OSS URL today because it currently serves the legacy NVM-based installer; the callout points users at a local checkout until the next release sync. - Tests: drop `latest` from the fragments equality assertion, add positive and negative regex coverage, add a failure-path case for sources whose default version is not `latest`, and pin the new guide markers so the callout cannot silently disappear. * feat(installer): verify installation release assets Adds `npm run verify:installation-release` and wires it into the release workflow after `Build Standalone Archives`, so a broken release directory fails CI before publishing. Local mode (`--dir PATH`) checks: - All five `qwen-code-{platform}.{ext}` standalone archives exist. - `SHA256SUMS` covers exactly those five — missing or unexpected entries fail. - Each archive's actual SHA256 matches its `SHA256SUMS` entry. Remote mode (`--base-url URL`) checks: - `SHA256SUMS` is downloadable, parseable, and contains exactly the expected archive entries. - Each archive URL is reachable via HEAD, with a 1-byte ranged GET fallback for hosts that disable HEAD. Hosted installer scripts (`install-qwen.sh` / `install-qwen.bat`) are intentionally out of scope here — they are served from the hosted endpoint prepared by `package:hosted-installation` (PR #3853), not from the GitHub Release surface this verifier targets. * fix(installer): tighten verifier base-url + clarify test helper Three small refinements from the second review pass: - normalizeHttpsBaseUrl rejects everything except https, since real release URLs are always HTTPS. Accepting http previously would let an operator silently target a stale or attacker-controlled mirror. - Drop EXPECTED_RELEASE_ASSET_NAMES from the public exports; it was only used internally for the verification log line. - Rename the test helper standaloneChecksumContent to placeholderChecksumContent and document that the hashes in its output are placeholders — the remote verifier does not download archives or compare hashes, it only validates that SHA256SUMS lists the expected names and that each archive URL is reachable. The non-https rejection test now also covers `http://` in addition to the existing `file://` case. * fix(installer): address standalone review follow-ups * fix(installer): repair Windows installer tests * fix(release): tighten standalone asset checks * fix(installer): stabilize Windows managed install checks * test(installer): relax Windows installer timeout * fix(test): escape release asset regex * test(cli): avoid POSIX node path in relaunch test * fix(installer): align npm fallback node gate with engines * test(installer): allow Windows archive validation more time * fix(installer): remove stale node 20 installer references * docs(installer): clarify hosted endpoint sync requirement * refactor(installer): reuse standaloneArchiveName in release verifier The verify-installation-release script was duplicating the archive name derivation logic with a hardcoded ternary instead of reusing the standaloneArchiveName helper from build-standalone-release. Export the helper and import it so the extension mapping lives in one place. * fix(scripts): address release verifier review feedback * feat(installer): add standalone archive installer with multi-platform release workflow - Add standalone archive installer (bat/sh) that downloads platform binaries from GitHub/Aliyun without requiring Node.js or npm on the target machine - Add fork-friendly release-test workflow for manual GitHub Release creation covering all 5 platforms (darwin-arm64/x64, linux-arm64/x64, win-x64) - Add OSS upload/mirror tools for staging and release distribution - Update .gitignore to exclude generated build artifacts (release-staging/, hosted-staging/) - Fix Windows PowerShell test command in copy-release-to-latest tool * feat(installer): support QWEN_INSTALL_GITHUB_REPO env var for custom repo * chore(installer): exclude local-only staging tools from PR The tools/ directory contained personal staging-OSS upload helpers (upload-staging, upload-release-mirror, copy-release-to-latest, test-upload-one) that should not ship in the public PR. They reference a personal staging bucket and only exist to validate the installer end-to-end before production release. Removes them from git tracking via `git rm --cached` (files stay on disk for the author's local use) and adds /tools/ to root .gitignore so they cannot be re-added accidentally. No runtime / installer code change. Production CI on ubuntu-latest is unaffected. * fix(installer): enforce CRLF line endings for .bat files via gitattributes cmd.exe requires CRLF in batch scripts; the global eol=lf was causing every line to be misparsed on Windows, producing errors like 'QWEN_VALIDATE_METHOD=detect is not recognized as a command'. * fix(installer): store .bat files with CRLF in git blob for raw GitHub downloads GitHub raw file serving bypasses gitattributes eol conversion and serves blob bytes directly, so eol=crlf alone was not enough. Use -text to disable normalization and commit with actual CRLF so raw downloads work on Windows. * fix(installer): follow HTTP redirects in UrlExists and RaceMirrorHead probes GitHub release asset URLs return HTTP 302 to objects.githubusercontent.com. [Net.WebRequest] with HEAD does not auto-redirect by default, so the existence check and mirror-race probe both incorrectly reported the file as missing. Set AllowAutoRedirect=true on HttpWebRequest instances. * fix(installer): surface download errors and add MaximumRedirection 10 * feat(installer): add hosted install-qwen.ps1 shim for irm|iex one-liner The previous Windows quick-install one-liner used `Invoke-WebRequest -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path …)`. When pasted into a narrow terminal, line wrap could land on `-OutFile`, orphaning the parameter from its value and producing the "missing argument for OutFile" failure followed by a "file not found" when the second `&` ran. PowerShell's line continuation rules cannot resolve this for parameter-name-at-EOL. Add `install-qwen.ps1` as a thin hosted entrypoint that downloads `install-qwen.bat` into TEMP, runs it, and cleans up. Documented one-liner becomes the standard pattern used by bun, uv, scoop, deno, pnpm: powershell -ExecutionPolicy Bypass -c "irm <url>/install-qwen.ps1 | iex" The `.bat` remains the source of truth for installer behavior; `.ps1` is just the modern hosted entrypoint. Version pinning via `$env:QWEN_INSTALL_VERSION` flows through unchanged. Stored with `*.ps1 -text` so CRLF survives both GitHub raw and OSS uploads, matching the existing `.bat` handling. * fix(installer): stage direct hosted install scripts * chore(installer): trim hosted release diff scope * chore(installer): narrow hosted release diff * feat(installer): restore hosted PowerShell entrypoint * chore(installer): stage standalone hosted entrypoints * fix(installer): address hosted installer review followups * fix(installer): stabilize Windows installer tests * fix(installer): make Windows option validation readable * feat(installer): wire Aliyun OSS sync, address review followups - Add Aliyun OSS sync steps to release workflow: package hosted assets, install pinned ossutil, configure credentials, upload versioned and latest paths, and verify upload via verify:installation-release plus curl probes against the hosted installer endpoint. - Document required production-release environment secrets and bucket variables in INSTALLATION_GUIDE.md. - Restructure hosted endpoint guidance to lead with the pre-sync warning, splitting "Run today" (local checkout) from "After the OSS sync" (hosted one-liners) so users no longer copy a one-liner that silently installs latest. - Distinguish mirror auto-selection timeout from successful selection in install-qwen-standalone.sh and install-qwen-standalone.bat: emit a "timed out; defaulting to github" log instead of pretending the HEAD probe picked github. - Support QWEN_INSTALLER_BAT_URL override (https only) in the PowerShell shim so staging mirrors can be exercised without forking the file. - Strip a leading UTF-8 BOM in verify-installation-release.js parseSha256Sums so BOM-prefixed SHA256SUMS reports a useful "Missing checksum entry" error instead of "Malformed SHA256SUMS line 1". - Add tests for verifier HEAD→Range fallback, partial-failure formatting, all-failure wording, and BOM tolerance. * ci(installer): add temporary OSS smoke test * fix(installer): make OSS release assets public-readable * chore(installer): remove temporary OSS smoke workflow * fix(installer): address hosted installer review gaps * feat(installer): refactor argument parsing and utility functions for release scripts * fix(installer): harden hosted release script checks * fix(installer): suppress PowerShell progress bar in hosted entrypoint shim Add $ProgressPreference = 'SilentlyContinue' to the .ps1 wrapper so Invoke-WebRequest downloads don't render a progress bar when invoked via the irm | iex one-liner. * fix(installer): suppress PowerShell progress bar in bat installer downloads Add $ProgressPreference = 'SilentlyContinue' to DownloadFile so the full-screen progress UI does not appear during archive downloads in interactive PowerShell sessions, consistent with the .ps1 shim. * fix(installer): use curl.exe -# progress bar in Windows downloads Prefer curl.exe with -# (hash-mark progress bar) for archive and installer downloads on Windows 10+. Falls back to Invoke-WebRequest (which shows its own progress bar) when curl.exe is unavailable. Matches the approach used by code-server (curl -#fL) and bun.sh (curl.exe -#SfLo). * fix(installer): suppress progress bars for small downloads and Expand-Archive - .ps1: replace curl.exe -# with silent mode, suppress Invoke-WebRequest progress bar; save/restore $global:ProgressPreference - .bat: add $ProgressPreference = 'SilentlyContinue' before Expand-Archive to prevent full-screen extraction progress UI - .sh: remove --progress-bar / --show-progress from download_file, always use silent curl/wget * fix(installer): auto-backup non-qwen directories and simplify output - ensure_managed_install_dir / :EnsureManagedInstallDir now back up non-qwen directories instead of refusing to install, so users upgrading from npm or old installers don't hit a hard error - Simplify header/footer output: remove banner bars, verbose INFO lines, and redundant "Installation completed!" message - Match bun.sh / code-server style: minimal, to the point * fix(installer): revert Expand-Archive progress suppression in bat The inline $ProgressPreference = 'SilentlyContinue' caused a cmd.exe parsing error ("此时不应有 >") on Chinese Windows. Revert to the original Expand-Archive invocation. * fix(installer): fix cmd.exe parsing error in backup fallback code The %s in the for /f fallback command string was interpreted as a variable reference by cmd.exe, causing "此时不应有 >" on Chinese Windows. Replace with a safe fallback and re-enable Expand-Archive progress suppression. * fix(installer): always persist install bin to user PATH Previously MaybeUpdateUserPath was only called when shadow qwen executables were detected. When no shadow was found, the PATH update was skipped entirely, leaving the user without qwen on PATH after restarting their terminal. Now always persist the bin directory to PATH (unless --no-modify-path is set), regardless of whether other qwen installations exist. * fix(installer): persist PATH to current terminal session on Windows Use the `endlocal & set` trick (same as bun/Rust installers) to export the install bin directory from the setlocal scope to the current cmd session. qwen is now usable immediately without restarting the terminal. * docs(installer): document cmd.exe one-liner for immediate PATH availability Add curl-based one-liner for cmd.exe users. Running the .bat directly in the current cmd session makes `qwen` available immediately via the `endlocal & set` trick. The `powershell -c "irm | iex"` path creates a child process so PATH changes cannot propagate to the parent. * feat(installer): make qwen usable immediately from PowerShell after install - .ps1: detect parent process, update current session PATH, and for cmd.exe parents emit a `set PATH=...` command - .bat: skip final instructions when called from PowerShell to avoid duplicate "Run: qwen" output * fix(installer): remove non-functional doskey approach for cmd parent doskey /exename from a child PowerShell process cannot modify the parent cmd.exe session. Replace with a simple set PATH=... command that the user can copy-paste. * fix(installer): make Windows standalone shim available in cmd * feat(installer): add standalone uninstall scripts * fix(uninstall): match shell-quoted paths when removing the wrapper The installer's write_unix_wrapper shell-quotes the binary path, so paths containing single quotes (or other shell metacharacters) appear as shell-quoted strings in the generated wrapper file. The uninstall script's literal grep -qF missed these, leaving the wrapper orphaned. Add shell_quote to the uninstall script and match against both the raw and shell-quoted forms before removing the wrapper. * fix(installer): update download commands to use progress indicators for curl and wget * fix(installer): resolve Aliyun latest via version pointer * fix(installer): cleanup mirror probe temp dirs * fix(installer): harden standalone release fallback * fix(installer): address standalone review feedback * style(installer): align standalone install output * fix(installer): print standalone uninstall commands * fix(installer): address release review follow-ups * fix(installer): harden Windows target detection * test(installer): stabilize Windows fake tool path * fix(installer): allow explicit Windows curl path * test(installer): use cmd fake curl on Windows * test(installer): cover Windows fake curl helper * test(installer): inject Windows arch overrides in cmd * test(cli): wait for prompt suggestion render * test(cli): revert prompt suggestion wait tweak * fix(installer): harden hosted release publishing * fix(installer): harden Windows latest pointer parsing * fix(installer): bound Windows download timeouts * fix(installer): bound hosted installer probes * fix(release): make ossutil download configurable * fix(installer): address hosted release review feedback * test(installer): keep dist backup on same filesystem * fix(installer): address remaining review feedback on PR #3828 - Remove REQUIRE_CHECKSUM dead code, always hard-fail on checksum issues - Add JSDoc to HOSTED_INSTALLER_BEHAVIOR_PATTERNS explaining its purpose - Add credential cleanup trap for ossutilconfig in release workflow - Add 3-attempt retry with exponential backoff for OSS uploads - Tighten findstr SOURCE regex to require leading letter * fix(release): correct OSS credentials lifetime and mirror probe fallback - release.yml: remove `trap EXIT` inside the Configure step; it deleted ${RUNNER_TEMP}/.ossutilconfig as soon as the configure shell exited, so every subsequent step (publish/sync/verify) lost the credentials. Move credential cleanup to a final `if: always()` step at the job tail. - install-qwen-standalone.sh: drop the predictable PID-based mktemp -d fallback in race_mirror_head; if mktemp fails, return "github" instead of using /tmp/qwen-mirror.$$ which a local attacker could pre-create to bias mirror selection. * fix(installer): address review feedback round 2 Workflow: - Move 'Publish Aliyun OSS Latest VERSION' to run after the hosted installer assets are uploaded and verified, so the latest/VERSION pointer only flips once every release artifact is in place. Previously a hosted-sync failure could leave the pointer ahead of the actual installer scripts. upload-aliyun-oss-assets.js: - Replace `spawnSync('sleep', ...)` retry backoff with an Atomics.wait-based cross-platform sleep so retries also work on Windows runners. install-qwen-standalone.bat: - :DetectTarget no longer emits TARGET=win-arm64 because RELEASE_TARGETS has no win-arm64 archive; ARM64 hosts now fall through to the unsupported-arch branch and (in detect mode) get the npm fallback instead of a 404. - Add QWEN_INSTALL_CURL_EXE to :ValidateRawEnvironmentOptions so this curl override is checked for shell metacharacters like every other knob. - Replace `call echo %%i>>...` with plain `echo %%i>>...` when capturing pre-install qwen.cmd paths; `call` triggered an extra parse pass that could interpret &/|/<,>/etc. inside a directory name as command separators. - Add `--retry 2` to curl.exe downloads (`:DownloadFile` / `:DownloadFileQuiet`) to match the shell installer. - Include expected vs actual hash in the checksum-mismatch error message. install-qwen-standalone.ps1: - Stage the downloaded installer at a cryptographically random temp path (`qwen-installer-<random>.bat`) so a same-user attacker cannot pre-stage a malicious .bat at a predictable path and race the verify/execute window. - Atomically install the current-session cmd shim by writing to a sibling `.new` temp file then renaming, so a partial write cannot leave a half-written shim on PATH. - Add `--retry 2` to the curl.exe download path. - Include expected vs actual hash in the checksum-mismatch error message. install-qwen-standalone.sh: - Include expected vs actual hash in the checksum-mismatch error message. uninstall-qwen-standalone.ps1: - Accept `-Purge` and `-Help` parameters; previously every CLI flag was silently dropped, so users running with `-Purge` got no purge and no error. `-Purge` maps to `QWEN_UNINSTALL_PURGE=1`. uninstall-qwen-standalone.sh: - `remove_install_wrapper` additionally requires the wrapper file to start with a `#!` shebang before it deletes it; a user-authored script that just happens to mention the install path now stays untouched. verify-installation-release.js, build-hosted-installation-assets.js: - Include expected vs actual hash in the checksum-mismatch error messages. scripts/tests/install-script.test.js: - Update assertions for the new error wording, the curl `--retry 2` flag, the dropped ARM64 detection, and the new release-step ordering. * fix(installer): address review feedback round 3 Workflow: - Configure Aliyun OSS Credentials: write the ossutil config file directly with restricted umask instead of invoking `ossutil config -k <secret>`. Passing the access-key secret via argv made it visible in /proc/<pid>/cmdline for the lifetime of that step; writing the INI file in-process keeps the secret out of the process table. upload-aliyun-oss-assets.js: - Upload assets in parallel with `Promise.all` + async `spawn` instead of a sequential `spawnSync` loop. Each asset keeps its own retry budget; failures are aggregated so one flaky upload does not mask a separate failure. - Replace the bespoke `Atomics.wait` retry sleep with `timers/promises#setTimeout` now that the loop is async. INSTALLATION_GUIDE.md: - Drop the misleading "instead of overwriting the global installation/ entrypoint objects" sentence; the workflow has always also refreshed the global versionless objects so curl|bash links keep resolving without a version segment. Document the rollback story instead. * test(installer): add parseUploadArgs unit tests and align verify derivation - scripts/tests/upload-aliyun-oss-assets.test.js: cover --help short-circuit, required-option validation (--bucket/--config/--prefix/empty assets), unknown options, missing option values, and trailing-slash prefix normalization. - scripts/verify-installation-release.js: switch the win-only zip branch from `startsWith('win-')` to the strict `=== 'win-x64'` check used by build-standalone-release.js, and add a comment recording that the two derivations must stay aligned. Without this the helpers would diverge the moment a non-x64 win target gets added. * test(installer): add uploadAssets integration tests with fake ossutil Add two integration tests that route a temp-directory ossutil shim onto PATH so uploadAssets actually spawns the real binary with the real cp argv: - happy-path test asserts the destination URI, `-c <config>`, `--acl public-read`, and per-asset cp invocations land for both inputs. - failure-path test asserts non-zero ossutil exits surface as an aggregate `asset uploads failed` error after the retry budget runs out. * revert(installer): drop over-engineered ossutil/upload changes Roll back two changes from a1ef8697b/0a5d308c9 that were not justified by the actual threat model or release-pipeline needs: - .github/workflows/release.yml: restore the supported `ossutil config -k` invocation. The earlier switch to writing the .ossutilconfig INI file in-process was meant to keep the access-key out of /proc/<pid>/cmdline, but GitHub-hosted runners are single-tenant ephemeral VMs where no other user can read that namespace. The benefit was theoretical; the cost was taking on a brittle dependency on ossutil's undocumented config format. - scripts/upload-aliyun-oss-assets.js: revert the uploadAssets parallel rewrite (Promise.all + spawn + setTimeout) back to the original sync spawnSync loop with retry. Release-time uploads of ~6 small files do not need parallelism, and the async refactor changed the public contract (sync→async) for no real wall-clock win. Kept from those commits: - The cleanup `if: always()` step that removes RUNNER_TEMP/.ossutilconfig at the end of the publish job. - The cross-platform sleepSync(ms) helper, since `spawnSync('sleep', ...)` still does not work on Windows runners. - The INSTALLATION_GUIDE.md doc fix. - All other round-2 fixes. Test assertions updated for the restored sync uploadAssets contract. * test(installer): cover Windows release script regressions * test(release): avoid Windows shim lookup in oss upload tests * test(installer): use stable fake Aliyun version on Windows * fix(installer): parse Aliyun latest version in batch * fix(installer): validate Aliyun latest version without findstr * fix(installer): normalize Aliyun latest version via PowerShell * fix(installer): avoid captured PowerShell output in batch latest parsing * fix(installer): normalize Aliyun latest pointer from file * test(installer): fix fake Windows curl output parsing * fix(installer): print checksum path on miss, gate hardcoded version pin in ps1 [skip ci] Address two narrow follow-ups from PR #3828 review: - build-hosted-installation-assets.js: add a HOSTED_INSTALLER_FORBIDDEN_PATTERNS guard for install-qwen-standalone.ps1. The ps1 shim has no VERSION variable of its own (it forwards @args to the .bat), so the existing default-version positive-match patterns don't apply. The new guard fails the build if a $env:QWEN_INSTALL_VERSION assignment or a --version flag prepended to the forwarded argument list ever lands in the shim. Patterns are line-anchored with /m so the documented usage examples in the header docstring stay valid. Two vitest cases cover the reject and allow paths. - install-qwen-standalone.sh / .bat: include the searched checksum-file path in the "SHA256SUMS not found" error. Operators triaging --archive failures could not tell from the prior message whether the fallback path (next to the archive) or the remote URL was being looked up. Existing test assertions updated to match the new wording. Local validation: npm run test:scripts -> 160 passed | 9 skipped (was 158 | 9). * fix: stamp release version in hosted installers and add Zip Slip protection [skip ci] 1. The hosted installation asset build now accepts --version and stamps it into the copied .sh/.bat installers so they default to the tagged release version instead of 'latest'. The release workflow passes the version. 2. install-qwen-with-source.bat now validates archive entries before calling Expand-Archive, rejecting paths with '..', leading '/', drive-rooted paths, empty names, or control characters — matching the protection already present in install-qwen-standalone.bat and the .sh installer. * fix(installer): add SOURCE to PowerShell unsafe-character validation [skip ci] The SOURCE variable is user-provided and used in path operations but was not included in the :ValidateOptions unsafe-character check. Add it alongside the other validated variables. * fix: correct copyright year 2025 -> 2026 in new files [skip ci] --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: yiliang114 <effortyiliang@gmail.com>6 天前
feat(cli): do not append trailing space for directory completions (#4092) (#4288) * feat(cli): do not append trailing space for directory completions (#4092) ## What 在 @路径补全和 /dir add 命令的目录补全中不再追加尾部空格。这样可以允许用户在补全目录后直接按 Tab 继续深入下一级子目录,无需先删除空格。 ## Examples - Input: `@src/com` + Tab → Output: `@src/components/` (no trailing space) - Input: `/dir add ./pac` + Tab → Output: `/dir add ./packages/` (no trailing space) - File completions still append a space (e.g., `@src/file.txt `) ## Changes - Added `isDirectory` flag to `Suggestion` and `CommandCompletionItem` interfaces - Updated `handleAutocomplete` to skip trailing space when `isDirectory === true` - Modified `getDirPathCompletions` to return `CommandCompletionItem[]` with `isDirectory: true` - Added test case for directory completion behavior * fix(cli): append trailing / to directory completions for deeper navigation * fix(cli): propagate isDirectory and fix JSDoc comment ## Comment 2: Fix JSDoc in SuggestionsDisplay Removed "(ends with /)" from isDirectory description since it was factually incorrect. ## Comment 3: Add test for isDirectory propagation - Added test suite in useSlashCompletion.test.ts to verify directory command structure - Real filesystem testing is done in directoryCommand.test.tsx * fix(cli): add comprehensive isDirectory propagation tests Added getDirPathCompletions unit tests that verify: - Directory suggestions include isDirectory: true - Directory values end with / for continued navigation - Prefix filtering preserves isDirectory flag - Comma-separated path completion works correctly - Deeply nested directories maintain isDirectory flag This closes the testing gap identified in review comment 3. * fix(cli): address wenshao feedback - lint rules, real test, cross-platform Fixes 4 new review comments from wenshao: - [Critical] Empty catch {} blocks: guarded with if (tempTestDir) + void err - [Critical] useSlashCompletion.no-op test: replaced with real integration test that verifies isDirectory propagation through toSuggestion pass-through - [Suggestion] Windows path separator: using path.sep instead of hardcoded / in both directoryCommand.tsx and related test assertions * fix(cli): remove unused import and fix Windows path separator in tests - Remove unused directoryCommand import in useSlashCompletion.test.ts (TS6133) - Replace hardcoded / regex with path.sep-aware assertions in directoryCommand.test.tsx to fix Windows CI failures Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * Apply suggestion from @wenshao Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * Update packages/cli/src/ui/commands/directoryCommand.test.tsx Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * Update packages/cli/src/ui/commands/directoryCommand.tsx Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * Update packages/cli/src/ui/commands/directoryCommand.tsx Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * fix(cli): normalize isDirectory to explicit boolean in toSuggestion Normalize isDirectory from three-state (true/false/undefined) to explicit boolean (true/false) to prevent latent bugs in future code that might distinguish between false and undefined. Fixes review comment: isDirectory normalization is inconsistent across completion paths. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * Update packages/cli/src/ui/hooks/useSlashCompletion.ts Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * chore: remove accidentally committed pr_body.md Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: add pr_body.md to .gitignore Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): remove duplicate .slice and orphaned test code from directoryCommand.tsx Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): only suppress trailing space for dir completions at end-of-line When isDirectory is true, the trailing space was suppressed unconditionally, even when the cursor is mid-line. This caused directory completions to merge directly with following text (e.g. '@src/components/something'). Now only suppress the space when the cursor is at end-of-line, allowing continued Tab navigation into subdirectories. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(cli): document crawler path separator dependency for isDirectory check The isDirectory detection uses p.endsWith('/') which depends on the crawler in @qwen-code/qwen-code-core normalizing paths with posix '/' (fdir.withPathSeparator('/') in crawler.ts). Add a comment to make this implicit coupling explicit. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): add mid-line directory completion test Verify that directory completions append a trailing space when the cursor is mid-line, preventing the completed path from merging with following text. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * Update packages/cli/src/ui/hooks/useCommandCompletion.test.ts Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> --------- Co-authored-by: 方磊 <fanglei@192.168.1.11> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>4 天前
chore: remove google registry 9 个月前
chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) * chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22 ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2, so this PR also bumps: - Node engines (root + cli + core) 20 -> 22 - React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep the transitive React graph deduped to a single instance) - @types/node pinned to 20.19.1 via overrides to avoid an unrelated Dirent NonSharedBuffer regression in sessionService tests - @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint regression introduced by the 1.6.x rule additions - react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2) - ink hoisted to root devDeps so workspace-private peer-dep contention doesn't push ink-link/spinner/gradient into nested workspace installs (which would skip transitive resolution for terminal-link) Workflow + image + installer alignment: - .nvmrc 20 -> 22 - Dockerfile node:20-slim -> node:22-slim - CI test matrix drops 20.x (keeps 22.x + 24.x) - terminal-bench workflow Node 20 -> 22 - Linux/Windows install scripts upgrade their Node version targets Documentation alignment: - README.md badge + prerequisites - AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md, docs/users/configuration/settings.md, docs/developers/contributing.md, docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md, packages/sdk-typescript/README.md, packages/zed-extension/README.md, scripts/installation/INSTALLATION_GUIDE.md Test gating: - Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput> through ink-testing-library now race ink 7's frame-throttled input delivery and land on the wrong option. The maintainers had already marked one of them unreliable (skip on Win32 + CI+Node20). Extend that gate to cover all environments until upstream ink-testing-library ships an ink-7-compatible release that flushes input deterministically. The other test now uses it.skip with the same comment. No business code changes. Verified locally: - npm run typecheck across all workspaces: clean - npm run lint (root): clean - npm run test --workspaces: cli 312/312 files, 4918 passed, 9 skipped core 266/266 files, 6836 passed, 3 skipped webui 6/6, 201 passed sdk 40/40, 283 passed, 1 skipped - npm ls ink: single ink@7.0.2 instance across all peer deps - single react@19.2.4 instance Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: align Node 22 floor across all shipping artifacts Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump leaked: SDK package metadata, web-templates engines, /doctor runtime check, main bundler target, and SDK bundler target. Each was a separate escape hatch letting Node 18/20 consumers install or run the artifact on an unsupported runtime. - packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0 - packages/web-templates/package.json: engines.node >=20 -> >=22 - packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22 - esbuild.config.js: target node20 -> node22 (main CLI bundle) - packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs) - packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+ Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci(e2e): bump E2E workflow Node matrix to 22.x Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version 20.x while root engines is now >=22, so every E2E run on push would either fail at npm ci with engine error or silently exercise the bundle on a runtime that's no longer in ci.yml's test matrix. The macOS job in the same workflow already reads .nvmrc (which is 22) so this only updates the Linux matrix. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2 predates this upgrade and forces every consumer (including ink) to v9, while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested install under node_modules/ink/, so ink 7 was running with a transitive dep one major below its declared minimum. Dropping the global override lets ink resolve its own wrap-ansi 10 nested install (now visible in the lockfile under node_modules/ink/node_modules/wrap-ansi), while the cli package's own direct `wrap-ansi: 9.0.2` dependency keeps the cli code path (TableRenderer.tsx) on the version it has been tested against. The nested cliui override is preserved for yargs which still needs v7. Verified via `npm ls wrap-ansi`: - ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested) - @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged) - yargs/cliui -> wrap-ansi@7.0.0 (unchanged) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): un-skip placeholder ID reuse after deletion Reviewer (tanzhenxin) flagged that the new it.skip on the 'should reuse placeholder ID after deletion' test was undisclosed in the PR description and removed coverage of real product behavior (freePlaceholderId / bracketed-paste backspace path) without a TODO(#NNNN) link. Their argument was sound: the skip rationale pointed at ink 7's input throttle, but this same file just bumped the wait helper from 50ms to 150ms specifically to give ink 7 frame time. Re-running the test under the bumped wait shows it passes reliably (5/5 runs in the full-file context, 9/10 alone), so the skip was masking the throttle-flake that the wait bump already addresses, not a real product bug. Drop the it.skip and the now-stale comment so coverage of the freePlaceholderId reuse logic is restored. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): bump first prompt-suggestion test wait to 350ms The "accepts and submits the prompt suggestion on Enter when the buffer is empty" test is the first in its describe block, so it pays the renderer cold-start cost. On macOS-22.x CI runners that pushes the Enter → onSubmit microtask past the default 150ms post-Enter wait. Match the 350ms initial render wait used immediately above to absorb the cold start. * Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms" This reverts commit 6add83b62ea80c551c81f54af1fda3e6e7478f55. * test(InputPrompt): wait for followup suggestion debounce before pressing Enter Root cause of the failing prompt-suggestion tests on macOS and Windows CI is not flaky timing of the test post-Enter wait — it's the 300ms debounce inside createFollowupController.setSuggestion (shared core). The Enter handler reads followup.state.isVisible synchronously, so if the debounce timer has not fired before stdin.write('\\r'), the suggestion path is skipped and onSubmit never runs. No amount of post-Enter wait can recover from that — the keypress was already processed against stale state. The original wait(350) only left ~50ms margin over the 300ms debounce, which ink 7 / React 19.2 mount overhead consumed on slow Windows runners. Bump the initial wait to 700ms (named SUGGESTION_VISIBLE_WAIT_MS) to give the debounce timer + cold-start render a generous buffer. Apply to the two sibling tests too — without the wait their "does not accept" assertions pass trivially when suggestion is never visible, which is a false green that hides regressions in the actual reject path. * fix(deps): align cli wrap-ansi with ink 7 (9.0.2 -> ^10.0.0) Ink 7 ships its own wrap-ansi@10. CLI's direct dep was pinned to 9.0.2, causing two copies of wrap-ansi in node_modules and a potential drift in CJK width / ANSI handling between ink's internal text wrapping and our TableRenderer. Upgrading the CLI's direct dep to ^10.0.0 lets npm dedupe to a single wrap-ansi@10 used by both ink and TableRenderer. API surface is identical; the only documented behaviour change is that tabs are expanded to 8-column tab stops before wrapping, which TableRenderer doesn't feed in. TableRenderer test suite (43 tests) passes against wrap-ansi@10. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore(deps): document @types/node 20.x pin in overrides The override pinning @types/node to 20.19.1 (while engines require Node >=22) is intentional: bumping to @types/node@22.x re-introduces a Dirent<NonSharedBuffer> type regression that breaks @qwen-code/qwen-code-core/sessionService tests. Add a sibling "//@types/node" note inside `overrides` so future maintainers see the rationale and know when to revisit the pin without having to dig through PR #3860 history. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(AskUserQuestionDialog): link skipped Submit-tab test to tracking issue The 'shows unanswered questions as (not answered) in Submit tab' test was switched to `it.skip` in the ink 7 upgrade because `ink-testing-library@4.0.0` doesn't flush input deterministically through ink 7's 30fps throttle. Add a `// TODO(#4036):` marker so the skip is greppable and can be re-enabled once upstream ships an ink-7-compatible release. Refs #4036 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): move @types/node pin comment out of overrides block npm's `overrides` field requires every key to be a real package name — the `"//@types/node"` comment-key added in 205855875 trips Arborist with "Override without name" and breaks `npm ci` across all CI jobs. Move the explanation to a sibling top-level `"//overrides"` key, which npm ignores at the document root. Same documentation value, no override-parser collateral damage. --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>16 天前
Merge branch 'main' into feat/add-vscode-settings-json-schema2 个月前
pre-release commit 9 个月前
Sync upstream Gemini-CLI v0.8.2 (#838) 6 个月前
docs(agents,pr-template): add Working Principles and restructure PR template (#4496) * docs(agents): add Working Principles and file/comment conventions Add a "Working Principles" section at the top of AGENTS.md, with Simplicity First (adapted from Andrej Karpathy's CLAUDE.md) as the lead principle. Extend Code Conventions with two new entries: - File naming: PascalCase for React components, kebab-case preferred for new non-component files, existing camelCase stays as-is. - Comments: default to none; explain why, not what. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(agents): link Karpathy's CLAUDE.md in attribution Per review feedback, make the source attribution clickable so reviewers can reach the original document in one hop. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(agents): align comments guidance — "default to none" Raise the bar for code comments from "add sparingly" to "default to none" in the runtime prompt, matching the AGENTS.md convention. Add a preservation clause to AGENTS.md so agents do not strip existing high-value comments during cleanup passes. Update snapshots. Co-Authored-By: Qwen Code <noreply@alibaba-inc.com> * docs(pr-template): restructure for reviewer test plan clarity - Reorganize PR template around a Reviewer Test Plan section with How to verify, Before/After, and Tested on - Add collapsible Chinese description section for bilingual PRs - Simplify create-pr command guidance to match the new template - Tighten AGENTS.md file naming and comments conventions; align PR submission guide with the new template This makes PRs easier to review by focusing contributors on the evidence reviewers need most. * docs(pr-template): merge Before/After into Evidence and require full Chinese translation - Consolidate Before and After sections into a single Evidence (Before & After) section - Update Chinese summary comment to require full paragraph-by-paragraph translation instead of abbreviated bullets This reduces template redundancy for non-UI changes and ensures the Chinese block is a proper translation, not a summary. --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Qwen Code <noreply@alibaba-inc.com>2 天前
chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) * chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22 ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2, so this PR also bumps: - Node engines (root + cli + core) 20 -> 22 - React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep the transitive React graph deduped to a single instance) - @types/node pinned to 20.19.1 via overrides to avoid an unrelated Dirent NonSharedBuffer regression in sessionService tests - @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint regression introduced by the 1.6.x rule additions - react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2) - ink hoisted to root devDeps so workspace-private peer-dep contention doesn't push ink-link/spinner/gradient into nested workspace installs (which would skip transitive resolution for terminal-link) Workflow + image + installer alignment: - .nvmrc 20 -> 22 - Dockerfile node:20-slim -> node:22-slim - CI test matrix drops 20.x (keeps 22.x + 24.x) - terminal-bench workflow Node 20 -> 22 - Linux/Windows install scripts upgrade their Node version targets Documentation alignment: - README.md badge + prerequisites - AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md, docs/users/configuration/settings.md, docs/developers/contributing.md, docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md, packages/sdk-typescript/README.md, packages/zed-extension/README.md, scripts/installation/INSTALLATION_GUIDE.md Test gating: - Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput> through ink-testing-library now race ink 7's frame-throttled input delivery and land on the wrong option. The maintainers had already marked one of them unreliable (skip on Win32 + CI+Node20). Extend that gate to cover all environments until upstream ink-testing-library ships an ink-7-compatible release that flushes input deterministically. The other test now uses it.skip with the same comment. No business code changes. Verified locally: - npm run typecheck across all workspaces: clean - npm run lint (root): clean - npm run test --workspaces: cli 312/312 files, 4918 passed, 9 skipped core 266/266 files, 6836 passed, 3 skipped webui 6/6, 201 passed sdk 40/40, 283 passed, 1 skipped - npm ls ink: single ink@7.0.2 instance across all peer deps - single react@19.2.4 instance Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: align Node 22 floor across all shipping artifacts Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump leaked: SDK package metadata, web-templates engines, /doctor runtime check, main bundler target, and SDK bundler target. Each was a separate escape hatch letting Node 18/20 consumers install or run the artifact on an unsupported runtime. - packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0 - packages/web-templates/package.json: engines.node >=20 -> >=22 - packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22 - esbuild.config.js: target node20 -> node22 (main CLI bundle) - packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs) - packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+ Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci(e2e): bump E2E workflow Node matrix to 22.x Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version 20.x while root engines is now >=22, so every E2E run on push would either fail at npm ci with engine error or silently exercise the bundle on a runtime that's no longer in ci.yml's test matrix. The macOS job in the same workflow already reads .nvmrc (which is 22) so this only updates the Linux matrix. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2 predates this upgrade and forces every consumer (including ink) to v9, while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested install under node_modules/ink/, so ink 7 was running with a transitive dep one major below its declared minimum. Dropping the global override lets ink resolve its own wrap-ansi 10 nested install (now visible in the lockfile under node_modules/ink/node_modules/wrap-ansi), while the cli package's own direct `wrap-ansi: 9.0.2` dependency keeps the cli code path (TableRenderer.tsx) on the version it has been tested against. The nested cliui override is preserved for yargs which still needs v7. Verified via `npm ls wrap-ansi`: - ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested) - @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged) - yargs/cliui -> wrap-ansi@7.0.0 (unchanged) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): un-skip placeholder ID reuse after deletion Reviewer (tanzhenxin) flagged that the new it.skip on the 'should reuse placeholder ID after deletion' test was undisclosed in the PR description and removed coverage of real product behavior (freePlaceholderId / bracketed-paste backspace path) without a TODO(#NNNN) link. Their argument was sound: the skip rationale pointed at ink 7's input throttle, but this same file just bumped the wait helper from 50ms to 150ms specifically to give ink 7 frame time. Re-running the test under the bumped wait shows it passes reliably (5/5 runs in the full-file context, 9/10 alone), so the skip was masking the throttle-flake that the wait bump already addresses, not a real product bug. Drop the it.skip and the now-stale comment so coverage of the freePlaceholderId reuse logic is restored. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): bump first prompt-suggestion test wait to 350ms The "accepts and submits the prompt suggestion on Enter when the buffer is empty" test is the first in its describe block, so it pays the renderer cold-start cost. On macOS-22.x CI runners that pushes the Enter → onSubmit microtask past the default 150ms post-Enter wait. Match the 350ms initial render wait used immediately above to absorb the cold start. * Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms" This reverts commit 6add83b62ea80c551c81f54af1fda3e6e7478f55. * test(InputPrompt): wait for followup suggestion debounce before pressing Enter Root cause of the failing prompt-suggestion tests on macOS and Windows CI is not flaky timing of the test post-Enter wait — it's the 300ms debounce inside createFollowupController.setSuggestion (shared core). The Enter handler reads followup.state.isVisible synchronously, so if the debounce timer has not fired before stdin.write('\\r'), the suggestion path is skipped and onSubmit never runs. No amount of post-Enter wait can recover from that — the keypress was already processed against stale state. The original wait(350) only left ~50ms margin over the 300ms debounce, which ink 7 / React 19.2 mount overhead consumed on slow Windows runners. Bump the initial wait to 700ms (named SUGGESTION_VISIBLE_WAIT_MS) to give the debounce timer + cold-start render a generous buffer. Apply to the two sibling tests too — without the wait their "does not accept" assertions pass trivially when suggestion is never visible, which is a false green that hides regressions in the actual reject path. * fix(deps): align cli wrap-ansi with ink 7 (9.0.2 -> ^10.0.0) Ink 7 ships its own wrap-ansi@10. CLI's direct dep was pinned to 9.0.2, causing two copies of wrap-ansi in node_modules and a potential drift in CJK width / ANSI handling between ink's internal text wrapping and our TableRenderer. Upgrading the CLI's direct dep to ^10.0.0 lets npm dedupe to a single wrap-ansi@10 used by both ink and TableRenderer. API surface is identical; the only documented behaviour change is that tabs are expanded to 8-column tab stops before wrapping, which TableRenderer doesn't feed in. TableRenderer test suite (43 tests) passes against wrap-ansi@10. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore(deps): document @types/node 20.x pin in overrides The override pinning @types/node to 20.19.1 (while engines require Node >=22) is intentional: bumping to @types/node@22.x re-introduces a Dirent<NonSharedBuffer> type regression that breaks @qwen-code/qwen-code-core/sessionService tests. Add a sibling "//@types/node" note inside `overrides` so future maintainers see the rationale and know when to revisit the pin without having to dig through PR #3860 history. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(AskUserQuestionDialog): link skipped Submit-tab test to tracking issue The 'shows unanswered questions as (not answered) in Submit tab' test was switched to `it.skip` in the ink 7 upgrade because `ink-testing-library@4.0.0` doesn't flush input deterministically through ink 7's 30fps throttle. Add a `// TODO(#4036):` marker so the skip is greppable and can be re-enabled once upstream ships an ink-7-compatible release. Refs #4036 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): move @types/node pin comment out of overrides block npm's `overrides` field requires every key to be a real package name — the `"//@types/node"` comment-key added in 205855875 trips Arborist with "Override without name" and breaks `npm ci` across all CI jobs. Move the explanation to a sibling top-level `"//overrides"` key, which npm ignores at the document root. Same documentation value, no override-parser collateral damage. --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>16 天前
chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) * chore(deps): upgrade ink 6.2.3 -> 7.0.2 + bump Node engine to 22 ink 7 requires Node >=22 and react-reconciler 0.33 with React >=19.2, so this PR also bumps: - Node engines (root + cli + core) 20 -> 22 - React/react-dom 19.1 -> 19.2.4 (pinned exact via overrides to keep the transitive React graph deduped to a single instance) - @types/node pinned to 20.19.1 via overrides to avoid an unrelated Dirent NonSharedBuffer regression in sessionService tests - @vitest/eslint-plugin pinned to 1.3.4 to avoid an unrelated lint regression introduced by the 1.6.x rule additions - react-devtools-core 4.28 -> 6.1 (ink 7 peerOptional requires >=6.1.2) - ink hoisted to root devDeps so workspace-private peer-dep contention doesn't push ink-link/spinner/gradient into nested workspace installs (which would skip transitive resolution for terminal-link) Workflow + image + installer alignment: - .nvmrc 20 -> 22 - Dockerfile node:20-slim -> node:22-slim - CI test matrix drops 20.x (keeps 22.x + 24.x) - terminal-bench workflow Node 20 -> 22 - Linux/Windows install scripts upgrade their Node version targets Documentation alignment: - README.md badge + prerequisites - AGENTS.md, CONTRIBUTING.md, docs/users/quickstart.md, docs/users/configuration/settings.md, docs/developers/contributing.md, docs/developers/sdk-typescript.md, docs/users/extension/extension-releasing.md, packages/sdk-typescript/README.md, packages/zed-extension/README.md, scripts/installation/INSTALLATION_GUIDE.md Test gating: - Two AuthDialog/AskUserQuestionDialog tests that drive <SelectInput> through ink-testing-library now race ink 7's frame-throttled input delivery and land on the wrong option. The maintainers had already marked one of them unreliable (skip on Win32 + CI+Node20). Extend that gate to cover all environments until upstream ink-testing-library ships an ink-7-compatible release that flushes input deterministically. The other test now uses it.skip with the same comment. No business code changes. Verified locally: - npm run typecheck across all workspaces: clean - npm run lint (root): clean - npm run test --workspaces: cli 312/312 files, 4918 passed, 9 skipped core 266/266 files, 6836 passed, 3 skipped webui 6/6, 201 passed sdk 40/40, 283 passed, 1 skipped - npm ls ink: single ink@7.0.2 instance across all peer deps - single react@19.2.4 instance Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: align Node 22 floor across all shipping artifacts Reviewer (tanzhenxin) flagged five surfaces where the >=22 engine bump leaked: SDK package metadata, web-templates engines, /doctor runtime check, main bundler target, and SDK bundler target. Each was a separate escape hatch letting Node 18/20 consumers install or run the artifact on an unsupported runtime. - packages/sdk-typescript/package.json: engines.node >=18.0.0 -> >=22.0.0 - packages/web-templates/package.json: engines.node >=20 -> >=22 - packages/cli/src/utils/doctorChecks.ts: MIN_NODE_MAJOR 20 -> 22 - esbuild.config.js: target node20 -> node22 (main CLI bundle) - packages/sdk-typescript/scripts/build.js: target node18 -> node22 (esm + cjs) - packages/cli/src/utils/doctorChecks.test.ts: rename test label to v22+ Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * ci(e2e): bump E2E workflow Node matrix to 22.x Reviewer (tanzhenxin) flagged that e2e.yml still pinned node-version 20.x while root engines is now >=22, so every E2E run on push would either fail at npm ci with engine error or silently exercise the bundle on a runtime that's no longer in ci.yml's test matrix. The macOS job in the same workflow already reads .nvmrc (which is 22) so this only updates the Linux matrix. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): drop root wrap-ansi override so ink 7 gets its declared dep Reviewer (tanzhenxin) flagged that the root overrides.wrap-ansi: 9.0.2 predates this upgrade and forces every consumer (including ink) to v9, while ink 7 declares wrap-ansi: ^10.0.0. The lockfile had no nested install under node_modules/ink/, so ink 7 was running with a transitive dep one major below its declared minimum. Dropping the global override lets ink resolve its own wrap-ansi 10 nested install (now visible in the lockfile under node_modules/ink/node_modules/wrap-ansi), while the cli package's own direct `wrap-ansi: 9.0.2` dependency keeps the cli code path (TableRenderer.tsx) on the version it has been tested against. The nested cliui override is preserved for yargs which still needs v7. Verified via `npm ls wrap-ansi`: - ink@7.0.2 -> wrap-ansi@10.0.0 (newly nested) - @qwen-code/qwen-code -> wrap-ansi@9.0.2 (unchanged) - yargs/cliui -> wrap-ansi@7.0.0 (unchanged) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): un-skip placeholder ID reuse after deletion Reviewer (tanzhenxin) flagged that the new it.skip on the 'should reuse placeholder ID after deletion' test was undisclosed in the PR description and removed coverage of real product behavior (freePlaceholderId / bracketed-paste backspace path) without a TODO(#NNNN) link. Their argument was sound: the skip rationale pointed at ink 7's input throttle, but this same file just bumped the wait helper from 50ms to 150ms specifically to give ink 7 frame time. Re-running the test under the bumped wait shows it passes reliably (5/5 runs in the full-file context, 9/10 alone), so the skip was masking the throttle-flake that the wait bump already addresses, not a real product bug. Drop the it.skip and the now-stale comment so coverage of the freePlaceholderId reuse logic is restored. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(InputPrompt): bump first prompt-suggestion test wait to 350ms The "accepts and submits the prompt suggestion on Enter when the buffer is empty" test is the first in its describe block, so it pays the renderer cold-start cost. On macOS-22.x CI runners that pushes the Enter → onSubmit microtask past the default 150ms post-Enter wait. Match the 350ms initial render wait used immediately above to absorb the cold start. * Revert "test(InputPrompt): bump first prompt-suggestion test wait to 350ms" This reverts commit 6add83b62ea80c551c81f54af1fda3e6e7478f55. * test(InputPrompt): wait for followup suggestion debounce before pressing Enter Root cause of the failing prompt-suggestion tests on macOS and Windows CI is not flaky timing of the test post-Enter wait — it's the 300ms debounce inside createFollowupController.setSuggestion (shared core). The Enter handler reads followup.state.isVisible synchronously, so if the debounce timer has not fired before stdin.write('\\r'), the suggestion path is skipped and onSubmit never runs. No amount of post-Enter wait can recover from that — the keypress was already processed against stale state. The original wait(350) only left ~50ms margin over the 300ms debounce, which ink 7 / React 19.2 mount overhead consumed on slow Windows runners. Bump the initial wait to 700ms (named SUGGESTION_VISIBLE_WAIT_MS) to give the debounce timer + cold-start render a generous buffer. Apply to the two sibling tests too — without the wait their "does not accept" assertions pass trivially when suggestion is never visible, which is a false green that hides regressions in the actual reject path. * fix(deps): align cli wrap-ansi with ink 7 (9.0.2 -> ^10.0.0) Ink 7 ships its own wrap-ansi@10. CLI's direct dep was pinned to 9.0.2, causing two copies of wrap-ansi in node_modules and a potential drift in CJK width / ANSI handling between ink's internal text wrapping and our TableRenderer. Upgrading the CLI's direct dep to ^10.0.0 lets npm dedupe to a single wrap-ansi@10 used by both ink and TableRenderer. API surface is identical; the only documented behaviour change is that tabs are expanded to 8-column tab stops before wrapping, which TableRenderer doesn't feed in. TableRenderer test suite (43 tests) passes against wrap-ansi@10. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore(deps): document @types/node 20.x pin in overrides The override pinning @types/node to 20.19.1 (while engines require Node >=22) is intentional: bumping to @types/node@22.x re-introduces a Dirent<NonSharedBuffer> type regression that breaks @qwen-code/qwen-code-core/sessionService tests. Add a sibling "//@types/node" note inside `overrides` so future maintainers see the rationale and know when to revisit the pin without having to dig through PR #3860 history. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(AskUserQuestionDialog): link skipped Submit-tab test to tracking issue The 'shows unanswered questions as (not answered) in Submit tab' test was switched to `it.skip` in the ink 7 upgrade because `ink-testing-library@4.0.0` doesn't flush input deterministically through ink 7's 30fps throttle. Add a `// TODO(#4036):` marker so the skip is greppable and can be re-enabled once upstream ships an ink-7-compatible release. Refs #4036 Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(deps): move @types/node pin comment out of overrides block npm's `overrides` field requires every key to be a real package name — the `"//@types/node"` comment-key added in 205855875 trips Arborist with "Override without name" and breaks `npm ci` across all CI jobs. Move the explanation to a sibling top-level `"//overrides"` key, which npm ignores at the document root. Same documentation value, no override-parser collateral damage. --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>16 天前
Sync upstream Gemini-CLI v0.8.2 (#838) 6 个月前
feat: update docs 5 个月前
feat(cli,sdk): qwen serve daemon (Stage 1) (#3889) * feat(cli): scaffold `qwen serve` HTTP daemon (Stage 1, #3803) Adds a `serve` subcommand that boots an Express 5 listener with bearer auth, host allowlist, and CORS modeled on `vscode-ide-companion/src/ ide-server.ts`. Ships only `/health` and `/capabilities` to begin with; session/prompt/event routes will land in follow-up PRs once the per- session ACP child-process bridge in `httpAcpBridge.ts` is wired. Defaults to 127.0.0.1 with auth disabled so local development needs no configuration. Binding beyond loopback (e.g. `--hostname 0.0.0.0`) refuses to start without a token (`--token` or `QWEN_SERVER_TOKEN`). Capabilities envelope versioned at v=1 with a `features` array — clients should gate UI off `features`, never off `mode`, so subsequent PRs can add capability tags without breaking older clients. Per design issue's Stage 1 scope (~700-1000 LOC). Adds ~430 LOC of implementation + tests in this scaffold; the remaining budget belongs to the route wiring + bridge implementation in follow-ups. * feat(cli): wire HttpAcpBridge + POST /session for `qwen serve` (#3803) Stage 1 follow-up to the scaffold. Implements the bridge between the HTTP daemon and the existing ACP child agent, plus the first session endpoint. `HttpAcpBridge.spawnOrAttach`: - Spawns `node $cliEntry --acp` per workspace via an injectable `ChannelFactory` (default uses `process.argv[1]`; tests use an in-memory `TransformStream` pair so they don't fork real processes). - Drives the ACP `initialize` + `newSession` handshake via the SDK's `ClientSideConnection`, with a 10s timeout that kills the channel. - Under `sessionScope: 'single'` (default), reuses the live session when the same canonical workspace cwd is requested again — backs the `attached: true` flag. - The `Client` impl on the bridge side proxies file reads/writes to local fs (daemon and agent share the host) and buffers `sessionUpdate` notifications for the SSE wiring in the next PR. `requestPermission` returns `cancelled` until the `/permission/:requestId` route lands. `POST /session`: - 400 on missing or relative `cwd`. - 200 with `{sessionId, workspaceCwd, attached}` on success. - 500 on bridge failure (the failing channel is killed, not leaked). `runQwenServe` constructs the bridge and ties `bridge.shutdown()` into the listener-close path so SIGINT/SIGTERM drain children before the socket closes. Tests (14 new, 0 regressions in the 4967-test baseline): - 9 bridge cases over an in-memory channel — fresh spawn, single-scope reuse, cross-workspace isolation, thread-scope independence, path canonicalization, relative-path rejection, init failure cleanup, init timeout, multi-channel shutdown. - 4 route cases for /session (missing/relative/200/500). - 1 lifecycle case asserting `runQwenServe.close()` calls `bridge.shutdown()` before closing the listener. Verified end-to-end: `qwen serve` boots, `POST /session` spawns a real `qwen --acp` child and returns the SDK-assigned `sessionId`, repeat calls under the same cwd return `attached: true`, `SIGTERM` reaps the child along with the listener. * feat(cli): wire POST /session/:id/prompt + /cancel for `qwen serve` (#3803) Stage 1 follow-up after the bridge scaffold. Adds the two routes a client needs to actually run a turn against the daemon. Bridge: - `sendPrompt(sessionId, req)` looks up the session, FIFO-queues the call against the per-session prompt queue, and forwards through the SDK `ClientSideConnection.prompt`. Concurrent calls observe ACP's "one active prompt per session" invariant — second waits for first. - A failed prompt does NOT poison the queue; the tail catches and keeps draining so the next caller still runs (the original caller still sees its own rejection). - `cancelSession(sessionId, req?)` bypasses the queue and forwards the ACP notification immediately. ACP semantics: the agent winds down the *currently active* prompt; queued work is unaffected. - Both methods throw `SessionNotFoundError` (a typed Error subclass) when the id is unknown so route handlers can map cleanly to 404 without brittle message matching. - Both methods overwrite the `sessionId` field in the request body with the routing id — a stale or spoofed body would otherwise be dispatched to the wrong agent process. Routes: - `POST /session/:id/prompt` → 200 with PromptResponse, 400 on missing/non-array prompt, 404 on unknown session, 500 on agent error. - `POST /session/:id/cancel` → 204 always (cancel is a notification), 404 on unknown session. Tests (14 new — 7 bridge + 7 route, 0 regressions in the 4981 baseline): - sendPrompt: success forwards & returns response · routing-id overrides body sessionId · concurrent prompts FIFO-serialize (verified via per-prompt start/end ordering with a release latch) · failed prompt doesn't block subsequent prompts · 404 for unknown id. - cancelSession: forwards with routing id · 404 for unknown id. - Routes: 200/400/404/500 paths for prompt; 204 with body or empty + 404 for cancel. Verified end-to-end against a real `qwen --acp` child: - POST /session/:id/prompt with `[{type:'text',text:'hi'}]` → 200 `{"stopReason":"end_turn"}` in ~3.4s. - POST /session/:id/cancel → 204. - POST /session/does-not-exist/prompt → 404 with the unknown id surfaced in the body. * feat(cli): wire SSE streaming for `qwen serve` events (#3803) Stage 1 follow-up that turns prompt into a real streaming experience. Replaces the in-memory `notifications: SessionNotification[]` buffer on each session with a per-session EventBus and exposes it through `GET /session/:id/events` as an `text/event-stream` SSE feed. EventBus (`packages/cli/src/serve/eventBus.ts`): - Monotonic per-session ids (`v: 1` schema). Each `publish` chains an id, returning the materialized BridgeEvent. - Bounded ring (default 1000) backs `Last-Event-ID` reconnect — a consumer that drops can resume from `lastEventId` and replay any still-buffered events before live events flow. - Per-subscriber bounded queue (default 256). When a slow consumer overruns its queue, the bus appends a synthetic `client_evicted` terminal frame and closes that subscription so it can't hold the daemon hostage. Other subscribers are unaffected. - `subscribe()` returns an AsyncIterable — registration is synchronous so events `publish`ed immediately after the subscribe land in the queue (a generator-style implementation deferred registration to first `next()` and raced with publishes). - AbortSignal-aware: aborting the signal closes the iterator promptly. Bridge (`httpAcpBridge.ts`): - `BridgeClient.sessionUpdate` now publishes onto the session's EventBus instead of pushing to a plain array — every ACP notification the agent emits becomes a stream event automatically. - New `subscribeEvents(sessionId, opts?)` returns the bus's AsyncIterable; throws `SessionNotFoundError` for unknown ids. - Shutdown closes every live event bus before killing channels so pending consumers unwind cleanly. Route (`server.ts`): - `GET /session/:id/events` sets the SSE content type, advertises a 3s reconnect hint, and writes a 15s heartbeat comment frame to keep proxy/NAT connections alive. - Forwards the `Last-Event-ID` header to the bus. - `req.on('close')` triggers an AbortController that propagates into the bridge subscription so disconnects don't leak subscribers. - 404 when the bridge can't find the session. Capabilities envelope: `STAGE1_FEATURES` now advertises `session_create`, `session_prompt`, `session_cancel`, `session_events` in addition to `health`/`capabilities` so clients can light up UI for the routes that have actually shipped. Tests (16 new, 0 regressions in the 4995 baseline): - 9 EventBus unit cases — id sequencing, live delivery, replay, replay+live splice, fan-out to N subscribers, eviction on overflow, abort-signal unsubscribe, bus.close() drains subscribers, ring-size eviction. - 4 bridge subscribe cases — 404, sessionUpdate→event publishing via real ACP fake-agent, shutdown closes live subscriptions. - 4 SSE route cases against a live HTTP listener — frame format, Last-Event-ID forwarding, 404, abort propagation on disconnect. Verified end-to-end against a real `qwen --acp` child: - Subscribed to `/session/$SID/events`, fired `POST /session/$SID/prompt` with text content. Captured 13 distinct `event: session_update` SSE frames in real time during the model's response — `available_ commands_update` metadata, 9 `agent_thought_chunk` frames carrying the model's chain-of-thought, 3 `agent_message_chunk` frames with the actual reply, and a final usage frame with token totals. - Frames carry monotonic ids 1..13, the daemon-side counter, and are valid SSE per the EventSource spec. * feat(cli): wire POST /permission/:requestId for `qwen serve` (#3803) Stage 1 follow-up that turns `BridgeClient.requestPermission` from a hardcoded `cancelled` placeholder into a real first-responder vote loop, and ships the HTTP route any attached client uses to cast the deciding vote. Bridge: - `requestPermission` generates a UUID requestId, registers a pending entry on a daemon-wide map (and the owning session's `pendingPermissionIds` set), publishes a `permission_request` event onto the session's EventBus (so SSE subscribers see it), and awaits the resolution. - New `respondToPermission(requestId, response)` resolves the pending promise with the supplied outcome. First call wins — subsequent calls return false. On success the bridge publishes a `permission_resolved` event so other attached clients can update their UI when the race is decided. - `cancelSession` and `shutdown` both resolve every still-pending permission for the affected session(s) as `{ outcome: { outcome: 'cancelled' } }` per the ACP spec requirement that a cancelled prompt MUST resolve outstanding requestPermission calls with cancelled. - New `pendingPermissionCount` getter exposes inflight count for inspection / tests. Route (`server.ts`): - `POST /permission/:requestId` validates the body's `outcome` is either `{ outcome: 'cancelled' }` or `{ outcome: 'selected', optionId: string }`, then forwards to `bridge.respondToPermission`. - 200 on accepted vote, 404 when the requestId is unknown or already resolved (Stage 1 doesn't differentiate), 400 on a malformed outcome. Capabilities envelope: STAGE1_FEATURES gains `permission_vote`. Tests (14 new — 9 bridge + 5 route, 0 regressions in the 5011 baseline): - Bridge: publishes permission_request with a generated requestId and waits; respondToPermission first-responder wins; publishes permission_resolved on vote; respondToPermission false for unknown requestId; cancelSession resolves outstanding as cancelled; shutdown resolves outstanding as cancelled. - Route: 200 on selected outcome; 200 on cancelled outcome; 404 on unknown requestId; 400 on malformed outcome; 400 on missing outcome. Verified end-to-end against a real `qwen --acp` child: - Subscribed to /session/$SID/events, sent a prompt asking the agent to write a file at /tmp/qwen-serve-permission-e2e-test.txt. - The agent triggered a permission_request via the bus, surfacing the three options Qwen Code presents (Allow Always / Allow / Reject) with their option ids. - POSTed `{outcome:{outcome:"selected",optionId:"proceed_once"}}` to /permission/$requestId — got HTTP 200. - Bus published the matching permission_resolved event. - Agent proceeded with the writeTextFile tool call; file was actually created on disk with the expected content. * feat(sdk): add DaemonClient for the qwen serve HTTP API (#3803) Stage 1 follow-up that proves the cross-mode protocol-isomorphism design assumption: an SDK client can drive the daemon's HTTP routes end-to-end without going through ProcessTransport's stdio + stream-json path. DaemonClient is a sibling of ProcessTransport, not a replacement. The two speak different protocols (ACP NDJSON over HTTP vs stream-json over stdio). Existing `query()` users keep getting subprocess-mode unchanged; applications that want daemon-mode (cross-client attach, shared MCP pool, network reachability, first-responder permissions) opt in by constructing a DaemonClient against a running `qwen serve`. API surface (`packages/sdk-typescript/src/daemon/`): - `new DaemonClient({ baseUrl, token?, fetch? })`. The `fetch` override is for tests; defaults to `globalThis.fetch`. Trailing slashes on `baseUrl` are stripped. - `health()`, `capabilities()` — discovery. - `createOrAttachSession({ workspaceCwd, modelServiceId? })` — `attached: true` on the response indicates a session was reused under sessionScope:single. - `prompt(sessionId, { prompt: ContentBlock[] })` — returns PromptResult with stopReason. - `cancel(sessionId)` — tolerates 204; throws on 404. - `subscribeEvents(sessionId, { lastEventId?, signal? })` — async iterator over parsed SSE frames; AbortSignal-aware. Native Node AbortController only — jsdom polyfills are incompatible with undici. - `respondToPermission(requestId, response)` — first-responder vote; returns true on 200, false on 404 (lost the race or unknown id), throws on 400/500. `DaemonHttpError` is thrown for any non-2xx (besides the 404 "already-resolved" case on permission votes); carries `status` and `body` so callers can branch on standard daemon HTTP semantics. `parseSseStream(body)` is the underlying SSE parser; exported separately so applications can consume daemon SSE outside the DaemonClient surface. Handles split-chunk frames, comment/retry directives, malformed JSON (skip), trailing frame without final newline. Wire types live SDK-side (no SDK→CLI dep); the capabilities envelope's `v` field signals breaking changes. Tests (26 new, 0 regressions in the 201 baseline): - 7 SSE parser cases — single frame, multiple frames, comments, chunked-split frame, malformed JSON skip, trailing frame on close, empty stream. - 19 DaemonClient cases — health success/error, capabilities, bearer auth presence/absence, createOrAttachSession success/400, prompt body shape + sessionId url-encoding, cancel 204/404, permission 200/400/404, subscribeEvents header forwarding + 404, baseUrl normalization. Verified end-to-end against a real `qwen serve` daemon driving a real `qwen --acp` child: - `client.capabilities()` returned `{v:1, mode:"http-bridge", features: [...7 tags]}`. - First `createOrAttachSession` returned `attached:false`; second returned `attached:true` with the same sessionId. - `client.prompt(...)` with text content yielded `{stopReason: "end_turn"}` while the parallel `subscribeEvents` iterator streamed 10 distinct frames during the same turn. - AbortController on the events iterator cleanly severed the SSE connection. * feat(cli,sdk): list workspace sessions + set session model (#3803) Closes the §04 Stage-1 routes table for `qwen serve` with the two remaining endpoints, plus matching SDK methods. `GET /workspace/:id/sessions` - `:id` is the URL-encoded canonical absolute workspace path (Express decodes path params automatically; clients pass `encodeURIComponent(cwd)`). - Returns `{ sessions: [{ sessionId, workspaceCwd }, ...] }` for live sessions whose canonical workspace matches. - Empty array (not 404) when the workspace is idle so picker UIs don't have to special-case "no sessions yet". - 400 when the decoded path isn't absolute. `POST /session/:id/model` - Body: `{ modelId: string, ... }`. The route's `:id` overrides any spoofed sessionId in the body. - Forwards to ACP's `unstable_setSessionModel` and publishes a `model_switched` event onto the session bus so cross-client UIs update. - 200 with the agent's response on success, 400 on missing/empty modelId, 404 on unknown session. - The SDK method is currently unstable; documented in the bridge comment in case the spec renames the method when it stabilizes. Bridge: - New `listWorkspaceSessions(workspaceCwd)` iterates `byId.values()` and filters by canonical workspace path; works for both `single` and `thread` session scopes. - New `setSessionModel(sessionId, req)` forwards through `connection.unstable_setSessionModel`, normalizes sessionId, publishes `model_switched`, throws SessionNotFoundError on unknown ids. `STAGE1_FEATURES` capabilities envelope grows to 9 tags, adding `session_list` and `session_set_model`. SDK (`DaemonClient`): - `listWorkspaceSessions(workspaceCwd)` URL-encodes the cwd and returns the parsed `sessions` array directly. - `setSessionModel(sessionId, modelId)` POSTs the body and returns the agent response (currently opaque per ACP unstable spec). - Wire types `DaemonSessionSummary` and `SetModelResult` exported from the SDK barrel. Tangential cleanup: `sendBridgeError` now extracts a useful message from non-Error values via a small `errorMessage` helper. JSON-RPC errors from the agent (`{code, message, data}`) used to surface as `"[object Object]"` in the 500 response body; they now show the inner `message` field. Caught while running the model-set e2e. Tests (17 new — 9 bridge + 7 route + 4 SDK, 0 regressions in the 5022 + 227 baselines): - Bridge listWorkspaceSessions: matching cwd returns the live sessions; canonicalizes the lookup; empty for relative paths. - Bridge setSessionModel: forwards modelId + overrides body sessionId; publishes model_switched event; 404 unknown session. - Route /workspace/:id/sessions: returns the bridge list; empty for idle workspace; 400 for relative path. - Route /session/:id/model: 200 success; 400 missing modelId; 400 empty modelId; 404 unknown session. - SDK listWorkspaceSessions: URL-encodes the cwd; throws on 400. - SDK setSessionModel: posts body; throws on 404. Verified end-to-end against a real `qwen serve`: - SDK reports 9 capability features, list returns the existing session, attached:true on repeat create, and `setSessionModel` rejects with HTTP 500 when the modelId isn't registered (with the daemon now surfacing "Internal error" instead of "[object Object]"). - 404 path through SDK on unknown sessionId works. * fix(cli,sdk): audit round 1 follow-ups for `qwen serve` (#3803) Self-review pass on PR #3889. Two real correctness bugs and an ergonomics gap, plus the test-coverage holes the audit surfaced. The loudest finding ("host allowlist no-op when bind=localhost") was a false positive — the conditional was misread; existing tests already prove the validator is active on `localhost` binds. Real fixes: - Bearer-auth timing-attack: `parts[1] !== token` short-circuits per byte, leaking which prefix is correct via response latency. Replace with SHA-256 of both sides + `crypto.timingSafeEqual` so comparison is constant-time regardless of token length. - Concurrent `spawnOrAttach` race in single-scope: two parallel callers for the same workspace both passed the `byWorkspace.get` check, both spawned, and one entry ended up orphaned in `byId` while the other won `byWorkspace`. Violates the "at most one session per workspace" invariant. Coalesce via an `inFlightSpawns` map: parallel callers attach to the in-flight promise and report `attached: true`. The slot is cleared on both success and rejection so a failed spawn doesn't poison the workspace forever. New test asserts ONE channel spawns under parallel calls and that retry works after rejection. - `Number.parseInt('1.5e10z', 10)` returns 1, so a malformed `Last-Event-ID` header silently passes through. Tighten `parseLastEventId` to `^\d+$` so anything not a pure decimal integer is dropped. New test exercises 'abc', '-1', '1.5e10z'. Ergonomics: - `LOOPBACK_BINDS` and `LOOPBACK_HOST_BINDS` now include `::1` and `[::1]`. IPv6 loopback users no longer have to set a token. Host-allowlist allows `[::1]:port` Host headers. Documentation: - `BridgeClient` doc-comment now states the Stage 1 trust model explicitly: agent runs as the same UID, the file-proxy methods are NOT a workspace-cwd sandbox, restricting them would be theatre. The audit flagged this as a "design gap" but the daemon-and-agent-on-same-host posture makes a sandbox here redundant — Stage 4+ remote-sandbox swaps the Client for a sandbox-aware variant. SDK fix: - `DaemonClient.failOnError` previously called `res.json()`, which consumes the body even on parse-failure; the subsequent `res.text()` returned empty. New impl reads once as text and attempts JSON-parse; raw text is the fallback. New test asserts a `text/plain` 502 surfaces the body verbatim. Test gap fills (audit-flagged): - Bridge: in-memory file-proxy tests for `BridgeClient.{read,write} TextFile` including line/limit slicing. - SSE route: `stream_error` synthetic frame on iterator throw mid-stream; numeric Last-Event-ID forwarded; malformed Last-Event-ID dropped. - DaemonClient: text/plain error body coerced to `body` field; `respondToPermission` 5xx throws; `subscribeEvents` null-body throws; `cancel`/`respondToPermission` URL-encode session/request ids that contain slashes. Verified end-to-end with a token-required daemon: right token → 200, wrong/missing/malformed → 401. All paths return uniform 401 messages so a side-channel can't distinguish between "no header", "bad scheme", and "wrong token". Test counts: cli serve **89** (was 81, +8), sdk daemon **35** (was 30, +5). Full suites still green. * fix(cli): audit round 2 follow-ups for `qwen serve` (#3803) Second self-review pass on PR #3889. Three real bugs (one correctness, one resource-cleanup, one cosmetic) plus consolidation of the loopback bindings into a single source of truth. Real fixes: - Shutdown could hang forever on a long-lived SSE consumer: `server.close` waits for every in-flight connection to drain, and a paused EventSource client never disconnects. Added a `SHUTDOWN_FORCE_CLOSE_MS` (5s) timer that calls `server.closeAllConnections()` to force-destroy stuck sockets, then resolves so `process.exit(0)` can run. New test asserts close completes well under 5.5s even when an SSE GET is in flight. - Signal-handler race during shutdown: round 1 detached the SIGINT/SIGTERM listeners *up front* in `handle.close()`. If a second SIGTERM arrived during the drain, no handler existed and Node's default termination ran, orphaning agent children. Switch to detaching at the *end* of the close path (in `finish()`): during the drain window the handler is still attached and the `if (shuttingDown) return` guard makes a second signal a no-op; after drain completes we can safely remove the listeners (this also fixes a test-suite MaxListenersExceededWarning that fired once we ran the runQwenServe tests >10 times in a single process). - SSE response had no `error` listener. When the underlying TCP socket died (RST, kill -9 on the client), the next `res.write` threw EPIPE and Express forwarded it to the default error handler, logging noisily. Added `res.on('error', cleanup)` so the failure is absorbed and triggers the same teardown path the `req.on('close')` handler uses. Validation: - `createHttpAcpBridge` now throws on invalid `sessionScope` (anything other than `'single'` or `'thread'`) and on `initializeTimeoutMs <= 0`. Misconfigured callers used to silently degrade to thread behavior; now they fail loudly. Cleanup: - The `LOOPBACK_BINDS` set was duplicated between `auth.ts` and `runQwenServe.ts` (round 1 missed this). Extracted into `packages/cli/src/serve/loopbackBinds.ts` with a single `isLoopbackBind(hostname)` helper. Both files now import; drift is impossible. - `res.flushHeaders?.()` lost the optional chaining. The method is on `http.ServerResponse` since Node 1.6; our `engines` floor is 20. Tests added: - bridge: `sessionScope` validation, `initializeTimeoutMs` validation. - server: shutdown force-close timeout, SIGINT/SIGTERM listener detach-after-drain. False positives from the round 2 audit (verified and dismissed): - "EventBus nextId overflow at 2^53" — theoretical only (would require ~9 quadrillion publishes per session). No code change. - "Subscribe-during-close race" — JS is single-threaded; the close() flag is set synchronously before the loop touches state. - "Queued prompts on shutdown" — by design; documented via the promptQueue tail comment. - "10MB body parser limit" — design choice for Stage 1's in-memory buffering model; revisit if ACP streaming lands in Stage 2. - "Unbounded body read in DaemonClient.failOnError" — daemon is local in Stage 1; the threat surface for adversarial-large error bodies is the same as the daemon's other unbounded buffers. Test counts: cli serve **93** (was 89, +4), full cli **5047** (no regressions), sdk **236** (no regressions). * docs(cli): audit rounds 3 + 4 follow-ups for `qwen serve` (#3803) Two more self-review passes on PR #3889. No correctness bugs surfaced this time — round 3 found a HIGH-severity Windows-path claim that turned out to be a false positive (`path.win32.isAbsolute('/foo/bar')` returns true; verified against Node 20). Round 4 confirmed every prior decision and surfaced one latent-but-not-currently-triggered concurrency note. Changes are pure documentation + a tiny optional-chain cleanup: - Drop `?.` on `server.closeAllConnections()` in runQwenServe.ts — the method exists since Node 18.2 and our `engines` floor is 20. The optional chain dated from before round 2's force-close timer landed; clean it up. - Help text for `qwen serve --port` now documents that port 0 means "OS-assigned ephemeral port" (which the implementation has always supported but never advertised). - `defaultSpawnChannelFactory` gains a comment near the spawn site documenting the FD-budget implication (~3 FDs per session, bump `ulimit -n` for many concurrent sessions) and the `stdio: ['pipe', 'pipe', 'inherit']` choice (child stderr lands in the daemon's stderr, interleaved across sessions). Both are Stage-1-accepted; Stage 2/4+ revisit each. - Comment on the bridge's `byWorkspace`/`byId` Maps documenting the known gap that a child crashing between requests leaves a garbage SessionEntry until daemon shutdown — surfaced as a per-prompt failure when the dead session is touched, not a hang. Stage 2's in-process bridge eliminates the spawned-child failure mode entirely so this gap goes away naturally. - `EventBus.subscribe` doc-comment now states explicitly that the returned iterator is NOT safe to drive from concurrent `.next()` callers — the underlying queue isn't atomic. Daemon usage is the sequential `for await ... of` inside the SSE route, so this is safe in production. Documented so a future fan-out consumer doesn't accidentally rely on undefined behavior. False positives verified and dismissed (round 3 + 4 combined): - `path.isAbsolute('/foo/bar')` Windows breakage — `path.win32. isAbsolute('/foo/bar')` is true; verified empirically. - "Windows drive divergence" causing duplicate sessions — different drives are different on-disk paths; sessions intentionally differ. - "parseSseStream early-break leaks reader" — `for await ... break` triggers `iterator.return()` which runs the generator's `finally` that calls `releaseLock`. Standard JS semantics. - "Promise executor sync-throw fragility in requestPermission" — sync throws inside `new Promise(executor)` reject the outer promise; functionally correct, just stylistic. - "Force-close timeout test elapsed assertion flakiness" — assertion is `< 5500ms` but the natural happy-path is sub-100ms. Generous headroom; not flake-prone in practice. - "fetch reference stale after polyfill" — `globalThis.fetch.bind` captures at construction; tests inject `opts.fetch` instead of polyfilling, which is the correct pattern. Test counts unchanged (cli serve **93**, sdk **236**); typecheck + lint clean. STAGE1_FEATURES still matches every implemented route 1:1, fakeBridge in tests implements every HttpAcpBridge method. * fix(cli): PR #3889 review round 1 — critical correctness (#3803) Addresses the four critical findings from the PR #3889 reviewer pass: 1. ACP `ReadTextFileRequest.line` is 1-based per spec, but the bridge's `BridgeClient.readTextFile` was treating it as a 0-based slice index. A client asking for `{line:1, limit:2}` ("first two lines") was getting lines 2-3 — a sign-off-by-one bug that breaks every editor / SDK client following the ACP schema. Convert to 0-based via `Math.max(0, line - 1)`. The existing slice test was asserting the wrong behavior; updated to expect the spec-correct result and added a second `line:3, limit:2` case to lock in the offset. 2. `modelServiceId` was accepted by the SDK + server `POST /session` path, forwarded into `bridge.spawnOrAttach`, and then silently dropped: `doSpawn` never wired it into the agent. Callers requesting a specific model got the agent's default and no indication anything was wrong. Now `doSpawn` issues `unstable_setSessionModel` immediately after `newSession`. If the agent rejects the model id, the half-initialized session is torn down and the spawn rejects so the caller can retry cleanly instead of inheriting silent drift. Three new bridge tests: happy path, omit-when-undefined, agent-rejection cleanup. 3. The CORS middleware used `cors({ origin: (o, cb) => cb(new CORSError(...), false) })` for browser-Origin requests. `cors` flows the Error into Express's error chain; without an explicit error handler that produces a 500 + HTML body, which is misleading for what is really a deterministic 403 denial. Replace with a tiny `RequestHandler` that checks `req.headers.origin` directly and returns `403 { error: 'Request denied by CORS policy' }` JSON. Drops the `cors` and `@types/cors` dependencies — there's no other consumer in the cli package. 4. The SSE `stream_error` synthetic frame hard-coded `id: 0`, which would regress the client's `Last-Event-ID` tracker and trigger duplicate replays on reconnect. The frame is terminal and daemon-emitted — it has no place in the per-session monotonic sequence. Refactor `formatSseFrame` to omit the `id:` line when the input event has no id field, and emit `stream_error` without one. Test updated to assert `frames[1].id === undefined` while the preceding `session_update` still carries its monotonic id. Tangential cleanup: `errorMessage` now formats the SSE error body (was `err.message` only — would have shown `[object Object]` for JSON-RPC errors mid-stream, mirroring the round-1 SDK fix). Test counts: cli serve **96** (was 93, +3 modelServiceId cases); existing readTextFile slice test rewritten in place. Full typecheck + lint + suite green. * fix(cli,sdk): PR #3889 review round 2 — SSE robustness + EventBus polish (#3803) Second batch of reviewer-flagged fixes for PR #3889. Addresses 7 robustness issues across the daemon's SSE pipeline + the bus + the SDK's stream parser. Daemon SSE (`server.ts`): - SSE writes now respect backpressure. `res.write` returns false when the kernel send buffer is full; the previous code ignored that and Node accumulated payloads in user-space memory unboundedly. A slow consumer on a chatty session could balloon daemon RSS. New `writeWithBackpressure` helper awaits `drain` (or `close`/`error`) before scheduling the next write — for both per-frame writes and heartbeats. - `parseLastEventId` rejects values > `Number.MAX_SAFE_INTEGER`. With the prior `^\d+$` regex a malicious 25-digit value would parse to a number that loses precision and confuses replay comparisons. EventBus (`eventBus.ts`): - `Last-Event-ID` replay events now `forcePush` past `maxQueued`. A client reconnecting with a 1000-event gap on a subscriber whose cap is 256 was silently losing entries 257-1000 — a sign-off-by- nothing breakage of the resume contract. Live publishes still go through the normal cap (slow live consumer must be evictable); historical replay is bypassed. - `onAbort` now disposes the subscription immediately instead of only closing the queue. An aborted-but-never-iterated subscriber used to linger in `bus.subs` until the consumer drove `next()` / `return()`. New tests cover both abort-after-subscribe and already-aborted-at-subscribe paths. - `BoundedAsyncQueue.next` now checks `buf.length > 0` before shifting instead of `buf.shift() !== undefined`. The bus never pushes `undefined` today but the queue is generic — the prior pattern would mis-handle a queue whose element type legitimately includes undefined. SDK SSE parser (`sse.ts`): - Now flushes the TextDecoder on stream close. Without the final `decoder.decode()`, an incomplete multi-byte UTF-8 sequence at the tail of the last chunk was silently dropped — corrupting any frame whose JSON ended mid-character. New test feeds a stream split mid-byte through "中" (3-byte UTF-8) and asserts the character round-trips. - Frame separators now accept both `\n\n` and `\r\n\r\n`. SSE spec allows CRLF, and intermediaries (corporate proxies, some Node http servers) sometimes normalize. Frame field splitter also accepts `\r?\n`. Two new tests cover pure CRLF + mixed-LF/CRLF. Test counts: cli serve **99** (was 96, +3 EventBus); sdk daemon-sse **10** (was 7, +3). Full typecheck + lint + suite green. * docs(cli,sdk): PR #3889 review round 3 — minor + docs (#3803) Last batch from the PR #3889 reviewer pass: mostly docs + a ReDoS-tooling-silencing rewrite + a yargs-key cleanup. - `commands/serve.ts` ServeArgs interface dropped the camelCase `httpBridge` mirror; the handler now reads `argv['http-bridge']` matching the declared option name. The dual surface relied on yargs's camelCase expansion behavior — fragile if yargs config ever changes. - `DaemonClient` constructor's `baseUrl.replace(/\/+$/, '')` (which is end-anchored and linear, but CodeQL's polynomial-regex detector flags any `\/+$` pattern on attacker-controlled input) swapped for a hand-rolled `stripTrailingSlashes` loop. Same behavior, no rule trigger. - `defaultSpawnChannelFactory`'s `cwd: workspaceCwd` flow into `spawn` is the second CodeQL finding ("uncontrolled data used in path expression"). It IS user-controlled, by design — that's the Stage 1 trust model. Added a `// lgtm[js/shell-command- constructed-from-input]` suppression with a comment explaining the model and pointing at issue #3803 §11 for the Stage 4+ remote- sandbox replacement. - Stale doc comment on `createServeApp` that still listed only `/health`, `/capabilities`, `POST /session` as shipped — now enumerates all 9 routes that match §04 of the design. - Stale doc comment on `HttpAcpBridge` saying "Stage 1 buffers them in-memory; SSE wiring lands in the next PR" — SSE wiring landed in commit 41aa95094. Replaced with a description of the actual flow through EventBus + SSE. No behavior change; tests + lint + typecheck still green. cli serve still **99**, sdk **38** (was 30 before this batch — daemon-sse +3, DaemonClient +5 from rounds 1+2). Full e2e against built daemon re-verified: CORS denial returns 403 JSON (was 500 HTML), bad `modelServiceId` now causes spawn to fail with HTTP 500 (was: silent default-model substitution), `POST /session` without modelServiceId unaffected. * fix(cli,sdk): self-audit round 5+ — close orphaned EventBus + DaemonEvent.id optional (#3803) Two more fixes from a final post-review-comment audit pass on PR #3889. Both are subtle correctness gaps that fell out of the round-1 critical fixes (modelServiceId apply + SSE id-less stream_error). - In `httpAcpBridge.ts:doSpawn`, when `unstable_setSessionModel` rejects after `newSession` succeeded, we tear down the entry from `byWorkspace` + `byId` (round 1 fix) but did NOT close the EventBus we'd just constructed for that entry. The agent could have published a session_update notification during init that queued in the (now unreachable) bus's ring buffer; without an explicit close the bus + buffer linger until the next GC cycle. Bounded leak (1 bus per failed spawn × 1000-event ring) but cleaner to close it. New regression test exercises the retry path after a model-rejection failure to lock in that we don't reuse the orphan and that subscribers on the fresh session see an empty iterator on immediate abort. - SDK `DaemonEvent.id` is now `id?: number` instead of `id: number`. The round-1 SSE fix made the daemon emit `stream_error` frames *without* an `id:` line so they don't pollute the per-session monotonic sequence. The SDK parser correctly returns `undefined` for the missing field, but the type still advertised `id: number` — TypeScript consumers persisting `lastSeenId = event.id` would accidentally store `undefined`. Made the field optional and added a doc comment instructing consumers to skip frames without an id. Plus one more false-positive verified and dismissed: - "writeWithBackpressure Promise double-settle race": the auditor flagged that `res.write(chunk, callback)` could fire its callback after the synchronous `ok=true` resolve. Verified harmless — Promise double-settle is a no-op, the callback only rejects on error (caught separately by `res.on('error', cleanup)`), and multiple parallel writes register independent listener sets that each remove their own pair after firing. Test counts: cli serve **100** (was 99, +1 retry-after-model-rejection regression). SDK unchanged at 239. Full typecheck + lint + suites green; flow re-verified end-to-end. * fix(cli,sdk): PR #3889 review round 4 — child-crash recovery + SSE/permission/SSE polish (#3803) Fourth and final batch of reviewer-flagged fixes for PR #3889. 14 inline threads addressed, plus 8 spam threads up for resolution. Critical correctness: - `eventBus.test.ts`'s ring-eviction test wrapped its assertion in a `void (async () => { … })()` IIFE that returned synchronously to vitest — the inner `expect` could fail without ever surfacing. Hoisted to a top-level `await` so the harness actually waits and a broken eviction would now fail loudly. - `runQwenServe.ts handle.close()` is now idempotent. Concurrent callers (test harness + signal handler firing simultaneously, explicit caller + finally-block fallback) used to each construct a new shutdown promise, arm a fresh force-close timer, and call `bridge.shutdown` redundantly. Cache a single `closePromise`; repeat calls return it. New test exercises 3 overlapping callers + a post-settle call → exactly one bridge.shutdown. - `POST /permission/:requestId` now rejects `outcome.selected` with an empty `optionId`. The string-typeof check passed `""` through; bridge would forward an opaque "unknown option" error from the agent. Tighten the validator + add a 400 test. - `denyBrowserOriginCors` now has explicit unit tests (3 cases: Origin-bearing GET → 403 JSON, no-Origin GET → 200, Origin-bearing POST → 403 + bridge untouched). The CSRF defense was previously implicit-only. Channel-exit recovery: - `AcpChannel` interface gains an `exited: Promise<void>` that resolves on either planned `kill()` or unexpected child crash. Bridge subscribes via `channel.exited.then(...)`: if the entry is still in `byId` when exit fires (i.e. unexpected crash), it cancels pending permissions, publishes a `session_died` event so SSE subscribers get notified, closes the bus, and removes the entry from `byWorkspace`/`byId`. Without this, a crashed child used to leave its `SessionEntry` stuck — under `sessionScope:'single'` (default) the whole workspace was unreachable until daemon restart. - `defaultSpawnChannelFactory` now wires `child.once('error', …)` in addition to `'exit'`. Without an `error` listener Node treats an async spawn failure (ENOMEM, EACCES, …) as an unhandled error and crashes the daemon. - Two new bridge tests: `crash()` simulates an unexpected exit → asserts `session_died` event + entry removed + retry spawns a fresh child; planned shutdown asserts the cleanup handler no-ops when the entry is already gone (no double-publish). SSE robustness: - SDK `parseSseStream` now calls `reader.cancel()` (not just `releaseLock`) in its `finally`. Early-break consumers were leaving the underlying HTTP body stream open; cancel propagates upstream so the connection drops promptly. New test asserts the underlying ReadableStream's `cancel()` runs. - SDK `parseSseStream` accepts `data:` (no space after colon) AND multiple `data:` lines per frame (joined by `\n` per spec). Two new tests cover both cases. - SDK `DaemonClient.subscribeEvents` now validates response Content-Type before delegating to the parser. A misconfigured proxy returning 200 + JSON was silently producing zero events; now throws `DaemonHttpError` with the actual mime type. - Daemon SSE route's initial `retry: 3000` write now `.catch(()=>{})`s. A socket that errors before the first write would have surfaced as an unhandled rejection. Documentation (deferred items now noted in code): - `EventBus.publish` ring shift is O(n) when full. Comment notes the deferral; circular-buffer refactor only if profiling flags it. - SSE heartbeat doesn't detect dead connections without TCP RST. Comment notes Stage 2 may add an explicit idle timeout. - `defaultSpawnChannelFactory` won't run a `.ts` entry directly — `npm run dev` users must build first. Comment in the spawn site. Test counts: cli serve **107** (was 100, +7), SDK daemon **42** (was 38, +4). Full typecheck + lint + suite green. * test(integration): qwen serve daemon — routes + streaming + recovery (#3803) Persists the e2e validation of every PR #3889 fix as vitest integration tests under `integration-tests/cli/`. Two files split by auth requirement: `qwen-serve-routes.test.ts` (18 cases, no LLM credential needed) - Bearer auth timing-safe compare: right token / wrong-same-length / wrong-shorter / missing / Basic-scheme. - CORS browser-Origin denial: GET-with-Origin → 403 JSON; no-Origin → 200. - Capabilities envelope: all 9 Stage 1 features advertised in order. - POST /session validation: relative cwd → 400; two parallel POSTs same workspace coalesce; bad modelServiceId tears down half-init. - POST /permission/:requestId validation: empty optionId → 400; missing optionId → 400; valid vote on unknown id → 404. - SDK SSE Content-Type guard: throws DaemonHttpError when upstream returns 200 + JSON. - Last-Event-ID strict parsing: malformed value accepted but ignored (`'1abc'` doesn't get parsed as 1). - Cancel idempotent + listWorkspaceSessions returns the live session. `qwen-serve-streaming.test.ts` (3 cases, gated by SKIP_LLM_TESTS) - Real `qwen --acp` child SIGKILL → daemon publishes `session_died`, removes the entry from `byWorkspace`/`byId`, next createOrAttachSession spawns fresh. Uses `pgrep -P` to locate the daemon's direct child by PID. - Two SSE subscribers + a tool requiring permission: both observe the same `permission_request` requestId; two concurrent POST votes resolve as exactly one 200 + one 404 (first-responder wins). - SSE reconnect with `Last-Event-ID: N` after consuming N frames yields events with `id > N` from the bus's replay ring. Both files spawn `node packages/cli/dist/index.js serve --port 0 --token …` per `beforeAll` and clean up in `afterAll`. Use the existing `@qwen-code/sdk` alias the integration-tests vitest config already wires to the built SDK bundle. Run with the existing `npm run test:integration:cli:sandbox:none` (or any of the integration-tests target). The streaming file is skip-able via `SKIP_LLM_TESTS=1` for environments without auth. Verified locally: 18/18 routes pass in ~6.8s; 3/3 streaming pass in ~23s against a real model. * fix(cli): PR #3889 review round 5 — claude-opus-4-7 audit (#3803) Seven new substantive findings from a `/qreview` pass on PR #3889. Six real bugs + one type-safety gap; all addressed. Critical correctness: - **EventBus replay overflow + eviction race**. Round 4's `forcePush` for `Last-Event-ID` replay bypassed the per-subscriber cap, but `BoundedAsyncQueue.push`'s cap check was `buf.length >= maxSize` — so the very next live publish saw the inflated buf, rejected, and triggered the `client_evicted` terminal frame. Concrete sequence the audit walked through: client reconnects after 300+ events, replay force-pushes 300 entries, next live event evicts them. Defeats the resume contract. Fix: track force-pushed items separately (`forcedInBuf` counter). `push()` cap is now on `(buf.length - forcedInBuf)`. `next()` decrements `forcedInBuf` as the consumer drains (force-pushed entries are FIFO at the front of `buf` since `forcePush` only runs at subscribe time, before any live `push`). Two new regression tests: (1) live publish after a >cap replay does NOT evict; (2) eviction triggers only after the LIVE backlog (excluding replay) hits the cap. Performance + UX: - **Eager express import on every `qwen` invocation**. The `serve` subcommand statically imported `../serve/index.js`, which transitively pulled express + body-parser + qs into cold-start path of every CLI invocation (interactive, mcp, channel, etc). ~50ms tax on the 99% of invocations that never run `serve`. Defer to dynamic `import()` inside the handler; types are still imported for the builder shape. - **Middleware order**: `express.json({limit:'10mb'})` ran BEFORE `bearerAuth`. Unauth POST got full JSON.parse before 401. Trivial DoS amp on non-loopback deployments. Reorder so auth + Host allowlist + CORS run first; body parser runs only for requests that pass the gate. - **`sendPrompt` no AbortSignal**. A stuck/dead child poisons the per-session FIFO; HTTP client disconnect didn't propagate so daemon CPU stayed tied up. `HttpAcpBridge.sendPrompt` now accepts `signal?: AbortSignal`. Route handler creates an AbortController and wires `req.on('close')` to abort it. On abort, bridge sends an ACP `cancel` notification; the agent winds down → prompt resolves with `stopReason: 'cancelled'` → next queued prompt can run. New test exercises real socket disconnect via `node:http` (jsdom AbortSignal isn't compatible with undici). Security: - **`--token` on argv leaks via `/proc/<pid>/cmdline`**. Default Linux permissions allow any local user to `ps auxww | grep 'qwen serve'` and read the bearer token. Daemon now warns to stderr when `--token` is used and recommends `QWEN_SERVER_TOKEN` (which uses `/proc/<pid>/environ`, owner-only). - **Token inherited by spawned `qwen --acp` child**. `env: process.env` in `defaultSpawnChannelFactory` passed `QWEN_SERVER_TOKEN` into the child. The agent runs user-supplied prompts with shell-tool access — leaving the token in env enables prompt-injection-into-self-call attacks. Strip `QWEN_SERVER_TOKEN` from the child's env before spawn. Robustness: - **`BridgeClient` publishes lacked try/catch on closed bus**. `BridgeClient.requestPermission` and `sessionUpdate` called `entry.events.publish(...)` directly. Shutdown closes the bus *before* killing the channel, so a late `sessionUpdate` from a not-yet-dead agent throws. For `requestPermission` the throw was particularly bad: `registerPending` had already mutated the daemon-wide map, so the throw left the registry inconsistent. Cleaner fix: make `EventBus.publish` a no-op on closed bus (returns undefined) instead of throwing. Removes the need for try/catch at every call site and keeps state consistent. Type safety: - **`STAGE1_FEATURES: readonly string[]`** widened the inferred tuple-of-literals back to `string[]`. A typo'd feature (`'sesion_set_model'`) compiled silent. Drop the annotation + add `as const`; export `Stage1Feature` literal-union for SDK-side `features.includes(...)` checks to narrow against. Test counts: cli serve **112** (was 105, +7); SDK unchanged at 243. Full typecheck + lint + suite green. * fix(cli): PR #3889 review round 6 — gpt-5.5 audit (#3803) Four new findings from a `/review` pass on PR #3889. Three real correctness bugs + one Stage 1 design-gap documentation. Critical: - **`[::1]` bind ENOTFOUND**. `LOOPBACK_BINDS` accepts `[::1]` for the auth gate, but `app.listen()` wants the unbracketed `::1`; `qwen serve --hostname [::1]` passed the gate and then crashed with ENOTFOUND. Strip brackets at bind-time, keep them for the printed URL. New test asserts the listener actually binds when the operator types `[::1]`. - **`sendPrompt` no transport-close detection**. The chained `entry.connection.prompt()` could hang indefinitely if the `qwen --acp` child wedged or the underlying stream broke mid-flight (the SDK's pending JSON-RPC promise never delivers a response). Because the per-session FIFO tail derives from that promise, a single stuck prompt poisoned every subsequent caller for the same session. Round 4's `channel.exited` is already wired to remove the entry, but the in-flight prompt itself wasn't racing it. Fix: race `entry.connection.prompt(...)` against `entry.channel.exited` inside `sendPrompt`; when the transport closes mid-flight, the prompt fast-fails with a descriptive error rather than hanging the queue. New test exercises this via a stuck fake agent + manual `crash()`. Real correctness: - **`spawnOrAttach` attach-path ignored modelServiceId**. Under `sessionScope:'single'` (default) a client requesting a specific model on attach got `attached:true` while continuing to use whatever model the shared session already had — a silent contract drift. Refactor the per-session `unstable_setSessionModel` call into a shared `applyModelServiceId(entry, modelId)` helper that runs both at create-time (existing path) AND on attach-with-model. Same helper publishes the `model_switched` event so cross-client UIs see the change. New tests cover apply-on-attach and the omit-modelServiceId-on-attach no-op case. Stage 1 design: - **`BridgeClient.{readTextFile, writeTextFile}` raw fs proxy**. The audit flagged that the bridge reimplements file I/O with `fs.{read,write}File` instead of delegating to core's filesystem service — divergence on BOM handling, non-UTF-8 encodings, original line endings. Wiring core's FileSystemService through the bridge is invasive (constructor dep, reaches into core's runtime), and Stage 2's in-process bridge eliminates the proxy entirely. Documented as a known gap with the exact user-visible scenarios; no behavior change in this PR. Test counts: cli serve **116** (was 112, +4); full cli **5070** (was 5066, +4); SDK unchanged at 243. Lint + typecheck green. * fix(cli): PR #3889 review round 7 — match CodeQL suppression to fired query (#3803) Single new CodeQL alert (#201) on `workspaceCwd → spawn({cwd})`. The round-3 suppression I added (`lgtm[js/shell-command-constructed-from- input]`) referenced the WRONG query id — the alert fires the `js/path-injection` query, not the shell-command one. The misnamed suppression also lived 30+ lines above the actual flagged spawn call, out of CodeQL's annotation scope. Move the suppression onto the line immediately preceding the spawn call and use the matching query id `js/path-injection`. The function-level comment block above still documents the Stage 1 trust model rationale (operator-controlled cwd is intentional; agent runs as same UID with shell-tool access; Stage 4+ remote sandbox replaces this factory entirely). Defense-in-depth note added: `workspaceCwd` is canonicalized via `path.resolve()` in `spawnOrAttach` before reaching this factory, and spawn's `cwd` doesn't pass through any shell. No behavior change. Test counts unchanged (cli serve 116, full cli 5070). * fix(cli): self-audit round 8 — concurrency + listener leak + IPv6 + CodeQL honesty (#3803) Multi-round audit pass on PR #3889 commits 5/6/7. Four findings, one real high-severity. High: - Attach-with-modelServiceId had no error recovery and no FIFO. If the agent rejected the new model on attach, `applyModelServiceId` threw, the route 500'd, and the existing session kept running the OLD model — caller sees a 500 with no easy way to detect the state. Worse, two simultaneous attaches with different modelServiceIds would race the `unstable_setSessionModel` calls with no serialization. Add a per-session `modelChangeQueue` (parallel to `promptQueue`); `applyModelServiceId` now chains through it. On failure publishes a `model_switch_failed` event to the bus so OTHER attached clients can see what happened (the failed-caller still gets the 500). Two new bridge tests cover rejection observability + concurrent FIFO. Medium: - `sendPrompt` was adding a `.then` listener to `entry.channel.exited` PER CALL, accumulating linearly with prompt count over a session's lifetime. ~hundreds of bytes per prompt; trivially observable on chatty long-running sessions. Cache a single `transportClosedReject` lazy-init promise on SessionEntry; every subsequent prompt's race uses the same promise. Low: - `[host]:port` IPv6 syntax in `--hostname` was being naively bracket-stripped to `host]:port`, which Node rejects with a cryptic ENOTFOUND at startup. Tighten the strip to only accept pure `[addr]` forms; reject the URL-with-port form upfront with a useful error pointing at `--port`. - `BoundedAsyncQueue.forcedInBuf` invariant comment was wrong: it claimed force-pushed items were always at the front of `buf`, but the eviction-frame path force-pushes at the BACK. The miscount that follows is functionally inert (`close()` blocks the next cap check), but the comment was actively misleading. Rewrote it to honestly describe both call paths and explain why the eviction-case miscount is harmless. CodeQL honesty: - Round 7's `// lgtm [js/path-injection]` comment doesn't actually suppress alerts — GitHub Code Scanning ignores inline `lgtm` annotations (LGTM.com retired 2021). Replaced the misleading `// lgtm` line with a NOTE block stating the constraint explicitly: suppression requires UI dismissal or `.github/codeql/codeql-config.yml`, both out of scope for a code-only PR. The function-level comment that explains the Stage 1 trust model rationale stays. Test counts: cli serve **119** (was 116, +3); full cli **5073** (was 5070, +3, no regressions). * fix(cli): self-audit round 9-10 — reject empty-bracket --hostname (#3803) Final fix from rounds 9-10 of the audit chain. One real concern + three nice-to-have test gaps that the code already handles correctly. - `--hostname '[]'` (empty brackets) used to slip past the bracket validator: `slice(1, -1)` produced `''`, which Node interprets as "bind to all interfaces". An operator typing `[]` clearly meant something specific, not wildcard. Reject the empty-inner case upfront with the same useful error as the `[host]:port` case. New test asserts the rejection. Round 10 ran a clean convergence pass and signed off: - Cross-cutting state invariants (byWorkspace, byId, inFlightSpawns, pendingPermissions, plus all per-entry queues and caches) — all mutations paired and async holes safe. - All test names match assertions. - Public type surface clean (DaemonEvent.id?, Stage1Feature CLI-only, DaemonClientOptions.fetch shape correct). - Production paths verified: non-executable child times out at 10s init, multiple-daemon EADDRINUSE rejects cleanly via `server.once('error', reject)`. - Three "missing test" notes (transportClosedReject cache sharing, full subscribe-publish-evict sequence, modelChangeQueue failure isolation) are diagnostic gaps — the code paths are correct and covered by adjacent tests. Test counts: cli serve **120** (was 119, +1 empty-bracket); SDK unchanged at 243. * docs(cli): note SSE single-line data emit vs multi-line parser (#3803) formatSseFrame emits the payload as a single `data:` line. The EventSource spec also allows a frame to span multiple `data:` lines (joined by `\n` on parse), and the SDK receive-side parser handles that variant — but we never emit it because the JSON payload has no embedded newlines after JSON.stringify. Document the in/out asymmetry so future readers don't mistake the absence of newline splitting for a bug. Closes review thread AMgP0. * fix(cli,sdk): close 11 #3889 review threads — race + leak + IPv6 + SSE Critical correctness: - setSessionModel now serializes through `entry.modelChangeQueue` so POST /session/:id/model can't race with the attach-with-different- modelServiceId path that already chains on the same queue. Without this two concurrent model changes interleave and the published `model_switched` event may not match the agent's actual model. - POST /session reaps the spawned child when the client disconnected during the 1-3s spawn window (`req.aborted && !session.attached`). Without this, every aborted request leaks one orphan child the daemon can't address by sessionId. Attached sessions skip the kill — another client legitimately owns them. - spawnOrAttach refuses dispatch once shutdown has started (`shuttingDown` flag set at the top of `shutdown()`). Late-arrivers on already-established HTTP connections that pass `server.close`'s rejection of NEW connections would otherwise spawn children the shutdown snapshot already missed. Late re-check inside `doSpawn` (after `connection.newSession` resolves) catches the in-flight case and tears down the half-built channel. - sendPrompt early-aborts pre-aborted callers before queuing — saves a queue trip and gives a clean trace for retry-after-abort flows. Defensive: - parseSseStream caps the unread buffer at 16 MiB. Without this, an upstream that returns non-SSE (misconfigured proxy, long-lived non-streaming body) feeds `buf` until the consumer OOMs. - parseSseStream now accepts an optional AbortSignal that is checked at each iteration, and DaemonClient.subscribeEvents forwards `opts.signal` into it. Post-200 aborts now actually stop iteration instead of buffering frames until the upstream closes. - DaemonClient.fetchTimeoutMs (30s default) wraps every short-poll method (health/capabilities/createOrAttachSession/listWorkspaceSessions/ setSessionModel/cancel/respondToPermission) with `AbortSignal.timeout`. Composes with caller-provided signals via `AbortSignal.any`. `prompt` is intentionally exempt (long-lived: model + tool turns can take minutes); `subscribeEvents` is exempt (long-lived SSE). - New `bridge.killSession(sessionId)` API mirrors the shutdown teardown for a single session — used by POST /session orphan-reap above and exposed for future routes that need targeted cleanup. Stale + cosmetic: - Bridge map header comment said "no path that removes a session... when its child process crashes between requests" — out of date since the `channel.exited` cleanup landed in an earlier audit round. Rewritten to describe the actual cleanup chain. - runQwenServe now wraps IPv6 hostname literals in brackets when building the URL (`http://[::1]:4170` not `http://::1:4170`). The bracket-stripping logic on `listenHostname` already handled `app.listen()` correctly; this fixes the printed/copy-paste URL. - Dead `mode: ServeMode` variable in serve.ts removed (the runQwenServe call hardcodes `mode: 'http-bridge'`); the warning condition is now inlined. Test plan: - `vitest run` cli/serve: 120/120 + 49/49 (httpAcpBridge) pass - `vitest run` sdk-typescript daemon: 42/42 pass - tsc --build packages/cli packages/sdk-typescript: clean - ESLint: clean * chore(lint): allow mime/lite in import/no-internal-modules (#3803) `packages/core/src/utils/fileUtils.ts` and its test import `mime/lite`, which is mime@4's documented public sub-export (a smaller bundle that omits the legacy mime DB) — not an internal module. The rule has been flagging these on PR CI runs even though main's CI happens to pass (likely stale-cache vs fresh-install timing). Add `mime/lite` to the allowlist so lint is consistent across main and PR runs. * fix(cli,sdk): close 14 review threads — env whitelist + races + Windows tests + structured errors (#3803) Critical correctness: - registerPending now resolves orphaned permissions as cancelled when the entry has been torn down between the agent's `requestPermission` decision and the bridge handler firing. Previously the permission would hang the agent forever (killSession's pendingPermissionIds iteration didn't include the just-orphaned id, shutdown's clear() dropped it without resolving). - Workspace key now goes through `realpathSync.native` (with a resolved-but-uncanonicalized fallback for non-existent paths) so case-insensitive filesystems (macOS APFS, Windows NTFS) don't silently degrade `sessionScope: 'single'` into "one session per spelling". Matches how `config.ts` / `settings.ts` / `sandbox.ts` resolve workspace paths. - killChild gets a hard 10s deadline after SIGKILL so a child stuck in uninterruptible sleep (D-state, e.g. NFS read on a dead server) can't block `bridge.shutdown()`'s `Promise.all` forever. `SHUTDOWN_FORCE_CLOSE_MS` in `runQwenServe` only covers `server.close()` — without this hard kill, daemon shutdown hangs. - setSessionModel now races the agent call against `transportClosedReject` and wraps in `withTimeout`, matching what `sendPrompt` and `applyModelServiceId` already do. Without the race, a wedged child blocks `POST /session/:id/model` forever. Also publishes a `model_switch_failed` SSE event on rejection so passive subscribers see the failure (matches `applyModelServiceId`). - shutdown() now awaits `inFlightSpawns` so the late-shutdown re-check inside `doSpawn` finishes its half-built channel teardown before `bridge.shutdown()` resolves. Without the await, `runQwenServe.close()` returns and `process.exit(0)` is queued before the orphan tears itself down, surfacing a stderr error AFTER the daemon claimed graceful shutdown. - sendPrompt re-checks `signal.aborted` immediately after `addEventListener` so a microsecond-window synchronous abort that fires between the early-exit check and listener registration still triggers the agent `cancel` notification. Security: - `defaultSpawnChannelFactory` now passes an *allowlisted* environment to the spawned `qwen --acp` child instead of `{ ...process.env }` with `QWEN_SERVER_TOKEN` deleted. The agent runs user-supplied prompts with shell-tool access; anything in its env (OPENAI/ ANTHROPIC/DASHSCOPE keys, AWS/GCP credentials, DB passwords, OAuth tokens) is reachable by prompt injection. Allowlist covers HOME/PATH/USER/LOGNAME/LANG/LC_*/TMPDIR/TEMP/TMP/NODE_PATH plus Windows essentials (SYSTEMROOT/USERPROFILE/APPDATA/...). The explicit `delete childEnv['QWEN_SERVER_TOKEN']` stays as defense-in-depth — anyone grepping for the token name finds the scrub explicitly named. Observability: - 5xx responses now carry structured `code` and `data` fields when the underlying error has them (JSON-RPC errors from the ACP SDK forward as `{code, message, data}`). Without this, every distinct failure (quota / rate-limit / auth / crash) collapses to the same opaque "Internal error" string at the client. - 5xx errors log to stderr (via `writeStderrLine`, not `console.error`, to keep the no-console lint rule happy). Stop-gap until structured access/error logging lands. - Eviction frame on EventBus subscriber overflow no longer consumes a `nextId` slot. The synthetic frame burning a sequence id meant healthy subscribers saw gaps (3 → 5) that the resume ring couldn't back-fill — silently broke the `BridgeEvent.id` "monotonic per- session" contract. `BridgeEvent.id` is now optional on the type to make the absence honest. Same pattern as `stream_error`. Cross-platform: - httpAcpBridge.test.ts now derives expected paths via `path.resolve(path.sep, 'work', 'a')` (factored out as `WS_A`/ `WS_B`/`SESS_A` constants) instead of hardcoded POSIX literals like `/work/a`. On Windows `path.resolve('/work/a')` returns `D:\work\a` so the literal expectation drifted; the bridge's internal canonicalization to that form was correct, the tests were wrong. Fixes 3 Windows CI matrices that have been red since the PR opened. Compatibility: - `DaemonClient.fetchWithTimeout` now feature-detects `AbortSignal.timeout` and `AbortSignal.any` with polyfills, so the SDK actually works on its declared minimum runtime (Node >=18.0.0). `AbortSignal.any` was added in Node 20.3 — without the fallback every non-streaming call throws on Node 18.0–20.2. Documentation: - `cancelSession` now explicitly documents that cancel only affects the currently active prompt; previously POST'd queued prompts continue to execute. Multi-prompt queueing is a daemon-introduced behavior (not in ACP spec), so the contract for queued prompts is ours to define and was previously implicit. - Removed misleading "still reliable on Node 20" comment around `req.aborted` and switched the orphan-cleanup signal to `res.writable` — the right "can we still send a response to this client?" check (`req.destroyed` is too eager: clients close their writable end after sending the body even though they're still listening for the response). * fix(cli): close 3 more review threads — case-insensitive Host, trim token, sliceLineRange (#3803) - hostAllowlist now lowercases the Host header before comparison. Per RFC 7230 §5.4 Host is case-insensitive; Express normalizes header *names* but not values, so a Docker proxy that capitalizes the hostname (`Host: Localhost:4170`) or a platform with case-preserving DNS (`HOST.docker.internal`) was getting 403 with an exact-match compare. - `runQwenServe` now `.trim()`s the token from both `--token` and `QWEN_SERVER_TOKEN`. Common gotcha: `export QWEN_SERVER_TOKEN=$(cat token.txt)` keeps the file's trailing `\n`, so the hashed-then- compared token never matches what well-behaved clients send. Every request returns the generic 401, no breadcrumb pointing at the whitespace, operators chase ghosts. - `BridgeClient.readTextFile` partial-read path no longer `content.split('\n')`s the entire file. New `sliceLineRange` walks `indexOf('\n', …)` forward only to the end-of-range boundary and returns a single substring. For a 100 MB file with `{line: 1, limit: 2}` this avoids a ~100 MB `String[]` allocation. * fix(sdk): close 2 #3889 polyfill leaks — abortTimeout + composeAbortSignals Two copilot review threads on commit 11567a43c's AbortSignal polyfill code: - `abortTimeout` polyfill scheduled `setTimeout` but never cleared it. Even after the awaited fetch resolved, the pending timer kept the event loop alive until it fired; on a heavily-used client the per-call timers accumulated. Fix: `.unref()` the handle (so a fast-resolving fetch doesn't pin the loop) AND clear it on the controller's `abort` event (so the composed-signal-aborted-first path also drops the timer). Defensive `typeof handle.unref` so the polyfill works in any runtime that returns a non-NodeJS Timeout shape. - `composeAbortSignals` polyfill added an `abort` listener to every input signal but never removed them. Long-lived caller signals (e.g. a session-scope cancel signal that lives for the whole SDK client) accumulated one listener per SDK call — slow leak that retained the closure + controller of every prior call. Fix: track per-input cleanups in an array, detach all on the first abort (whichever input fires) AND on the composed controller's own abort path (defense-in-depth for callers that abort the composed signal independently). Both leaks only fire on the polyfill path — runtimes with native `AbortSignal.timeout` / `AbortSignal.any` (Node 20.3+) take the early-return path and bypass the leak surface entirely. 29/29 DaemonClient.test.ts pass; tsc + ESLint clean. * fix(cli,sdk): close 13 deepseek review threads — error handling + race + log noise (#3803) Correctness: - `applyModelServiceId` now races against `transportClosedReject` like `setSessionModel` and `sendPrompt` already do, so a child crash during attach-with-different-model fails fast instead of waiting the full 10s `withTimeout`. - `POST /session` disconnect guard now handles the `attached` case: previously `!res.writable && session.attached` fell through to `res.json` and threw EPIPE through Express's default handler. - `POST /session/:id/prompt` now drops `AbortError` silently. When the HTTP client closes mid-prompt the bridge re-throws as `AbortError`; routing it through `sendBridgeError` produced a noisy 500 + stderr stack trace that under active use generated dozens of misleading log lines per second. - `POST /session/:id/prompt` now rejects empty arrays (`[]`) and non-object elements with a 400 instead of letting the ACP SDK surface 500s on degenerate input. - `readTextFile` rejects `limit <= 0` up front (previously `sliceLineRange` hit the `end < start` path with surprising results). - `inFlightSpawns` tracks ALL `doSpawn` promises now, not just single-scope ones. Under `thread` scope, `shutdown()` previously resolved before in-flight spawns finished their child cleanup, surfacing stderr noise after the daemon claimed graceful shutdown. Use a unique `${workspaceKey}#${randomUUID()}` key per thread-scope spawn so simultaneous spawns don't collide. Shutdown ordering: - The 5s force timer is now armed AFTER `bridge.shutdown()` resolves, so it only races `server.close()` (the listener drain) — not the bridge's own 10s `KILL_HARD_DEADLINE_MS` child cleanup. The earlier arrangement could resolve this promise while the bridge was still killing children, orphaning anything not yet at the deadline. Express error handling: - Final 4-arg error middleware catches `express.json()`'s `SyntaxError` on malformed bodies and returns JSON `400` instead of Express's default HTML page (which trips SDK clients that expect a JSON body on every response). - SSE `res.on('error')` handler now logs the error before cleanup, so operators get a breadcrumb for flaky-network triage instead of silent disconnect. Performance: - `ALLOWED_CHILD_ENV_KEYS` moved to module scope so the 22-element Set is allocated once at load instead of rebuilt on every `defaultSpawnChannelFactory` call. (Renamed from `ALLOWED_ENV_KEYS` for clarity.) Documentation: - `canonicalizeWorkspace` now explicitly notes the cross-module contract with `config.ts`/`settings.ts`/`sandbox.ts`. A shared utility was considered but deferred — the call sites use slightly different fallback policies and Stage 2 in-process collapses the bridge into core, removing the bridge-side path resolution entirely. Tests: - Two new DaemonClient tests exercise `fetchWithTimeout`'s AbortSignal.timeout / composeAbortSignals polyfill paths against a never-resolving fetch promise. Previously every test used `recordingFetch` with synchronous resolution, so those polyfills shipped untested — a logic error there would only surface when a real daemon became unresponsive. * docs(serve): close §08 Stage 1 doc gap — user guide + protocol reference + DaemonClient example (#3803) Stage 1 of issue #3803 §08 budgeted "Documentation + examples + e2e tests" as the closing 1d task. The e2e tests landed (22 cases under integration-tests/cli/), the docs did not. After merge, anyone who discovers `qwen serve` via `qwen --help` had nowhere in-repo to read about it — the only complete description lived on the PR page itself. This commit fills that gap with three complementary docs and a README mention: - `docs/users/qwen-serve.md` — operator-facing quickstart: 5-step curl walkthrough (start → /health → /capabilities → /session → /prompt → /events), CLI flag table, default-deployment threat model summary, and a pointer to the orchestrator-shaped multi-session future. - `docs/developers/qwen-serve-protocol.md` — full HTTP protocol reference: per-route request/response shapes, auth contract, error envelope, SSE frame format and event-type table, Last-Event-ID reconnect semantics, environment variables, source layout. - `docs/developers/examples/daemon-client-quickstart.md` — TypeScript end-to-end snippet with the SDK's DaemonClient: capabilities probe, spawn-or-attach, subscribe-before-prompt event handling, reconnect via Last-Event-ID, first-responder permission voting, shared-session collaboration between two clients, auth, cancel. - README.md — "Daemon mode" added to the 5-way usage list + a short section under Usage with three doc links. - `docs/users/_meta.ts` and `docs/developers/_meta.ts` — sidebar entries for the new pages. No code changes; no test changes. * docs(serve): close 8 deepseek doc-review findings (#3803) Inline doc review on the Stage 1 doc set caught real issues: - `qwen-serve-protocol.md`: `session_died` (and `client_evicted`, `stream_error`) now explicitly marked as terminal — SSE stream closes after the frame; subscribers should reconnect via POST /session for `session_died`. - `qwen-serve-protocol.md`: documented coalesced spawn failure path — when the underlying spawn fails, all coalesced callers receive the same error and the in-flight slot is cleared so a follow-up call can retry. - `qwen-serve-protocol.md`: clarified the `modelServiceId` (back-end provider, picked at session create) vs `modelId` (model within an already-bound service, picked via POST /session/:id/model) distinction, and explained why `/capabilities`'s `modelServices` array is always `[]` in Stage 1. - `qwen-serve-protocol.md`: typo "Re-races" → "Races" on the model switch description. - `qwen-serve.md`: reordered quickstart so SSE subscribe (now step 4) comes before the prompt POST (now step 5). Previously, step 4's blocking prompt resolved before step 5's `curl -N` was open, so readers following the steps verbatim never saw a streaming event. Also expanded the event-types paragraph to call out which frames are terminal. - `daemon-client-quickstart.md`: closed a TOCTOU race in the example — `sendPrompt` fired before the SSE handshake completed, so fast-starting agents could emit events into the ring before the iterator was actually pulling. Pass `lastEventId: 0` so the daemon's replay buffer covers the gap; comment in the example explains the rationale. - README.md: "Loopback bind has no auth" → "no auth by default" (since the user can opt into bearer auth on loopback by setting `QWEN_SERVER_TOKEN`). * fix(cli,sdk,docs): close 21 review threads — env regression + races + doc accuracy (#3803) CRITICAL regression fix: - Child env scrub flipped from allowlist back to denylist (just QWEN_SERVER_TOKEN). The earlier allowlist was overzealous: it dropped OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / QWEN_* / DASHSCOPE_API_KEY / custom modelProviders[].envKey, all of which the agent legitimately needs to authenticate to the LLM. Daemon-mode users with env-only auth would start the daemon, attach a session, then watch every prompt fail with auth errors. Threat- model rationale documented at the call site: prompt-injected shell tools can already read ~/.bashrc, ~/.aws/credentials, etc., so env passthrough isn't the security boundary; the user-as-trust-root is. QWEN_SERVER_TOKEN stays scrubbed to prevent agent → its own daemon escalation. Other code fixes: - doSpawn no longer tears down the session when create-time model switch fails. The session is still operational on the agent's default model; tearing it down left the caller with a 500 and no sessionId to retry against. The model_switch_failed SSE event is the visible signal; caller can retry via POST /session/:id/model once they have the sessionId. - doSpawn now uses applyModelServiceId for the create-time model switch (was raw conn.unstable_setSessionModel + withTimeout). The helper races against transportClosedReject too, so a child crash during model switch fails fast instead of consuming the full init timeout. - sendPrompt's abort handler now calls cancelPendingForSession before the ACP cancel notification (matching cancelSession). A client disconnecting mid-permission was leaving the agent stuck waiting on a vote that no SSE subscriber would ever cast. - shutdown() and killSession() now publish a terminal `session_died` SSE event before closing the bus. Previously the channel.exited handler's "byId.get(...) !== entry" guard short-circuited (entry already removed), so SSE subscribers couldn't tell daemon shutdown from a transient network error. - Express error middleware now special-cases `status: 413` (EntityTooLargeError from body-parser when a request exceeds the 10 MB JSON limit) and returns a JSON 413 instead of a misleading 500. - /health is now registered BEFORE bearerAuth middleware, so liveness probes work without credentials when the daemon was started with --token. CORS deny + Host allowlist still apply. - SSE writes serialize through a per-connection chain so the heartbeat interval can no longer interleave with the main event- write loop. Two concurrent res.write calls would otherwise bypass the backpressure guard and could interleave bytes between SSE frames on the wire. SDK: - abortTimeout / composeAbortSignals exported for direct unit testing. The existing test claimed to cover the polyfill paths via subscribeEvents, but subscribeEvents calls _fetch directly (not fetchWithTimeout), so composeAbortSignals never ran in the test. New tests exercise the helpers directly across native + polyfill runtimes. Doc accuracy fixes: - daemon-client-quickstart.md: createOrAttachSession({ cwd: ... }) → ({ workspaceCwd: ... }) (SDK type), client.sendPrompt → prompt, client.cancelSession → cancel. The example wouldn't typecheck. - qwen-serve.md: "binds one workspace" claim removed — a single daemon hosts sessions for any cwd the caller passes; the per-instance constraint is per-user / scale, not per-workspace. Auth verification example switched from /health to /capabilities (since /health is now exempt from bearer auth). - qwen-serve-protocol.md: env var was QWEN_E2E_LLM, real var is SKIP_LLM_TESTS (inverted polarity). Streaming test count was 4, actually 3. Added Stage 1 limitation notes for "no DELETE /session" and "no permission timeout". Added client-side ring-buffer gap detection guidance for Last-Event-ID reconnect. Test updates: - httpAcpBridge.test.ts: rewrote two tests for the new doSpawn-on-model-switch-fail contract (publish event, keep session). Updated shutdown-closes-subscriptions test to expect the new terminal `session_died` frame. - server.test.ts: switched bearer-auth rejection probes from /health to /capabilities (since /health is now exempt). Added a test that locks /health's exemption. * docs(serve): close 2 last review threads — prompt timeout limitation note (#3803) A05Yk (deepseek): document that `POST /session/:id/prompt` has no server-side timeout. The bridge only races against the agent child exiting + the caller's HTTP-disconnect AbortSignal; a wedged-but-alive agent blocks the per-session FIFO. Long-running prompts are legitimate (deep research / large-codebase analysis) so a default deadline is deliberately not set; Stage 2 will expose a configurable opt-in. Callers should set their own client-side timeout and disconnect / POST /session/:id/cancel on expiry. AyoUy (copilot): same env-allowlist concern as A09HB — already addressed by the allowlist→denylist revert in the previous commit (e74aa9919). No additional code change needed; the resolve here just acks that the upstream fix covers it. * fix(serve): close 3 copilot review threads — SSE envelope shape + integration test ordering (#3803) A8uSe / A8uSt — the SSE frame examples in qwen-serve.md and qwen-serve-protocol.md showed `data:` containing only the inner ACP payload (e.g. `{"sessionUpdate": ...}`). The daemon actually emits the full event envelope — `{id?, v, type, data, originatorClientId?}` — JSON-stringified on a single line. Readers copying the curl output and writing parsers against the documented shape would extract garbage or fail JSON-shape validation. Both docs now show the real envelope and call out the SSE-level `id:` / `event:` lines as EventSource convenience that duplicates fields already inside the JSON envelope. A8uSz — integration `qwen serve — bearer auth` tests probed `/health` for 401 assertions, but `/health` is now intentionally registered BEFORE the bearer middleware (per the A8dZT fix in the previous commit) so liveness probes work without credentials. Switched probes to `/capabilities`, plus added a `/health exempt` test that locks the exemption so a future middleware ordering change can't silently break liveness probes. Also: integration `bad modelServiceId tears down half-init session` asserted the OLD doSpawn-on-model-switch-fail behavior (throw + clear maps). Per #3889 review A05Ym the new behavior keeps the session operational on the agent's default model and surfaces the failure via the `model_switch_failed` SSE event. Test renamed to `bad modelServiceId keeps the session alive on the default model` and rewritten to assert the new contract. * fix(serve): close 3 copilot review threads — sync write throw, polyfill name, blockquote (#3803) A800o (server.ts:360): `res.write(chunk, cb)` callback isn't documented to receive an error argument in Node — errors come on the `'error'` event, which the surrounding code already wires up. The dead `(err) => if (err) reject(err)` branch was misleading. The real concern was that `res.write()` can throw synchronously when the socket is already destroyed (typical EPIPE shape), and the throw escaped the promise executor. Wrapped the `res.write` call in try/catch so that surfaces as a rejection on the returned promise instead of an unhandled exception. A8008 (DaemonClient.ts:375): `abortTimeout` polyfill called `new DOMException('TimeoutError')`, which sets the *message* to "TimeoutError" and leaves `name` at its default ("Error"). Native `AbortSignal.timeout()` aborts with `name === 'TimeoutError'` (per WHATWG), so callers doing `if (err.name === 'TimeoutError')` to distinguish timeout from user-abort would see the polyfill behave differently from the native runtime. Constructor signature is `new DOMException(message, name)` — fixed both args. A801J (qwen-serve-protocol.md:254): blockquote was broken — one line in the middle of the multi-line `>` block was missing the `>` prefix, which dropped the rest of the list out of the quote and rendered awkwardly. Added the missing `>`. * fix(cli,sdk): close 8 review threads — DoS cap + SDK plumbing + cleanup (#3803) Critical: - A9UEi — `EventBus` had no subscriber cap and evicted subscribers lingered in the `subs` Set until the consumer drove `next()`. An attacker opening thousands of SSE connections to one session would amplify each `publish()` (O(N) over subs) into a CPU/memory DoS, with each evicted-but-stalled connection's `BoundedAsyncQueue` pinned in memory forever. Two fixes: per-bus subscriber cap of 64 (refuses new subs at the limit by returning an empty iterable), AND `subs.delete(sub)` immediately when a subscriber is evicted so subsequent publishes don't pay the dead-sub iteration cost. Also set `server.maxConnections = 256` on the listener to bound socket descriptors against connections that never finish their headers. SDK: - A9UEv — `prompt()` now accepts an optional `AbortSignal`. Caller cancellation forwards through the underlying TCP close, which the daemon already translates into an ACP `cancel` notification. The bridge's `sendPrompt(sessionId, req, signal)` always supported it; only the SDK surface was missing the parameter. - A9UEn — `subscribeEvents` now applies `fetchTimeoutMs` to the CONNECT phase only (request → headers received). The SSE body itself stays uncapped (it's long-lived by design), but a daemon that's TCP-open but never returns headers no longer blocks callers indefinitely. Implementation: a setTimeout-driven AbortController composed with the caller's signal, cleared in `finally` once `_fetch` returns. - A9UEr — `respondToPermission` now drains the response body via `res.body?.cancel()` on both 200 and 404. undici keeps the underlying socket pinned waiting for an unconsumed body; long- running clients with frequent permission votes would exhaust the connection pool. Cleanup: - A9UNF — `MAX_BUF_BYTES` renamed to `MAX_BUF_CHARS` (the guard checks `buf.length`, which is UTF-16 code units, not bytes). The cap's job is "stop runaway non-SSE bodies", not exact accounting, so the proxy is intentional — but the name now matches the unit. Error message updated. - A9UNb / A9UNp — both integration tests' boot-timeout `setTimeout` is now stored and `clearTimeout`'d on success and on early exit. Without the clear the un-cancelled 10s timer outlived the spawn promise and could keep the vitest event loop alive past the test, manifesting as intermittent timeouts on slow CI. A9UEy was already addressed by the prior commit's `status === 413` branch in the Express error middleware (body-parser sets both `status: 413` and `type: 'entity.too.large'` on body-too-large errors); resolve only. * fix(cli,test): close 2 copilot review threads — case-insensitive bearer + Windows skip (#3803) A9sCe (auth.ts:88): bearer scheme parsing was case-sensitive (`parts[0] !== 'Bearer'`). Per RFC 7235 §2.1 / RFC 7230 §3.2.6 the auth scheme token is case-insensitive — `Bearer` / `bearer` / `BEARER` are all valid, and conformant clients may send any. The old code returned 401 on those. Switched to a regex-based split that also tolerates runs of whitespace between scheme and credentials, then `.toLowerCase()`s the scheme before comparing. The token value itself stays case-sensitive (it's user-defined opaque material). A9sCw (qwen-serve-streaming.test.ts): the streaming integration suite shells out to `pgrep` / `kill -KILL` to simulate child-process crashes for the `SIGKILL → session_died` test. Those binaries are POSIX-only — on Windows runners the suite would fail even when `SKIP_LLM_TESTS` is unset. Added `process.platform === 'win32'` to the SKIP gate. A Windows-equivalent (`taskkill /F /PID …`) needs different scaffolding; deferred. * fix(cli,sdk,docs): close 6 review threads — CodeQL regex, body cancel, env doc (#3803) A90nk (auth.ts:93): CodeQL flagged the new bearer-scheme regex `^(\S+)\s+(.+)$` as a polynomial-regex risk on user-controlled input — `\s+` and `.+` overlap on whitespace-heavy adversarial headers (the alert example: `'!\t' + '\t'.repeat(N)`). Replaced with a hand-rolled split (`indexOf(' ')` + manual whitespace skip) so there's no backtracking. Behavior unchanged: scheme is still case-insensitive, runs of whitespace between scheme and credentials still tolerated, scrubs `header.charCodeAt() === 0x20` explicitly so we don't accidentally consume tab/newline as scheme separator. A90oi / A96Q8 (qwen-serve.md:117): the threat-model bullet still claimed the spawned child runs with an "allowlisted environment" (HOME / PATH / USER / LOGNAME / LANG / etc), but the prior commit flipped the implementation to a denylist (only `QWEN_SERVER_TOKEN` scrubbed) so the agent could authenticate to LLM providers. Doc now matches code: explicit pass-through with a one-key scrub, plus the threat-model rationale (user-as-trust-root, env passthrough is not the boundary). A90ou (qwen-serve-protocol.md:300): `stream_error` example showed the inner ACP-style payload `{"error":"<message>"}` instead of the full envelope `{v, type, data:{error}}` that other SSE-frame examples in the same doc already use. Updated to match. A96RL (DaemonClient.ts:352): `subscribeEvents` threw on a 200 with the wrong content-type without consuming the response body first. On undici-backed `fetch` an unconsumed body keeps the underlying socket pinned waiting for the consumer; long-running clients hitting this path repeatedly would exhaust the connection pool. Same `await res.body?.cancel()` pattern as `respondToPermission`. A96RR (server.ts:167): prompt-element validation accepted any non-null object, but `typeof [] === 'object'`, so `prompt: [[]]` slipped past with a confusing 500 from the ACP SDK layer downstream. Added `!Array.isArray(item)` so the 400 actually catches array elements. * fix(cli,sdk,docs): close 10 review threads — DoS observability + race + tests (#3803) Code: - A-Ur8 (httpAcpBridge.ts:1319): SCRUBBED_CHILD_ENV_KEYS gets a prominent WARNING that the denylist-only design is correct ONLY because the agent has unrestricted shell-tool access. Any future sandbox-locked variant MUST switch back to allowlist or expand the denylist to cover provider/CI/cloud secret prefixes. - A-XfH (auth.ts:60): Host allowlist now accepts the no-port form (`localhost`, `127.0.0.1`, `[::1]`, `host.docker.internal`) when the bind port is 80. Per RFC 7230 §5.4 clients may legitimately omit the port suffix when it matches the URI scheme default. - A-UsJ (httpAcpBridge.ts:564): unify model-switch failure handling. The create-session path swallows the error to keep the session alive on its default model; the attach path now does the same (was: throwing a 500 with no sessionId, denying the caller any way to recover). Both paths surface failure via the `model_switch_failed` SSE event. - A-UsN (httpAcpBridge.ts:621): extracted the lazy-init `transportClosedReject` pattern into `getTransportClosedReject` helper. Three call sites (`applyModelServiceId`, `sendPrompt`, `setSessionModel`) collapsed to one, single-listener invariant documented at one place. - A-UsH (eventBus.ts:194): subscriber-cap rejection is now observable. EventBus.subscribe throws a typed `SubscriberLimitExceededError` (was: silent empty iterable). SSE route catches it, logs to stderr, and emits an SSE-shaped `stream_error` terminal frame so the rejected client sees a readable failure rather than a closed-with-no-frames stream. - A-UsO (server.ts:72): `/health` is now exempted from bearerAuth ONLY on loopback binds. On non-loopback the route is registered AFTER bearerAuth so probes must carry the token — otherwise an unauthenticated caller could probe arbitrary IP:port to confirm a `qwen serve` exists. Doc updated. Tests added: - A-UsP: new test sends an 11 MB body to verify the 413 path in the Express error middleware returns the actionable "Request body too large" JSON instead of a generic 500. - A-UsQ: new test for `DaemonClient.prompt(sessionId, req, signal)` AbortSignal forwarding through to fetch. - A-UsS: two new tests for `subscribeEvents` connect-timeout (never-resolving fetch aborts; fast-resolving fetch clears the timer so it doesn't leak as a dangling handle). - A-UsU: new test for `sendPrompt` abort path resolving pending permissions as cancelled — the bug being regressed: an HTTP client disconnecting mid-permission would leave the agent stuck waiting on a vote that no SSE subscriber would ever cast. Test contract updates: - `publishes model_switch_failed and surfaces the error when the agent rejects` rewritten for the new attach-path swallow contract: attach now returns the existing session with `attached: true` and the `model_switch_failed` event is the visible failure signal instead of a thrown error. * fix(serve): add missing v field on subscriber-limit stream_error frame (#3803) `tsc --build` (which CI runs as part of the lint job) caught what `tsc --noEmit` (the local typecheck script) missed: the new `stream_error` frame in `server.ts:344` was constructed without the `v` field, but `OmitId<BridgeEvent>` requires it. Local typecheck in the previous commit was clean; the build's stricter project graph reported `error TS2345` and broke both Lint and Test (Ubuntu) jobs. Set `v: 1` to match the existing `stream_error` construction in the SSE iterator-throw path in the same file. * docs(users): close 1 copilot review thread — GitHub canonical casing in nav (#3803) A_U2e: nav label "Github Actions" was inconsistent with the canonical "GitHub" casing used elsewhere in the repo (skills, README, etc.). Rename to "GitHub Actions" for consistent branding. Pre-existing entry in `docs/users/_meta.ts` adjacent to the `'qwen-serve'` line this PR added — flagged in the diff context. * fix(serve): close 4 deepseek review threads — closed-bus race + per-session stderr + entry override (#3803) BBb9H (correctness): `BridgeClient.requestPermission` could orphan a pending permission if the bus closed between `registerPending` and `entry.events.publish` (the shutdown path closes per-session buses BEFORE awaiting `channel.kill()`, so the agent can still issue `requestPermission` in that window). Pending was registered in the daemon-wide map but `publish()` returned `undefined` (closed bus) → no SSE subscriber ever saw the request → no client voted → agent's `requestPermission` hung forever, blocking the daemon's `Promise.all` over child kills. Now: check publish's return; if `undefined`, roll back the pending via a new `rollbackPending` callback that resolves it as `cancelled`. BBb8e (Critical observability): child stderr was `'inherit'` — all sessions' stderr interleaved on the daemon's stderr stream unattributed. Switched to `'pipe'` and forward each line with a `[serve pid=<n> cwd=<dir>]` prefix; operators can now `grep pid=12345` to pull one session's trace cleanly. Updated the now-stale doc comment that claimed inherit was current. BBb8- (deployability): `process.argv[1]` is brittle — fails on non-`qwen` launchers (bundled binaries, npx wrappers, `node -e`, `tsx`, container images that relocate the script). Added `QWEN_CLI_ENTRY` env override as the higher-priority resolution path. Improved the failure message to suggest the env var as the actionable fix. BBb82 (documented limitation): `withTimeout` REJECTS but doesn't ABORT the underlying ACP op. For `unstable_setSessionModel` this means a timed-out caller perceives failure while the agent may eventually complete the switch — drift between caller's perceived model and agent's actual model + contradictory SSE events. Documented as a Stage 1 limitation in the `withTimeout` JSDoc; acceptable because (1) ACP doesn't expose a cancel signal for `unstable_setSessionModel` yet so we couldn't abort even if we wanted to, (2) model switches complete in milliseconds in practice — a timeout means genuinely wedged, not just slow. Stage 2 will add abort plumbing once ACP exposes the hook. * ci(noop): re-trigger workflow for f8509dde5 (#3803) * fix(cli,sdk): close 8 review threads — sse abort + queue drain mode + perf + doc engine drift (#3803) Correctness: - BCcd6 (sse.ts:80): trailing flush at EOF used `splitFrames(buf)` which returned `[buf]` — a multi-byte split that completed multiple frame separators in the final `decoder.decode()` would merge the frames into one parse and silently drop events. Switched the EOF flush to `consumeFrames()` (same walker the main loop uses), then attempt one more `parseFrame` on any trailing fragment. Removed the now-unused `splitFrames` helper. - BCybH (sse.ts:67): `parseSseStream` only checked `signal.aborted` before each `reader.read()`, leaving the generator parked inside a pending `read()` if the upstream went idle right when the caller aborted — contradicting the docstring's "AbortSignal cleanup is prompt" claim. Added a one-shot abort listener that calls `reader.cancel()` (cleared in `finally`), so abort reliably terminates even on a stalled stream. - BCce_ / BCycT (eventBus.ts:391/253): subscribe documented "abort closes the iterator promptly" but `BoundedAsyncQueue.next()` drained any items already in `buf` before honoring `closed`. Aborted SSE subscribers could keep yielding hundreds of queued events to a closed socket. Added a `close({drain: false})` mode that truncates `buf` immediately, used by the abort path; the default drain-on-close behavior is preserved for the eviction path (which needs the synthetic `client_evicted` terminal frame to reach the consumer before the iterator unwinds). Performance: - BCcfe (auth.ts:72): `hostAllowlist` was allocating a fresh `Set` + 4 interpolated strings on every request. Cache once per resolved port (relevant because tests bind to ephemeral 0 and the port is only known after `listen()`); SSE heartbeats and high-frequency probes now skip the allocation. - BCcgJ (DaemonClient.ts:137): `fetchWithTimeout` used `AbortSignal.timeout()` — the timer fires regardless of whether the fetch resolved early. On a fast-resolving request with the default 30s timeout, the pending timer hangs around. Switched to `AbortController` + `setTimeout` + explicit `clearTimeout` in `finally`, so each timer is released the moment its fetch settles. Also `.unref()`s the timer so it doesn't pin the event loop on its own. Doc accuracy: - BCyc0 (DaemonClient.ts:468): the `abortTimeout` / `composeAbortSignals` JSDoc claimed Node 18-20.2 polyfill compatibility, but `engines.node` is `>=22.0.0` now. Reframed as a generic feature-detect for non-Node runtimes (browsers / edge workers) so future maintainers don't reason about the wrong floor. - BCydi (server.ts:368): "Always present in Node >= 20" → "on the supported Node versions (engines.node >=22)". CodeQL alert #207 (httpAcpBridge.ts:1342, `js/path-injection` on `cwd: workspaceCwd`) is the renumbered version of the already-accepted #201 — same trust-model rationale documented at the call site, same need for maintainer UI dismiss / config exclusion. * feat(serve): close 3 chiga0 audit items — ringSize 4000, --max-sessions, /health?deep=1 (#3803) Three "30-minute" items from chiga0's external architecture audit (2026-05-11). All actionable within Stage 1 scope; remaining items in chiga0's review (SaaS positioning, multi-token to Stage 1.5, acp-bridge package extraction, reference orchestrator) are larger scoping decisions deferred to Stage 1.5/2. DEFAULT_RING_SIZE 1000 → 4000 (Risk 4): - A single long turn can emit hundreds of frames (test plan reports 13 for a SHORT turn, real workloads can be 10× that). 1000 was exhausted by a moderate turn before a 5s reconnect window finished. 4000 gives ~30× headroom over a typical busy turn at the cost of a few hundred KB RAM/session. Updated user + protocol docs and the daemon-client-quickstart example. --max-sessions <n> (default 20) (Rec 3): - New `ServeOptions.maxSessions` + matching `BridgeOptions`. Bridge throws `SessionLimitExceededError` when `byId.size + inFlightSpawns.size >= max` BEFORE issuing a fresh spawn. Attaches to existing sessions (single scope) bypass the cap so an idle daemon's reconnects keep working at-capacity. `0` disables. Default of 20 sized below the design's N≈50 cliff (per-session ~30–50 MB RSS + FD pressure). HTTP route maps to 503 with `Retry-After: 5` and `code: session_limit_exceeded`. Tests cover: cap rejection under thread scope, attach-not-counted under single scope, `0` disables. Documented in CLI flags table + protocol Common-error section. /health?deep=1 (Risk 3): - Default `/health` stays cheap (no bridge access). With `?deep=1` the response includes `sessions` and `pendingPermissions` from the bridge — touches state so a wedged bridge surfaces as 503 `{status: "degraded"}` instead of "200 ok" on a zombie daemon (the `k8s rolling deploy will see healthy` failure mode chiga0 flagged). Loopback-vs-non-loopback bearer-exempt logic from the earlier A8dZT fix is preserved via a shared handler. Tests cover: cheap default, deep response shape, throwing-getter → 503. * fix(serve,sdk,docs): close 9 review threads — req.on('close') prompt-cancel bug + doc + types (#3803) Critical correctness: - BQAnZ (server.ts:225): `POST /session/:id/prompt` wired cancellation to `req.on('close')` — but Node's `IncomingMessage` fires that event when the request body has been fully consumed, even when the client is still listening for the response. Result: ordinary prompt calls were getting cancelled the moment their upload finished, returning `{stopReason: "cancelled"}` instead of completing. Switched to `res.on('close')` guarded by `!res.writableEnded` (the documented "client gave up before we could send the response" pattern, same as the POST /session disconnect-detection from earlier in the PR). Already addressed earlier — resolve as ack: - BQAna (httpAcpBridge.ts:767): no global session cap. Already shipped in commit 66ffd7cc6 — `--max-sessions` flag + bridge enforces with `SessionLimitExceededError` mapped to 503; both in-flight spawns and live sessions count against the cap. Doc fixes: - BDAOf (DaemonClient.ts:49): `fetchTimeoutMs` JSDoc said it applies to "every non-streaming method including prompt", but `prompt()` actually bypasses fetchWithTimeout (model+tool turns are minutes-scale, can't be 30s-capped). Doc now lists the short-lived methods explicitly and notes prompt's exemption. - BDAPY (qwen-serve-protocol.md:283): blockquote was broken — the `POST /session/:id/cancel` line was missing the leading `>` and a stray "- POST /session/:id/cancel." rendered orphaned outside the quote. Reformatted as a single coherent quote. Reviewer-tooling resilience: - BQAnf / BQAng (integration-tests/...:325/185): added explicit `DaemonSessionSummary` type to two `.find` / `.every` callbacks. Local typecheck infers the type fine via the SDK's source declarations; the reviewer's environment resolves `@qwen-code/sdk` against a possibly-stale `dist/index.d.ts` (per `integration-tests/tsconfig.json` `paths` mapping) and the `s` parameter widens to `any`. Annotation makes both envs happy. Reviewer-only artifacts (no code action): - BQAnb / BQAnc (integration-tests/...:26/30) — same SDK-dist staleness; the imports are correct and resolve fine when `packages/sdk-typescript` has been built. - BQAni (server.test.ts:8 supertest module not found) — Node 20 setup blocker the reviewer noted; resolves cleanly under Node >=22 (our declared engines floor) with `npm install`. * fix(serve,sdk,test): close 7 review threads — fetchTimeoutMs negative + bridge-error context + perm scope contract (#3803) Real fixes: - BQPRo (DaemonClient.ts:136): `fetchTimeoutMs` accepted any number, including negatives that would slip past the `Number.isFinite` check inside `fetchWithTimeout` and fire `setTimeout(-1)` → immediate abort, killing every request before it could complete. Coerce non-positive / non-finite to 0 (the documented disable sentinel) at the constructor so call-site math stays simple. - BQLdO (server.ts:725): `sendBridgeError` now accepts a `ctx` arg `{ route, sessionId }` folded into the stderr log line. Bare `ECONNRESET` / `ENOMEM` traces are no longer unattributable on a busy daemon — operators see `qwen serve: bridge error (POST /session/:id/prompt session=abc-123): ...`. All five route call sites pass context. - BQI-6 (qwen-serve-streaming.test.ts:123): `sseFrames` test helper forwards `opts.signal` into `parseSseStream` so post-connect abort terminates iteration immediately (the parser's own abort- -wired-to-reader.cancel landed earlier; this just plumbs through the test harness). Doc / contract: - BQNqL / BQNqM (httpAcpBridge.ts:692, server.ts:199): `cancelPendingForSession` cancelling all session permissions on client disconnect is intentional under the per-session FIFO + ACP spec — permissions are issued inline DURING an active prompt, the agent awaits them, so the only outstanding permissions at any moment belong to the prompt being cancelled. Cross-client caveat (B's vote 404s when A disconnects mid-A's-prompt) is the right behavior — a vote on a cancelled-prompt's permission wouldn't drive the agent forward. Documented the scope contract + multi-client caveat in `cancelPendingForSession` JSDoc. Already addressed (resolve as ack): - BQI-c (qwen-serve-protocol.md): blockquote was already reformatted in the previous round (`POST /session/:id/cancel` now sits inline on a single quoted line); copilot reviewed an older commit. - BQI-v (DaemonClient.ts): `fetchTimeoutMs` JSDoc was already updated last round to explicitly note `prompt()` is excluded; copilot reviewed the older shape. * fix(serve,test,docs): close 6 review threads — TEST_CLI_PATH + Stage 2 markers + SSE phantom-conn warning (#3803) Real fix: - BQpu6 / BQpvW (integration-tests/cli/...): both qwen-serve test files hardcoded `../../packages/cli/dist/index.js`, while the rest of the integration suite reads `process.env.TEST_CLI_PATH` (set by `globalSetup.ts` to the root `dist/cli.js` bundle). The difference made our tests sensitive to which build step (`build` vs `bundle`) ran last. Now read `TEST_CLI_PATH` first, fall back to per-package dist for direct vitest invocations that bypass globalSetup. Operator-facing doc: - BQsOD (server.ts:497 KNOWN GAP): added an operator warning to `docs/users/qwen-serve.md`'s threat-model section about phantom SSE connections behind NATs that swallow TCP RSTs (kernel keepalive ~2h Linux default → can accumulate to the 256-conn ceiling on `--hostname 0.0.0.0` deployments). Stage 2 will add application-level idle deadline; until then operators on such networks may want to lower `server.keepAliveTimeout` via reverse proxy. Stage 2 maintenance markers (no code change, just visible TODOs): - BQsOA (httpAcpBridge.ts:1247): added `FIXME(stage-2)` on the sync `realpathSync.native` call so the Stage 2 in-process refactor doesn't ship without removing this event-loop-blocking syscall. - BQsOB (server.ts:243): added a SECURITY NOTE on the `...(body as object)` passthrough explaining the spec-defined `_meta` forwarding contract + the rule that an explicit pick is required if any new bridge field starts being trusted by name. Pattern repeats on cancel/model — note covers all four sites. - BQsOF (httpAcpBridge.ts:1041): `FIXME(stage-2)` noting that `setSessionModel` reuses `initTimeoutMs` (default 10s) for the in-flight model swap — conceptually distinct from cold-start init, currently sharing only by coincidence; Stage 2 should split into `modelSwitchTimeoutMs` and remove the no-abort `withTimeout` race-condition once ACP exposes a cancel signal for `unstable_setSessionModel`. * fix(serve): close 4 review threads — unhandled rejection + maxSessions plumbing + 2 docs - httpAcpBridge.sendPrompt: attach .catch(() => {}) to the abort-listener cleanup chain. The chain is `racedPromise.finally (...)` and we never await it; if `racedPromise` rejects, the finally returns a rejected promise that surfaces as an unhandled rejection (Node's default behavior on unhandled rejection is process termination). The route's own catch handles the original rejection — only the cleanup chain needs the swallow. - httpAcpBridge.sendPrompt: FIXME(stage-2) for absolute prompt deadline — buggy agent ignoring cancel + alive channel = slow prompt-promise leak. - server.createServeApp: forward opts.maxSessions when constructing the default bridge. Direct callers (tests, embeds) were silently falling back to DEFAULT_MAX_SESSIONS (20); only the runQwenServe path piped the option through. - docs/users/qwen-serve.md: clarify Host allowlist is loopback-only; non-loopback binds rely on bearer + operator-managed front proxy. * docs(sdk): close 1 review thread — sse.ts MAX_BUF_CHARS docstring lead-line said "bytes" Doc lead-line claimed "Hard cap on accumulated unread bytes" while the implementation enforces the cap via `buf.length` (UTF-16 code units), which the rest of the same docstring already correctly explained. Fix the lead-line so a reader skimming the first sentence isn't misled. The runtime error message and constant name (MAX_BUF_CHARS) already say "code units" — only the docstring lead-line needed alignment. * fix(serve,sdk): close 5 review threads — disconnect/attach race + 3 spec fixes + 1 doc - httpAcpBridge: add SessionEntry.attachCount + new killSession({requireZeroAttaches:true}) opt to fix the BQ9tV race. When client A spawned (attached:false) but disconnected mid-spawn, A's disconnect-reaper (server.ts) could tear down a session that client B had just attached to. spawnOrAttach now bumps attachCount on each attached:true return, and killSession with the new opt bails when attachCount > 0. The check + the eager byId/byWorkspace deletes both run in killSession's synchronous prefix, so the guard is atomic across the await boundary. - server.ts disconnect-reap path now passes requireZeroAttaches:true. - loopbackBinds.ts: lowercase the operator-supplied hostname before Set lookup so --hostname Localhost / LOCALHOST aren't forced to require a token. Aligns boot-time detection with the runtime Host-header check (auth.ts already lowercases). - auth.ts bearer parsing: accept HTAB (0x09) in addition to SP between scheme and credentials per RFC 7230 §3.2.6 BWS. - sdk sse.ts parseFrame: guard against `null` / primitive JSON parses so the AsyncGenerator<DaemonEvent> contract isn't violated by a misbehaving proxy emitting `data: null`. Daemon itself never emits these — defense-in-depth only. - docs/developers/qwen-serve-protocol.md: document the modelServiceId-rejection-on-fresh-session corner case + tell subscribers to pass Last-Event-ID:0 to replay the spawn-time model_switch_failed event from the ring. - 3 new unit tests: BQ9tV positive + negative race paths, BQ9ze parseFrame null guard. * fix(serve): close 4 review threads — 2 critical (NaN cap, stderr buffer) + IPv6 zone-id + deep doc - httpAcpBridge maxSessions normalization (BRApy [Critical] gpt-5.5): NaN / negative values previously fell through `!Number.isFinite(...)` to `Infinity`, silently disabling the daemon's session cap (fail-OPEN on a typo). Now throw TypeError on NaN / negative; explicit 0 and Infinity remain valid "unlimited" sentinels. - httpAcpBridge stderr line buffer (BRAp3 [Critical] gpt-5.5): the per-spawn `buf` accumulating stderr until `\n` had no length cap; a child that wrote a huge line or never emitted a newline could grow daemon memory unboundedly per session. Cap at 64 KiB per line and force-flush with a `[truncated]` marker — keeps the prefix-attributed log line, bounds memory, no content drop. - runQwenServe.formatHostForUrl (BQ-6V copilot): RFC 6874 requires `%` in IPv6 zone IDs (e.g. `fe80::1%lo0`) to be percent-encoded as `%25` in URLs. Now encode on the raw-IPv6 path; already-bracketed input is the operator's responsibility. - /health?deep=1 (BQ-6F copilot): the 503 path is unreachable for the real bridge (counter getters are simple Map-size accessors that don't throw). Reframed in code + protocol doc as INFORMATIONAL observability ("capacity dashboards, not real liveness"); keep the try/catch as defense-in-depth for custom bridge impls. - 2 new unit tests: BRApy NaN/negative throws + 0/Infinity ok; BQ92B Localhost case-insensitive boot. * fix(sdk): close 1 review thread — sse parseFrame tighter shape guard (BREsR followup to BQ9ze) The previous parseFrame guard only rejected null/primitive JSON; arrays and shape-incomplete objects still cast through to DaemonEvent. Tighten to require: non-null non-array object with v === 1 and type: string. Now the generator's static AsyncGenerator<DaemonEvent> type is a genuine runtime guarantee instead of a structural hope. Daemon never emits malformed frames (formatSseFrame always serializes {v: 1, type: string, ...}); guard remains defense-in-depth against misbehaving proxies / alternate implementations. Existing test fixtures already conform to the shape so no other tests needed updating. * fix(sdk): close 1 review thread — fetchWithTimeout keeps timer alive through body consumption (BRN1o) Pre-fix: `fetchWithTimeout` cleared the timer in `finally` the moment the underlying `fetch` resolved. But `fetch` resolves at headers, not at body completion. A daemon or proxy that sent headers and then stalled mid-body left `await res.json()` (and `failOnError`'s `res.text()`) without any deadline — calls to `health()`, `capabilities()`, `createOrAttachSession()`, `listWorkspaceSessions()`, `setSessionModel()`, `cancel()`, `respondToPermission()` could hang indefinitely past `fetchTimeoutMs`. Refactor `fetchWithTimeout<T>` to take an optional `consume(res)` callback whose execution is included in the timer scope. The composed abort signal still flows through to fetch's body stream, so an in-progress `res.json()` rejects cleanly when the timer fires. All JSON-returning routes updated to pass the body-read code as the callback. SSE (subscribeEvents) + prompt are unchanged: they bypass fetchWithTimeout intentionally (long-lived). Regression test: response with a never-emitting body that errors via the composed AbortSignal — pre-fix would hang for 5s+, post-fix rejects within ~80ms (configured timeout). * fix(serve,sdk): close 8 review threads — coalescing race fix + --max-connections + 5 docs/cleanups - httpAcpBridge spawnOrAttach (BRSCi [Critical] DeepSeek): the BQ9tV attachCount fix was incomplete for the in-flight coalescing path. When two callers await the same doSpawn and the second has a modelServiceId, the attach-bump landed AFTER an extra await for applyModelServiceId — leaving a microtask window in which A's killSession sync-prefix would still see attachCount==0 and reap a session B was about to receive. Move the bump to the very first sync step after `await inFlight` (and same in the direct-attach branch) so the bump-before-killSession ordering holds even when the model-switch yields. Test added for the coalescing-race path. - commands/serve + serve/types + runQwenServe (BRQQb): add `--max-connections` flag (default 256), wired through ServeOptions and `server.maxConnections`. Operators with high-concurrency deployments can now tune the listener-level cap without waiting for Stage 2. - commands/serve (BRQQZ): wrap `new Promise<never>(() => {})` in a named `blockForever()` helper so a future maintainer doesn't read the bare expression as a never-resolving-promise bug. - auth.ts (BRQQd): rewrite the comment about HTAB BWS — clarify that the scheme→credentials separator is `1*SP` per RFC 9110 §11.6.2, and HTAB is only accepted in the BWS *after* the SP. `Bearer\t<token>` (pure HTAB) is intentionally rejected. - types.ts + qwen-serve-protocol.md (BRQQf): document `modelServices: []` is always empty in Stage 1 so SDK consumers don't build off it. - qwen-serve.md (BRQQl + BRQQm): add operator note about subscribing to /events BEFORE posting modelServiceId on attach (otherwise the model_switch_failed event is missed). Document the four-layer load cap stack near --max-sessions so operators can size the related knobs together. - sdk index (BRSCv): drop the historical `Daemon`-prefixed type aliases (`DaemonPromptRequest` / `DaemonSubscribeOptions`) for consistency with the other un-prefixed daemon-type exports. SDK is Stage-1-experimental with no shipping consumers. * fix(sdk): close 1 review thread — sse parseFrame must not drop frames whose first line is a comment/retry (BRgq-) Per the EventSource spec, comment lines (`:` prefix) and `retry:` are line-level fields, not frame-level. The previous early return at the top of `parseFrame` dropped the entire frame when its first line was a comment or retry directive — meaning an intermediary that prepends `: keep-alive` or `retry: 5000` to every frame would cause the embedded `data:` payload to be silently lost. Removed the `startsWith` guard. The line-level `data:` collection loop already produces an empty `dataLines` array for pure-comment / pure-retry frames, so the existing `if (dataLines.length === 0) return undefined` branch still skips them — without dropping real events that just happen to be preceded by a comment line. Existing test still pins the standalone-comment / standalone-retry behavior; new test pins the leading-comment + data-line case. * docs(sdk): close 1 review thread — sse MAX_BUF_CHARS comment was overpromising byte-equivalence (BRker) The previous wording suggested "one code unit ≈ one byte" for mostly-ASCII content, then qualified it with mixed BMP / supplementary caveats. Reviewer flagged that JS string.length isn't a reliable byte proxy in either direction — engine string representation (V8 Latin-1 path vs UTF-16) makes the actual memory cost vary in ways the comment didn't capture cleanly. Rewrote to state plainly: cap measures code units, not bytes; intent is "stop runaway non-SSE bodies", not exact memory accounting; byte-precise bounds belong at a front proxy. Threshold and code unchanged — only the comment. * fix(serve): close 7 review threads — atomic write, read-size cap, force-exit on 2nd signal, doc fixes - httpAcpBridge.writeTextFile (BSA0D): atomic write-then-rename via `<path>.<pid>.<ts>.tmp` + `fs.rename`. Closes the SIGKILL-mid-write truncation hole. Tmp file lives in the target's directory so the rename can't cross filesystem boundaries; cleaned up on rename failure. - httpAcpBridge.readTextFile (BSA0E): `fs.stat` pre-check rejects files past 100 MiB so a `{ line: 1, limit: 10 }` against a 500 MB log doesn't allocate 500 MB of RSS just to return 10 lines. - runQwenServe SIGINT/SIGTERM (BSA0K): second signal during drain forces `process.exit(1)` with a stderr message instead of silently no-oping. Standard daemon behavior — `^C^C` works. - commands/serve --hostname help text (BRqFe): now mentions the full loopback set (127.0.0.1, localhost, ::1, [::1]) so IPv6 users aren't misled into thinking ::1 needs a token. - runQwenServe boot-refusal error (BRqFy): same correction — error message now lists all loopback aliases the operator can rebind to. - httpAcpBridge withTimeout doc (BSA0C): explicit Stage 2 follow-up marker for the modelSwitchTimedOut / model_switch_late_success observability gap (already a known limitation). - server.errorPayload (BSA0G): documented the multi-tenant info-leak trade-off (Stage 1 single-user/small-team trust model accepts verbatim ACP error data) and pointed to a Stage 2 --redact-errors follow-up. - 2 new tests: writeTextFile leaves no tmp turd; readTextFile rejects 200 MiB sparse file via the size cap. * fix(sdk): close 1 review thread — sse parseFrame must validate optional `id` (BSP1-) The previous shape guard only validated `v === 1` and `type: string`, leaving `DaemonEvent.id: number | undefined` unchecked. A misbehaving proxy emitting `data: {"id":"1","v":1,"type":"x",...}` would survive the cast and break consumer resume logic — Last-Event-ID resume does numeric comparisons against the monotonic counter, and a string id silently corrupts that math. Reject the frame entirely when `id` is present but not a finite safe integer (`Number.isSafeInteger`). Negative integers and missing-id both still pass; the daemon never emits negative ids in practice but the guard's responsibility is the type-cast contract, not the daemon's id-allocation policy. New test covers: string id, float id, > MAX_SAFE_INTEGER id (all rejected); negative-id, no-id, plain integer (all pass). * docs(serve): Stage 1.5 markers from chiga0 follow-up architecture review (#3889 c4427773706) chiga0's follow-up review explicitly states "None of the findings here block Stage 1. That holds." All 6 findings are Stage 1.5 convergence work for when downstream consumers attach. None require code changes for this PR. Adding inline FIXME(stage-1.5) markers at the natural pivot points so the future refactor has clear breadcrumbs back to the audit comment, instead of Stage 1.5 implementers having to re-discover the convergence story: - types.ts STAGE1_FEATURES → finding 5 (capability registry + extMethod HTTP route). - eventBus.ts EventBus class → finding 2 (lift to packages/event-bus, multi-consumer subscribe). - httpAcpBridge.ts BridgeClient.requestPermission → finding 3 (PermissionMediator + policy plugin point; closes prior chiga0 Risk 2 too). - httpAcpBridge.ts BridgeOptions → findings 1 + 4 (split into AcpChannel + Transport packages; thread FileSystemService through BridgeOptions). No behavior change. Each marker links to the audit comment for traceability. * docs(serve): tighten Stage 1 scope framing + durability + Stage 1.5 must-haves (#3889 c4427875644) chiga0's third review walks three downstream-consumer scenarios (IM bot, mobile companion, IDE extension) against Stage 1's runtime guarantees. The bottom-line concern is framing: the PR body promises "real workloads" but the protocol surface is sized for demo / single-user / never-crashes. Reviewer offers two paths — tighten the framing or add 7 must-haves to Stage 1.5. Author classifies all 10 must-haves as Stage 1.5/2, none as Stage 1 changes. In-scope action for this PR (doc-only, no behavior change): - `docs/users/qwen-serve.md` "Status" block: explicit scope-honesty note — Stage 1 is sized for prototyping clients + local single-user/small-team. Production-grade multi-client / mobile / flaky-network workloads need Stage 1.5+ guarantees. - New "Durability model" section spelling out sessions-are-ephemeral (closes must-have 10): no resume on child crash / daemon restart, ring-overflow on long disconnects, writeTextFile atomic across crash but not across restart. - New "Stage 1.5+ runtime guarantees" section listing the 10 must-haves (blockers 1-3, reliability 4-7, ergonomics 8-10) with a link back to the audit comment for traceability. - `httpAcpBridge.ts` BridgeOptions.sessionScope: FIXME(stage-1.5) marker referencing must-have 1 (per-request override), since this is the most prominent client-facing lock-in risk. No code behavior changes — this is roadmap commentary surfaced into the artifacts where downstream integrators will look (user docs + code pivot points). * fix(serve): close 2 correctness findings from tanzhenxin review Two bugs surfaced in the CHANGES_REQUESTED review: Issue 1 — `--max-connections 0` silently bricks the daemon on Node 22: - Docs say "Set to 0 to disable" and the code did `server.maxConnections = opts.maxConnections ?? 256`, but on Node 22.15.0 setting `server.maxConnections = 0` makes the listener refuse EVERY connection (every fetch → SocketError other side closed). The operator following the documented disable path got a daemon that boots cleanly, logs "listening on …", and then silently rejects health/session/SSE. - Fix: treat 0 / Infinity / non-finite as "leave the property unset" (Node's default = unlimited at this layer). Reviewer verified the Node 22 quirk; verified locally that 100 still binds the cap, 0 and Infinity now both accept connections. Issue 2 — Orphan agent child when both coalesced spawnOrAttach callers disconnect: - The BQ9tV `attachCount` race guard is monotonic. Once B's `spawnOrAttach` bumps it (synchronously, before the route handler can see `!res.writable`), the spawn-owner A's disconnect-reaper sees attachCount > 0 and skips the reap — permanently. If B then also disconnects, neither A nor B's route handler does anything, and the agent child stays alive with no client knowing the id. - Fix: add `bridge.detachClient(sessionId)` that decrements attachCount and reaps iff (attachCount == 0 && subscriberCount == 0). Server's `POST /session` handler calls it on the `!res.writable && session.attached === true` branch (symmetric to the existing spawn-owner-disconnect reap). - Subscriber-count check prevents reaping when a third client C is already on SSE — `detachClient` only fires when the session has no live consumers at all. 2 new tests for issue 1 (max-connections 0 + Infinity still accept connections; 100 still binds as supplied). 2 new tests for issue 2 (detach reaps when alone; detach preserves when SSE subscriber exists). fakeBridge updated with the new method. * fix(serve): close 3 review threads — maxConnections NaN/negative validation + doc fix + close-contract honesty - runQwenServe maxConnections validation (BUF9-): NaN / negative values previously slipped through `cap > 0 && Number.isFinite(cap)` to "leave unset = unlimited", silently fail-OPEN on a CLI typo and weakening the DoS / FD-exhaustion guard. Now throw TypeError upfront (before `app.listen()`) so a malformed cap fails the `runQwenServe` promise instead of escaping as an uncaught exception from the listen callback. - types.ts maxConnections doc (BUb7C): comment said "Node treats 0 as unlimited" but the runtime fix treats 0 as a sentinel and leaves `server.maxConnections` unset (Node 22 quirk). Updated to match. - runQwenServe close()/force-timeout (BUb7h): the 100ms eager `setTimeout(() => finish(), 100)` after `closeAllConnections()` resolved the close promise WITHOUT waiting for `server.close()`'s callback — breaking the "fully closed" contract. Now: force-close just accelerates `server.close` by killing sockets; we still wait on the close callback. A secondary 2s deadline handles the pathological "server.close never fires" case (kernel-stuck socket) with a logged warning, so shutdown stays bounded. * docs(serve): close 8 review threads — code-comment clarity + 3 new Stage 1 known gaps 8 threads in a single Claude Opus 4.7 review pass — 4 duplicate existing chiga0 finding FIXME markers, 1 code-comment clarity, 3 real new doc-worthy Stage 1 known gaps. Code clarity (BUy4U): - The shutdown re-check at doSpawn (`if (shuttingDown) { kill; throw }`) is the LOAD-BEARING correctness contract, not a band-aid as the reviewer framed it. Updated comment to explain: shutdown() runs tear-down in parallel with awaiting `inFlightSpawns` (faster fan-out); the re-check catches spawns whose `newSession` returns AFTER the flag flipped. The alternative — await all inflight to settle BEFORE snapshotting byId — is cleaner to reason about but serializes shutdown by up to `initTimeoutMs` (10s) before any live session starts tearing down. Documented the trade-off. New Stage 1 known gaps in docs/users/qwen-serve.md threat model: - BUy4H (permission auth daemon-global): cross-session vote risk acceptable under Stage 1 single-user / small-team trust model; Stage 1.5 will scope to `POST /session/:id/permission/:requestId` + session-scoped pending map + per-client identity (closes must-have #3 from the downstream review). - BUy4L (10 MB body limit on /prompt): multimodal content past 10 MB hits a cliff; workaround via path reference; Stage 1.5 accepts chunked encoding. - BUy4e (CORS deny blocks `packages/webui`): document explicit deployment options (Electron/Tauri shell, same-origin reverse proxy); Stage 1.5 adds `--allow-origin <pattern>` for opt-in named frontends. Already-marked duplicates (BUy4O, BUy4P, BUy4X, BUy4b) — covered by existing `FIXME(stage-1.5, chiga0 finding N)` / `FIXME(stage-2)` markers from prior rounds. * fix(serve): close 1 review thread — catch --hostname localhost:4170 typo upfront (BU-sh) The previous code path for unbracketed `host:port` typos went: 1. Loopback check fails (`localhost:4170` doesn't match the loopback set after lowercase normalization). 2. Throw "Refusing to bind localhost:4170:0 without a bearer token" — misleading because the operator's real bug is the colon in the hostname, not the missing token. Alternative path if a token IS supplied: hostname flows through to `formatHostForUrl` which sees the `:` and treats as IPv6, wrapping to `[localhost:4170]:port` in the printed URL. Then `app.listen()` fails with ENOTFOUND. Triple-unhelpful failure mode. Fix: catch the typo BEFORE the loopback/token check. Unbracketed input with exactly one `:` is unambiguously the host:port shape — raw IPv6 literals always have ≥2 colons (shortest is `::`), and bracketed IPv6 is handled by its own form check below. Error message suggests the corrected form (`--hostname localhost --port 4170`). * docs(serve): two new Stage 1 scope boundaries (option A + option iii) from LaZzyMan reviews LaZzyMan's two-part review surfaced two structural framing concerns distinct from the chiga0 roadmap items. Neither requires code changes in this PR — they want explicit scope honesty in the user docs: 1. TUI super-client framing (option A from the review): TUI UI is strictly larger than the wire protocol. The ~15 Ink dialogs and `local-jsx` slash commands are local-only; mutating commands like `/approval-mode`, `/memory`, `/mcp`, `/agents`, `/tools`, `/auth`, `/init` change agent behavior but emit no wire event. Documenting remote clients as sharing the agent↔user conversation axis only, NOT the full TUI session state. Implementers told to re-fetch state on reconnect, not rely on incremental events. 2. N parallel sessions cost N× (option iii from the comment): the "1 daemon = 1 session" axiom means N concurrent sessions on one workspace = N daemons with zero resource sharing. Concrete cost table at N=5 (~1.5-2.5 GB RSS, 15 MCP processes, 5× OAuth refresh) so users hit the wall with eyes open. Won't-fix on the main-line Stage 1/1.5/2 roadmap; alternatives (#3803 §21 Path A/B, in-project sidecars) materially change the architecture in ways we won't commit to mid-Stage-1. Peer-agent comparison noted (Cursor / Continue / Claude Code / OpenCode / Gemini CLI all do single-process multi-session). Both choices are intentionally the less-ambitious option; the substantive alternative (option B for taxonomy, option i/ii for N:1) moves to #3803 if real-usage data ever justifies it. * docs(serve): clarify option-A across Mode 1 (headless) vs Mode 2 (TUI co-host) Previous wording treated "TUI is a super-client" as universal truth. But Stage 1's actual shipping configuration is HEADLESS — no TUI shell runs inside the daemon — and in that mode the slash commands listed (`/approval-mode`, `/memory`, `/mcp`, `/agents`, `/tools`, `/auth`, `/init`) simply don't exist. Session state is boot-time- frozen from settings + disk, with only `/model` mutable via HTTP. Restructured the section to split the consequences: - **Mode 1 (headless `qwen serve`, this PR)**: no TUI exists; session state is boot-time-frozen + `model_switched` over HTTP; remote clients see the FULL session state; no drift possible. - **Mode 2 (Stage 1.5 `qwen --serve` co-hosted TUI, future)**: TUI exists alongside remote clients; TUI slash commands mutate session state with no wire events; remote clients see a strict subset; drift possible — re-fetch state on reconnect. The original "super-client" framing applies cleanly only to Mode 2. Mode 1 has no asymmetry — same option-A choice, different consequences. * fix(serve,sdk): close 12 review threads — 6 critical bugs + 6 follow-ups Six critical correctness fixes from the latest review pass: - httpAcpBridge.readTextFile (BX8YO): reject non-regular files via `stats.isFile()`. Char devices / FIFOs / procfs entries report `size: 0` but stream unbounded data; the 100 MiB cap wasn't enough. New `describeStatKind()` helper for human-readable error message ("named pipe (FIFO)" / "character device" / etc.). - httpAcpBridge.writeTextFile (BX8Yp + BX9_h): temp filename now includes randomUUID + exclusive flag `wx`. PID + Date.now() alone collides under concurrent writes within the same ms (sessionScope: 'thread' or coalesced spawns on same workspace). Exclusive mode fails fast on any residual collision instead of silent overwrite. - httpAcpBridge.writeTextFile (BX8Yw): resolve via `fs.realpath` before write-then-rename so symlinks are preserved. Pre-fix rename replaced the symlink with a regular file, leaving the real target unchanged while the write appeared successful. Test added covering both regular targets and symlink targets. - server.parseLastEventId (BX9_I): log a stderr breadcrumb when rejecting a non-empty non-decimal Last-Event-ID header. Pre-fix, clients with a malformed resume header silently resumed from 0 and lost every event buffered during the disconnect with zero evidence in logs. - httpAcpBridge channel.exited (BX9_P): thread {exitCode, signalCode} from the spawn factory through `session_died` event payload. Operators triaging a crash can now read the cause from the SSE frame instead of grepping daemon stderr for the child's pid. - httpAcpBridge spawnOrAttach in-flight coalesce path (BX9_U): defensive re-check that `byId.get()` is still defined after attachCount++ — if a concurrent kill tore down the entry, throw `SessionNotFoundError` instead of returning `attached: true` with a zombie sessionId. Six follow-ups in the same diff: - httpAcpBridge attachCount comment (BVryk + BWGSL): outdated "monotonic, we never decrement" claim — detachClient() now decrements. Comment rewritten to state the actual invariant ("reflects clients whose response was written or is about to be"). - runQwenServe.close() contract (BV-qW): bridge.shutdown errors are now propagated through the close promise (was: silently caught + resolved success). onSignal exits 1 instead of 0 when teardown fails. Server.close error takes precedence; bridge error is the fallback. - sdk sse parseFrame id guard (BX8Y1): require id >= 1 (was: any safe integer including negative). The daemon's Last-Event-ID parser only accepts non-negative decimals and EventBus emits ids starting at 1; negative ids on the wire diverge from resume math. Existing test updated. - runQwenServe server error listener (BX9_i): swap `server.once('error', reject)` for a persistent `server.on('error', log)` after listening. Pre-fix, a post-boot error (EMFILE etc.) was unhandled and crashed the daemon. Tests: +2 for BX8YO (FIFO) and BX8Yw (symlink preserve). Test infrastructure updated for the new `channel.exited` Promise<ExitInfo | undefined> signature. * fix(serve,sdk): close 4 more review threads — frame-scan perf + publish contract + AbortError narrowing + cross-module doc - sse consumeFrames perf (BX9_a): short-circuit the LF path first. In the common LF-only case the CRLF scan was traversing the entire remaining buffer for nothing; now CRLF is only scanned when LF is absent or potentially appears later than a CRLF separator (mixed-encoding edge). - EventBus.publish contract (BX9_p): explicit JSDoc says publish NEVER THROWS (closed-bus returns undefined, subscriber-enqueue errors caught internally). Historical try/catch wrappers in httpAcpBridge.ts are defense-in-depth, not load-bearing; new callers should not add them. - canonicalizeWorkspace doc (BX9_q): elevate the cross-module contract from "undocumented" to explicit — config.ts / settings.ts / sandbox.ts / this file all canonicalize the same way for sessionScope: 'single' re-attach. A divergence silently forks sessions per spelling. The Stage 1.5 @qwen-code/acp-bridge lift (chiga0 finding 1) is the natural place to extract a shared primitive; until then, any change to those modules needs a matching change here. - POST /session/:id/prompt AbortError swallow (BX9_k): narrow the swallow to only fire when `abort.signal.aborted` is true. The previous blanket `err.name === 'AbortError'` would also silently drop AbortErrors raised internally by the bridge (e.g. child process aborting mid-prompt), leaving the client with no response and no log trace. * docs(serve): correct N:1 framing — qwen-code's ACP agent natively supports multi-session Maintainer feedback (verified against the code): the ACP agent in packages/cli/src/acp-integration/acpAgent.ts:194 has `private sessions: Map<string, Session>` — one `qwen --acp` child natively hosts multiple sessions, and yiliang114's VSCode plugin already uses this pattern. The earlier "qwen-code is the only entry treating no multi-session resource sharing as a feature" framing (from the LaZzyMan reply + docs) was wrong. Stage 1 bridge in this PR doesn't yet leverage that capability — it spawns one `qwen --acp` child per session for simplicity (easier debugging, no cross-session interference during initial stabilization). That's a bridge-side design choice, not an ACP limitation. Revised docs/users/qwen-serve.md: - "N parallel sessions cost N×" section now distinguishes Stage 1 bridge (current N× cost) from Stage 1.5 bridge (multi-session per child, ~1/5th the cost at N=5). Cost table extended with the Stage 1.5 column. No more "won't fix on main-line roadmap" framing — the fix is a bridge refactor that pairs naturally with chiga0 finding 1 (`@qwen-code/acp-bridge` package lift), NOT the #3803 §21 Path A/B/C intra-daemon multi-session workstream (qwen-code already does that at the agent layer). - Status block's "Scope honesty" note: removed the implicit permanent-cost framing; replaced with explicit "Stage 1 bridge pays N×; Stage 1.5 refactor closes the gap" pointer. - Peer-agent comparison rewritten: qwen-code's *agent* matches Cursor / Continue / Claude Code / OpenCode / Gemini CLI on single-process multi-session; the bridge is the artifact. `httpAcpBridge.ts:doSpawn`: inline `FIXME(stage-1.5)` marker explaining the refactor (keep one child per workspace, call `connection.newSession()` multiple times on the same channel), with the link to `acpAgent.ts:194` so a future maintainer doesn't re-derive the discovery. * feat(serve): Stage 1 bridge now multiplexes sessions on one qwen --acp child per workspace Per LaZzyMan / tanzhenxin reviews + maintainer feedback verified against `packages/cli/src/acp-integration/acpAgent.ts:194` (the agent's `private sessions: Map<string, Session>`): qwen-code's ACP agent natively supports multi-session in one child process. The Stage 1 bridge previously spawned one child per session for simplicity, paying N× memory / OAuth / file-cache cost. Now refactored to leverage the agent's existing multi-session capability — one `qwen --acp` child per workspace, N sessions share it via `connection.newSession({cwd, mcpServers})`. Cost at N=5 sessions on same workspace: - Before: 300-500 MB RSS (5 children), 5× OAuth refresh, 5× file cache, 5× CLAUDE.md parse, 5× cold start - After: 60-100 MB RSS (one child), one OAuth path, shared FileReadCache, parsed once, <200ms cold start after first session Architecture changes: - New `ChannelInfo` type holds the shared channel + connection + BridgeClient + the set of session ids multiplexing on it. - New `byWorkspaceChannel: Map<workspace, ChannelInfo>` + new `inFlightChannelSpawns` coalesce-map for concurrent channel creation. - New `getOrCreateChannel(workspaceKey)` helper: reuse existing channel or spawn one (with `initialize` happening exactly once per channel, not once per session). Coalesced via `inFlightChannelSpawns` so two parallel callers don't both spawn. - `doSpawn` now calls `getOrCreateChannel` + `connection.newSession` separately (was: spawn+initialize+newSession together per session). - `BridgeClient` updated: `resolveEntry(sessionId?)` dispatches by the sessionId ACP carries in each request — one BridgeClient now serves all sessions on its channel. `sessionUpdate`, `requestPermission`, etc. all pass `params.sessionId`. - `channel.exited` cleanup moved into `getOrCreateChannel` and now tears down ALL sessions on the channel (not one). Each session gets its own `session_died` event so SSE subscribers learn the bad news on their own stream. - `killSession` now removes session from `channelInfo.sessionIds` and kills the channel ONLY when its sessionIds set drops to zero. Other sessions on the same channel keep running. - `shutdown` tears down channels (the deduplicated set) and awaits both inFlightSpawns and inFlightChannelSpawns. Cross-workspace channel sharing intentionally NOT done — `acpAgent.ts: 601 (this.settings = loadSettings(cwd))` reloads settings on each newSession call with a different cwd, so different workspaces in one child would step on each other. One channel per workspace is the safe scope. MCP server children stay per-session for now (each session can have different mcpServers config). Stage 1.5 follow-up: refcount MCP children by (workspace, config-hash) so identical configs share. Tests: - Updated `spawns fresh per call under sessionScope:thread` → now expects `handles.length === 1` (channel reused) but `sessionCount === 2` (distinct sessions). - New: `Stage 1.5 multi-session: N sessions on same workspace share ONE channel` (5 sessions, 1 factoryCalls). - New: `Stage 1.5: killSession on one of N sessions does NOT kill the shared channel` (kill 2 of 3, channel still alive; kill 3rd, channel killed). - New: `Stage 1.5: channel.exited tears down ALL multiplexed sessions` (each gets its own session_died). - FakeAgent.newSession suffixes call-count so multiple newSession calls on the same channel return distinct ids (matches real ACP behavior). Docs: - `docs/users/qwen-serve.md` N:1 section rewritten — no longer "Stage 1 pays N×, Stage 1.5 fixes". Cost table reflects current shared-channel architecture; MCP refcount called out as the one remaining Stage 1.5 follow-up; "1 daemon = 1 session" framing removed from related sections. * fix(serve,sdk): close 12 review threads — 6 critical bugs + 6 follow-ups Critical fixes: - server.ts safeBody() helper (BZ9uv/va/vs/wD + Bd10m + Bd1zz): prototype-pollution sanitization at the body-spread boundary. `__proto__` / `constructor` / `prototype` keys are stripped and the result is an Object.create(null) target. Replaces 5 sites of copy-pasted `typeof req.body === 'object'...` preamble + makes the `...(body as object)` spread sites safe. - httpAcpBridge requestPermission (Bd1yh): per-request wall-clock deadline (default 5 min, configurable via `BridgeOptions.permissionResponseTimeoutMs`). Without this, an agent calling requestPermission with no SSE subscriber connected would hang the per-session FIFO forever. After deadline, resolve as cancelled + log stderr warning. - httpAcpBridge requestPermission (Bd1z5): per-session pending permissions cap (default 64, configurable via `BridgeOptions.maxPendingPermissionsPerSession`). New requests past the cap resolve as cancelled with stderr warning. Prevents a chatty agent from growing pendingPermissions unboundedly. - runQwenServe onSignal double-signal force-exit (Bd1y6): new `bridge.killAllSync()` + `AcpChannel.killSync()` method synchronously SIGKILLs every live qwen --acp child BEFORE `process.exit(1)`. Previously double-Ctrl+C bypassed the async bridge.shutdown() and left children running as orphans. - server.ts SSE subscriber-limit response (Bd1zJ): 429 + Retry-After instead of 200 + stream_error frame. EventSource treats 4xx as terminal (no auto-reconnect); the previous 200+close-stream triggered EventSource's reconnect loop, amplifying the load the limit existed to prevent. - doSpawn ghost sessionId guard (Bd1zc): re-check byId.has() after applyModelServiceId(). The model-switch yields and can race channel.exited; without this, caller got HTTP 200 with a sessionId that 404s on every subsequent request. Follow-ups in the same diff: - sse.ts consumeFrames CRLF scan comment (BcRh_): the comment claimed the CRLF scan was bounded to `[cursor, lf)`, but Node's `indexOf` has no upper bound. Rewrote to describe what the code actually does (scan full remainder; only USE the result if it falls before `lf`). - sse.ts SseFramingError export (Bd10T): typed error class for framing-level failures so SDK consumers can distinguish "upstream isn't SSE" from generic network errors via instanceof check. Re-exported from @qwen-code/sdk. - protocol doc /health auth (Bctum): document the loopback exemption — `/health` doesn't require Authorization on loopback binds even when a token is configured. Matches `createServeApp`'s registration order. Bd1xz (cross-session permission escalation) acknowledged as duplicate of BUy4H — already documented as a known Stage 1 gap under the single-user / small-team trust model; fix is Stage 1.5 must-have #3 (per-client identity + per-session permission scope). Tests: - New: prototype-pollution test verifies `__proto__` spread doesn't pollute `Object.prototype`. - All 70 server + 55 bridge + 16 daemon-sse + 60 DaemonClient tests pass (203 total). `killSync()` stubbed on every inline test channel fake; fake bridge has `killAllSync()`. * fix(sdk): close 2 review threads — consumeFrames CRLF scan now actually bounded (BeFHR + BeFId) Previous attempt at the BX9_a perf optimization left the CRLF scan running over the full remainder of `buf` on every loop iteration where an LF separator existed — only the LF-not-found fallback path was actually bounded. Comments claimed the CRLF scan was restricted to `[cursor, lf)` or "only fires when needed", but Node's `String.indexOf` doesn't accept an end index. Bound the scan via a `buf.slice(cursor, lf)` window before `indexOf` so the assertion is now true: in the common LF-only case we pay one full scan (for LF) plus one bounded scan over the matched frame's bytes (small). * fix(serve): close 3 review threads + Windows test skip — dangling symlink, no-sessionId throw - httpAcpBridge.writeTextFile BfFvO: dangling-symlink case. `fs.realpath` throws ENOENT for a symlink whose target doesn't exist, and the blanket catch silently fell back to writing through the symlink itself — `rename(tmp, params.path)` then replaced the symlink with a regular file, exactly the bug BX8Yw was supposed to fix. Use `fs.readlink` to disambiguate "truly non-existent" from "dangling symlink"; resolve the dangling target manually and write through to it so the symlink stays a symlink. Regression test added. - httpAcpBridge BridgeClient resolveEntry BfFut: defensive throw on no-sessionId ACP call against a multi-session channel. ACP today carries sessionId on every per-session call, but if a future no-sessionId call lands, silently dropping it on a multi-session channel would be invisible. - httpAcpBridge.test.ts BX8YO Windows skip: hard-skip via `process.platform === 'win32'`. Git-Bash etc. ship a `mkfifo` binary that degenerates on Windows (creates a regular file or silently no-ops), making the assertion match the wrong error shape. Linux + macOS coverage is sufficient for a platform- agnostic `!stats.isFile()` check. BfFvW (CRLF scan comment) was already addressed in 0a4146a02 — the reviewer's diff was against the pre-fix version. * fix(serve): close 6 review threads — 4 critical bugs + 2 doc updates Critical fixes: - httpAcpBridge.doSpawn newSession-failure cleanup (BkwQA): if `connection.newSession()` throws on a freshly-created channel whose sessionIds set is empty, tear the channel down rather than leaking the empty `qwen --acp` child in `byWorkspaceChannel` (invisible to `sessionCount` / `maxSessions`). Channels with other live sessions still survive — only the truly-empty case reaps. - httpAcpBridge.detachClient + killSession tombstone (BkwQP): detachClient no longer reaps live sessions. Scenario: A spawns (attached: false, hasn't opened SSE yet), B attaches (attachCount: 1), B disconnects → previous code reaped A's still-valid session. New behavior: * killSession({ requireZeroAttaches: true }) sets `entry.spawnOwnerWantedKill = true` when it bails on attachCount > 0 (instead of just returning). * detachClient ONLY decrements attachCount. It completes the deferred reap only when (spawnOwnerWantedKill && attachCount === 0 && subscriberCount === 0). * Both-disconnected case still works (reap completes via B's detachClient seeing the tombstone). Spawn-owner-alive case no longer reaps. Existing tanzhenxin-issue-2 test rewritten; new test pins the spawn-owner-alive case. - httpAcpBridge.writeTextFile mode preservation (BkwQW): stat the target before writing; if it exists, chmod the tmp file to the preserved mode (and chown owner/group — best-effort, EPERM ignored for non-root). Previously a 0600 secret/config edit would downgrade to umask-default 0644, exposing contents to other local users. - bridge.respondToPermission option-ID validation (BkwQI): new `InvalidPermissionOptionError` thrown when the voter's `optionId` isn't in the set of options the agent originally offered in the `permission_request` event. PendingPermission now carries `allowedOptionIds`. Server route catches the error → 400 (vs. 404 for unknown requestId). Prevents authenticated clients from forging hidden outcomes like `ProceedAlways*` when the prompt's `hideAlwaysAllow` policy intentionally suppressed them. Doc fixes: - httpAcpBridge top-of-file (BkdCg) + types.ts ServeMode (BkdC8): rewrite the "each session spawns its own qwen --acp child" framing to match the actual Stage 1.5 multi-session-per-channel architecture (one child per workspace, sessions multiplex via `connection.newSession()`). * fix(serve): close 4 review threads — close write-mode race + 2 missing tests + 1 doc - writeTextFile mode-bits race (Blehd): the BkwQW fix preserved mode via `chmod` AFTER `fs.writeFile`, leaving a brief window where a `0600` secret-edit was readable at the directory's umask default (commonly `0644`). Now pass `mode` to writeFile directly so the file is CREATED with the preserved mode atomically via the `open(O_CREAT, mode)` syscall. The post-write `chmod` remains as belt-and-suspenders against a tight operator umask (POSIX `mode & ~umask` could drop bits we wanted preserved). - httpAcpBridge.test.ts: new bridge-level test for the BkwQI `InvalidPermissionOptionError` path (Blehk). Forge a vote with an `optionId` not in the agent-offered set; assert the throw AND that the pending permission survives so a valid vote can still resolve it. - server.test.ts: new route-level test for the BkwQI 400 mapping (Blehl). Fake bridge throws `InvalidPermissionOptionError`; assert response is 400 with `code: 'invalid_option_id'`, `requestId`, and `optionId` in the body. - commands/serve --http-bridge help text (Bk59I): updated to reflect Stage 1.5 multi-session — "one `qwen --acp` child per workspace, with multiple sessions multiplexed via the agent's native `newSession()`" (was: "per-session child"). * fix(sdk): close 1 review thread — parseSseStream abort path catches body-read rejection (BlqF_) Some fetch impls (undici on abort) reject the in-flight `reader.read()` with an AbortError after `reader.cancel()` fires. Pre-fix that rejection bubbled to the consumer's `for await`, contradicting the "abort cancels cleanly" public contract — code that called `controller.abort()` to wind a subscription down saw an unexpected throw on the next iteration. Wrap `reader.read()` in try/catch: - if `signal?.aborted` is true → treat the rejection as clean completion (return from the generator) - otherwise re-throw, so real upstream failures (network drop, unexpected close, malformed body) still reach the consumer Two regression tests pin the guard's scope: signal-aborted mid-stream returns cleanly with the frames received so far; a non-abort `streamController.error(...)` still bubbles via `rejects.toThrow`. * fix(serve): close 1 review thread — eventBus eviction detaches abort listener (BmJT1) Pre-fix: `publish()`'s eviction path deleted the sub from `this.subs` but never invoked `dispose()`, leaving the AbortSignal abort-listener registered in `subscribe()` attached. Because the consumer is by definition stalled (that's what caused the overflow), `next()` / `return()` never fire to detach the listener through the iterator path. Closures over the queue + sub stayed live until the AbortSignal itself went out of scope. Under attack (thousands of opened-then-stalled SSE clients), this amplified into significant heap retention. Fix: store `dispose` on `InternalSub` and invoke `sub.dispose()` from the eviction path. The same closure used by the abort listener / the iterator's `next()`/`return()` cleanup now runs through the eviction path too — idempotent through `disposed` so a post-eviction abort or iterator-return is still safe. Regression test pins the post-eviction abort + publish path producing zero side effects. * fix(serve): close 1 review thread — restore double-Ctrl+C force-kill broken by multi-session refactor (BkUyD) The Bd1y6 design promised a second SIGINT/SIGTERM during graceful drain synchronously SIGKILLs every live agent child via `bridge.killAllSync()` before `process.exit(1)` — the operator- visible "kill it now" path for a wedged child ignoring SIGTERM. The Stage 1.5 multi-session refactor (commit 6a170ef8) inadvertently broke this. `shutdown()` snapshots `byWorkspaceChannel` then CLEARS the map BEFORE awaiting the per-child SIGTERM-grace kills (up to ~10s each). If the operator double-taps mid-window, `killAllSync()` snapshotted from the now-empty `byWorkspaceChannel.values()` and silently no-op'd — the for-loop iterated nothing, `process.exit(1)` fired, and any child still inside its SIGTERM grace window was left orphaned with dangling pipes. Exactly the scenario the force-kill path was added to handle. Fix: introduce a separate `liveChannels: Set<ChannelInfo>` as the source of truth for "channels with potentially-alive child processes". Added in `getOrCreateChannel` alongside `byWorkspaceChannel.set(...)`; removed only when `channel.exited` fires (the OS-level "really dead" signal). `killAllSync()` now iterates `liveChannels`, so a mid-shutdown second signal still sees every still-alive child regardless of where the graceful drain currently is. Other paths (`killSession` last-session reap, `channel.exited` crash handler) automatically remove via the same exit-handler hook. Regression test: - Builds two sessions on different workspaces - Replaces each channel's `kill()` with a never-resolving Promise (simulating stuck SIGTERM grace) - Calls `bridge.shutdown()` to enter mid-drain state - Yields twice so shutdown's sync prefix runs (clears byWorkspaceChannel, starts the never-resolving awaits) - Calls `bridge.killAllSync()` — pre-fix this saw an empty `byWorkspaceChannel` and the spy array would have been empty; post-fix both channels' `killSync` is invoked. (tanzhenxin's other observation — channels-package duplicate ACP bridge — is the same architectural concern as chiga0 finding 1+5, already tracked under existing FIXME(stage-1.5) markers. No code change in this commit for that.)14 天前
fix: update security vulnerability reporting channel - Update SECURITY.md with proper security reporting portal - Change reporting link to Alibaba Cloud security console - Add clear guidance for security vs non-security issues Fixes #1883 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> 2 个月前
fix(build): tree-shake React reconciler dev build to prevent PerformanceMeasure leak (#4462) The ink 6→7 upgrade (v0.15.11) pulled in react-reconciler 0.33, whose development build calls performance.measure() on every component render. Since NODE_ENV was never set to "production" in the esbuild define map, the bundle shipped both dev and prod builds and selected dev at runtime, causing an unbounded measureEntryBuffer leak (~45% of heap after moderate use, confirmed via heap snapshots). Set process.env.NODE_ENV to "production" at build time so esbuild statically resolves the conditional require and tree-shakes the entire 15k-line dev build. Bundle shrinks by ~700 KB / 15,800 lines.4 天前
fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366) * fix(core): consolidate AbortController handling to stop listener leaks in long sessions Users hit `MaxListenersExceededWarning: 1509 abort listeners added to [AbortSignal]` in long interactive sessions. The agent runtime nests parent→child controllers (masterAbortController → per-message round → per-API-call round → tool execution) and each layer registered its own `addEventListener('abort', ...)` on the parent without `{once:true}` or reverse cleanup, so listeners accumulated on long-lived parents across hundreds of model turns. Add `utils/abortController.ts` with three helpers: - `createAbortController(maxListeners = 50)` — factory that pre-caps the signal so the warning never fires on per-request signals. - `createChildAbortController(parent)` — WeakRef-based parent→child propagation with `{once:true}` on the parent listener AND a reverse-cleanup listener on the child that detaches the parent listener when the child aborts. This is the key mechanism — short-lived children stop accumulating dead listeners on long-lived parents. - `combineAbortSignals(signals, {timeoutMs})` — N-way combiner that replaces the existing one-input `combinedAbortSignal.ts` (kept as a `@deprecated` shim so `httpHookRunner.ts` doesn't churn). Migrate every production `new AbortController()` in `packages/core/src` (24 sites) to the helper. Wrap `_runReasoningLoopInner` per-iteration body and `AgentHeadless.execute` in `try/finally` so the round controller is aborted (triggering reverse cleanup) even when the model stream or tool execution throws. Add `{once:true}` to the manual abort listeners in `hookRunner`, `functionHookRunner`, and `message-bus` that were missing it. Remove the `raiseAbortListenerCap` band-aid from `openaiContentGenerator/pipeline.ts` — no longer needed now that the per-round signal carries `maxListeners=50`. Add `cli/utils/warningHandler.ts` as a belt-and-suspenders: hides `MaxListenersExceededWarning.*AbortSignal` from end users in production (any shape Node ≥20 emits), keeps it visible under `DEBUG`/`QWEN_DEBUG`/ `NODE_ENV=development`. Uses `process.on('warning', ...)` without `removeAllListeners` so third-party warning subscribers stay intact. Direct reproducer in `docs/verification/abort-controller-refactor/` proves the old pattern accumulates 2000 listeners over 2000 rounds while the new pattern stays at 0. * fix(core): address PR #4366 review feedback Four issues from the Copilot review: 1. combineAbortSignals — add a per-iteration `aborted` check inside the for-loop so we short-circuit if an input signal flips aborted between the initial scan and listener registration. In single-threaded JS this can't actually interleave, but the defensive check makes correctness obvious and protects against signals whose `aborted` getter has side effects. New test exercises the path via a Proxy that flips after the initial scan. 2. warningHandler docstring — was stale: said "AbortSignal / EventTarget" while the regex was tightened to AbortSignal-only in the previous review. 3. README.md — replace personal absolute path with `$WT` placeholder so the verification recipe is shareable. 4. README.md — replace the markdown table with per-scenario headed sections. Prettier had interpreted an inline `ps -ef | grep sleep` pipe character as a column separator, breaking the table rendering on GitHub. Per-section format is also easier to scan and edit. * test(core): fix abortController race-defense test to actually hit the loop check The previous version set the Proxy's `aborted` to true before calling combineAbortSignals, so the initial `find` scan caught it and we took the fast path — not the per-iteration check the test was meant to validate. Switch to an access counter so `aborted` is false on the first read (during `find`) and true on subsequent reads (inside the loop). This forces the loop to enter, then catches the flip via the defensive per-iteration check before any listener is attached to the next input. Verified the test fails if the per-iteration check is removed. * fix(lint): include docs/**/*.mjs in the script ESLint block so the AbortController repro passes lint CI Lint flagged 11 no-undef errors in docs/verification/abort-controller-refactor/listener-accumulation-repro.mjs (AbortController, console, process) because the project's flat config only declared Node globals for ./scripts/**/*.mjs. The reviewer's suggestion (`/* eslint-env node */`) doesn't work under ESLint 9 flat config — env directives are deprecated there. The proper fix is to extend the existing script-globals block to also cover the verification repro script under docs/. * fix(core,cli): address PR #4366 critical review findings Two real bugs the reviewer caught and I confirmed locally: 1. warningHandler.ts didn't actually suppress anything. Adding a `process.on('warning')` listener does NOT prevent Node's default onWarning printer from writing to stderr — the default is just an ordinary listener registered in `lib/internal/process/warning.js`. My previous code therefore: - failed to suppress targeted AbortSignal warnings (they still hit stderr via the default printer) - produced a SECOND copy of every non-suppressed warning (default printer + my handler's own stderr.write) The unit tests missed it because they synthesised a fake warning and called `process.listeners('warning')` directly rather than going through `process.emitWarning`. Fix: snapshot the existing `'warning'` listeners (which include the default printer and any third-party telemetry hooks) BEFORE replacing them. Install ours as the sole listener. For non-suppressed warnings fan out to the captured set so the default printer + telemetry still fire; for suppressed warnings stop here. Tests now use `process.emit('warning', ...)` to drive the real listener chain, plus a spawned-child integration test that asserts the real stderr from `process.emitWarning` is empty for AbortSignal warnings and still contains DeprecationWarning text. 2. abortController.createChildAbortController kept a WeakRef to the child controller. A natural usage pattern — pass `child.signal` into an async API and drop the controller object — could let the controller be GC'd while the signal is still in use, after which `parent.abort()` would no longer propagate. Reproduced with `node --expose-gc`. Fix: hold the child strongly via the parent's listener closure. The reverse-cleanup listener still removes the closure when child aborts (closure releases child → GC-eligible), and the parent's `{once:true}` listener self-removes when parent fires (same effect). Net listener accounting on long-lived parents is unchanged; the only difference is the controller now stays alive long enough for propagation to reach downstream consumers that hold only the signal. Tests updated: drop the old `--expose-gc`-dependent assertion that abandoned children GC immediately (that was a property of the OLD contract); add a signal-only-retention test that verifies propagation under the new contract without needing GC at all. Verified: 32 helper/warning tests pass (incl. spawned-child stderr integration); 363 affected caller tests pass; typecheck + prettier + eslint clean for the touched files. * fix(core,cli): address PR #4366 review — fix combineAbortSignals orphan listeners + runtime DEBUG toggle Two real bugs the reviewer caught: 1. combineAbortSignals registered its cleanup listener on controller.signal AFTER the for-loop. Node does NOT fire 'abort' listeners added to an already-aborted signal, so when the per-iteration defensive check aborted the controller mid-loop, the cleanup never ran — orphaning every input-signal listener registered before the break, and leaving the (also-registered-after-the-break) setTimeout uncleared. Fix: skip timeout scheduling when controller.signal.aborted is already true post-loop, and when it's true call cleanup() synchronously instead of registering a doomed listener. Existing test for the mid-iteration path now also asserts that the pre-break input signal (a) has zero abort listeners — that's the assertion that catches the orphan bug. New test for the already-aborted-input + timeoutMs combination confirms the timer isn't scheduled (would otherwise overwrite the abort reason). 2. warningHandler captured isDebugMode() in a closure at init time, so toggling DEBUG / QWEN_DEBUG at runtime (e.g. via a /debug slash command) didn't update suppression behavior. Moved the check inside the handler — warnings are rare so the per-emit env-lookup cost is negligible. New test asserts a mid-stream DEBUG=1 flip starts forwarding suppressed warnings to the prior-listener chain. * test(core): strengthen the timeout-guard test in combineAbortSignals to actually exercise the new !aborted check Reviewer correctly pointed out that the previous version of this test took the pre-loop fast path (since `a.abort('pre')` ran before `combineAbortSignals`), so it never reached the in-loop guard at abortController.ts:138. Switched to the Proxy `aborted`-getter pattern from the sibling mid-iteration test (so the loop genuinely re-checks `aborted` and short-circuits inside the for-loop), and added a `setTimeout` spy that asserts the timer was never scheduled — this is the only observable difference from "scheduled then immediately cleared by synchronous cleanup()", which is what the timer-advance assertion alone couldn't distinguish. Verified by mutation testing: removing the guard makes the new test fail; restoring it makes it pass. Refs PR #4366. * test(core): cover timeout-triggered cleanup of input-signal listeners in combineAbortSignals Reviewer noted the timeout path only had an empty-input test, leaving the leak-sensitive case uncovered: when timeoutMs fires with a long-lived source signal in the input list, do the input-side listeners get released? They do (the timeout callback aborts the combined controller, which fires the auto-cleanup listener registered on its signal, which calls the per-input removeEventListener), but that path wasn't tested. Adds a test that snapshots the source listener count before, asserts it increased by 1 after combineAbortSignals returns, advances fake timers past timeoutMs, and asserts the count returns to baseline. Refs PR #4366. * fix(test): use pathToFileURL for the warning-handler e2e import on Windows CI failure on windows-latest: AssertionError: expected '\r\nnode:internal/modules/run_main:12…' to match /DeprecationWarning.*Plain deprecation/ Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:' The e2e test wrote a child script with an `import "<helperPath>"` where helperPath was a raw Windows absolute path (`D:\a\qwen-code\...`). Node's ESM loader parses that as a URL on Windows and rejects the `D:` "scheme". Converted the helper path to a `file://` URL via `pathToFileURL`. macOS test still passes; the Windows-specific schemes-must-be-URL behavior is now honored. Refs PR #4366. * fix(core,cli): address PR #4366 review batch — onAbort leak, migrate missed sites, tighten tests Adopted 6 of the 7 review threads (skipping the debug-logging suggestion). 1. processFunctionCalls onAbort leak (CRITICAL): the new `finally { roundAbortController.abort(); }` in _runReasoningLoopInner would fire the `onAbort` handler in `processFunctionCalls` if scheduler.schedule or batchDone threw (the explicit removeEventListener at the old happy-path exit would be skipped), emitting spurious "Tool call cancelled by user abort." TOOL_RESULT events for every un-emitted callId — corrupting the transcript and misleading the model on the next round. Fixed by wrapping schedule + batchDone in their own try/finally so removeEventListener always runs before the outer finally's abort. 2. Migrate 3 new-from-main `new AbortController()` sites that this PR's audit missed (they came in via the merge from main): - goals/goalHook.ts (2 sites: judgeController, fallback signal) — consistency - hooks/promptHookRunner.ts (1 site: internalAbortController) — real leak (manual addEventListener without {once:true} or cleanup, exactly the pattern this PR exists to fix). Switched to createChildAbortController + finally `internalAbortController.abort()` for reverse cleanup on the success path. 3. Repro script (`listener-accumulation-repro.mjs`): inlined helper diverged from production — used WeakRef on child, while production was changed to strong-ref earlier in this PR. Updated the inlined copy to match production exactly, with a comment noting the intentional WeakRef-on-parent-only pattern. 4. warningHandler.ts: documented the snapshot-and-replace trade-offs in the JSDoc (late-added listeners bypass our filter; late `removeListener` calls have no effect on our fan-out). Tried the re-snapshot-per-warning approach the reviewer suggested but it doesn't work — `removeAllListeners('warning')` permanently removes the snapshot from Node's tracking, so a `process.listeners('warning')` filter at fan-out time always returns empty for prior listeners. The current design is the right trade-off; documentation is the correct fix. 5. abortController.test.ts: added three coverage gaps the reviewer identified — - createChildAbortController forwards custom maxListeners - manual cleanup() before scheduled timeout fires cancels it - timeoutMs <= 0 is treated as "no timeout" 6. Migrated `httpHookRunner.ts:202` (the lone caller of the deprecated `createCombinedAbortSignal`) to `combineAbortSignals` directly, then deleted `combinedAbortSignal.ts` + its test. All semantics covered by `combineAbortSignals` tests in abortController.test.ts. Refreshed `migration-completeness.txt` (now empty — clean grep). Tests: 194 pass across abortController/warningHandler/agent-runtime/ followup/hooks/goal/promptHook suites. Typecheck + prettier clean. * docs(verification): commit the headless-scenario scripts referenced by the PR body The PR body's "End-to-end scenarios I drove locally" section points at docs/verification/abort-controller-refactor/scripts/02-lite.sh and 06-headless-sigint.sh. These are the actual reproducible commands behind the EXIT codes / warning counts reported there — checking them in so anyone can replay without copy-pasting from the PR description. Refs PR #4366. * docs(verification): sync automated-results with current state Two doc fixes the reviewer flagged: - migration-completeness.txt was a 0-byte file with a confusing cross-reference. Populated with the actual grep command + its "(no output)" result so the empty-output state is explicit. - automated-results.md still referenced combinedAbortSignal.test.ts (8 tests, @deprecated shim) — both files were deleted in 94e8c5812 when httpHookRunner.ts migrated to combineAbortSignals directly. Replaced the line with a reference to httpHookRunner.test.ts. Also updated the test counts to reflect current state (26 abortController, 13 warningHandler — both grew with the review cycle) and removed the stale combinedAbortSignal.ts entry from the prettier-check command. Refs PR #4366. * test(core): pin two abort-cascade behaviors PR #4366 introduced Adopting 2 of 3 new review threads (the third — automated-results.md drift — was already fixed in 5aa7110e4). 1. packages/core/src/agents/arena/ArenaManager.test.ts: pin the master→agent abort cascade introduced by switching per-agent controllers to `createChildAbortController(this.masterAbortController)`. New test spawns ≥2 agents, calls `manager.cancel()`, and asserts every `agentState.abortController.signal.aborted === true`. Existing cancel test only checked backend + status; if a future refactor re-introduced independent controllers, the cascade would silently regress. 2. packages/core/src/followup/speculation.test.ts: cover the `startSpeculation` abort wiring introduced when the manual addEventListener + .finally removeEventListener pattern got replaced by createChildAbortController + .finally abort(). Three tests: - parent abort propagates to state.abortController (lifetime contract) - parent-already-aborted fast path returns aborted state - parent-signal listener count returns to baseline after the fire-and- forget loop settles (reverse-cleanup proof) Mocked `runWithForkedChatModel` and `OverlayFs` so the background loop is a no-op — these tests only assert the synchronous wiring, not the loop's content. * fix(test): speculation.test.ts TS errors + sync verification doc counts Two real CI blockers in the just-added speculation tests (TS2554 and TS2339) plus stale doc counts the reviewer flagged. 1. saveCacheSafeParams takes 3 positional args (generationConfig, history, model), not a single object. Compile error on every platform. Fixed by switching to the correct shape; also moved getEventListeners to a static `import` at the top of the file (dynamic `await import('node:events')` exposes EventEmitter's static method via the namespace type rather than as a direct property, so destructuring fails type-check). 2. docs/verification/abort-controller-refactor/README.md still claimed "18 + 1 GC" tests for abortController and "9" for warningHandler; actual current counts are 26 and 13. Also dropped the stale combinedAbortSignal reference and added a note about the new ArenaManager cascade + startSpeculation wiring pin tests. Refreshed smoke-boot.log against current built bin (still 0.15.11, which is what package.json reports on this branch). Refs PR #4366. * refactor(core): narrow PR #4366 scope per yiliang's review — revert independent-controller migrations Adopting @yiliang114's review feedback (#4366 review comment, 2026-05-22): keep only the migrations that fix the real leak path (the agent-runtime parent→child chain that accumulates listeners on a long-lived parent signal in long sessions) and revert the consistency-only migrations on independent short-lived controllers. Issue #4423 confirms the user-visible bug is the nested-chain accumulation — the reverted sites do not contribute to that bug. Migrations KEPT: - agents/runtime/agent-interactive.ts (master + per-message round) - agents/runtime/agent-core.ts (per-iteration + wait + processFunctionCalls) - agents/runtime/agent-headless.ts (external → execution) - hooks/promptHookRunner.ts (real cleanup leak: addEventListener without {once:true}, never removed) - hooks/httpHookRunner.ts → combineAbortSignals direct (shim deleted) - hookRunner.ts / functionHookRunner.ts / message-bus.ts: {once:true} only - openaiContentGenerator/pipeline.ts band-aid removal (per-request signals are children of the per-round controller, which carries maxListeners=50) - warningHandler.ts belt-and-suspenders Migrations REVERTED (independent short-lived controllers; restored to `new AbortController()` + their original cleanup patterns): - agents/arena/ArenaManager.ts (master + per-agent) - agents/background-agent-resume.ts (3 sites) - core/client.ts (recall — restored manual addEventListener + finally removeEventListener pattern from main) - followup/speculation.ts (restored parentAbortHandler + finally removeEventListener) - goals/goalHook.ts (judgeController + fallback signal) - memory/manager.ts (dream controller) - services/chatCompressionService.ts (fallback signal) - services/chatRecordingService.ts (autoTitle controller) - tools/agent/agent.ts (fg + bg subagent controllers — restored manual onParentAbort + finally removeEventListener) - tools/monitor.ts (entryAc) - tools/shell.ts (promote + 3 entryAc) - utils/fetch.ts (fetchWithTimeout) Tests removed alongside the reverts: - ArenaManager.test.ts "cancels cascades..." — the cascade itself was an intentional behavioral improvement that's now reverted, so the pin-test belongs with it - speculation.test.ts "startSpeculation — abort-controller wiring" block (3 tests) — they tested helper-wired behavior we reverted Verification docs updated to reflect the narrower scope. Net change: 19 raw `new AbortController()` remain (intentional, per migration-completeness.txt rationale); previously was 0. Refs PR #4366, issue #4423.1 天前
feat(telemetry): client-side HTTP span + opt-in W3C traceparent propagation (#4384) (#4390) * feat(telemetry): propagate W3C traceparent on outbound LLM requests Part 1 of #4384 (sub-issue of #3731 P3 deeper observability). Today qwen-code's only OTel instrumentation is `HttpInstrumentation`, which only patches Node's `http`/`https` modules. The `openai` and `@google/genai` SDKs use `globalThis.fetch` (undici), so outbound LLM requests carry no `traceparent` header and trace context dies at the qwen-code process boundary. Adds `@opentelemetry/instrumentation-undici@0.14.0` (peer-compatible with the installed `@opentelemetry/instrumentation@0.203.0`) and wires it into `initializeTelemetry()` next to the existing `HttpInstrumentation`. Default propagator (W3C tracecontext + baggage) remains unchanged — no explicit `textMapPropagator` needed. `ignoreRequestHook` skips OTLP exporter endpoints to avoid the classic feedback loop (OTel SDK uses fetch to upload OTLP data; without the hook each upload would create a span that gets uploaded, infinitely). Configured `otlpEndpoint` / per-signal endpoints are stripped of trailing slash and query string for robust prefix matching against undici's `request.origin + request.path`. Outbound LLM calls now also produce a client-side HTTP span (separating network TTFB / transfer time from the existing `api.generateContent` total-duration span). Design doc: docs/design/telemetry-outbound-propagation-design.md (Part A — traceparent; Part B — session id header — lands in a follow-up PR per the design's split rationale.) 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): harden OTLP feedback-loop guard + slim lockfile diff Review feedback on #4390: 1. CI was failing on npm ci because the lockfile was generated with npm 11 locally (it sprinkles `peer: true` annotations npm 10 reads differently and rejects). Regenerated with npm 10 (matching CI's Node 22.x default), so the diff vs main is now 18 lines (the actual instrumentation-undici entry) instead of 105 lines of npm-version drift noise. 2. (Copilot inline at sdk.ts:330) `otlpUrlPrefixes` was derived from raw Config strings, so a settings.json `"otlpEndpoint": "\"http://...\""` (quoted) or trailing `#fragment` would silently miss the prefix match and reintroduce the feedback loop the hook exists to prevent. Replaced the regex-based suffix trim with a WHATWG URL parser: - strips ?query, #fragment, trailing slash - trims symmetric ASCII quotes a user may have placed in settings.json - falls back to safe suffix trimming if URL parsing fails (misconfigured endpoint still gets SOME protection) 3. (CodeQL inline) Replaced the `/\?.*$/` regex in ignoreRequestHook with `indexOf('?')`/`indexOf('#')` slicing for ReDoS hygiene. The regex was linear in practice but flagged as polynomial — using indexOf removes the ambiguity and is arguably simpler. Added 3 tests in sdk.test.ts covering the new normalizations (#fragment on incoming path, quoted endpoint, #fragment on configured endpoint). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * feat(telemetry): propagate X-Qwen-Code-Session-Id on outbound LLM requests Part 2 of #4384. Stacks on top of PR #4390 (traceparent via undici). Adds a product-namespaced HTTP header X-Qwen-Code-Session-Id to every outbound LLM request when telemetry is enabled, so server-side ingestion can correlate observed requests with qwen-code session metric/log records. Pattern matched from claude-code (X-Claude-Code-Session-Id, verified at src/services/api/client.ts:108 in their open-source repo). Critical design decision (design doc section 4.3): the OpenAI / Anthropic providers use a per-request fetch wrapper rather than the SDK defaultHeaders option, because content-generator SDK clients are constructed once and NOT recreated on /clear-triggered session resets (Config.resetSession updates this.sessionId but the contentGenerator keeps using the stale header value). Reading config.getSessionId() from inside the wrapper at request time gives the live value. Gemini provider uses static httpOptions.headers — @google/genai HttpOptions interface does not expose a fetch hook (only headers, baseUrl, apiVersion, timeout, extraParams). This is a known limitation: after session reset, Gemini X-Qwen-Code-Session-Id stays stale until the contentGenerator is recreated. Documented in telemetry.md and the design doc section 8.6; spans/logs continue to carry the live session id for trace/log correlation. Lazy-invalidate fix is a follow-up sub-issue. Header is omitted when telemetry is disabled OR when getSessionId returns an empty string (some HTTP middleware rejects empty header values). Integration sites: - packages/core/src/core/openaiContentGenerator/provider/default.ts (base class — automatically covered by deepseek/minimax/mistral/ modelscope/openrouter subclasses; openrouter calls super.buildHeaders) - packages/core/src/core/openaiContentGenerator/provider/dashscope.ts (overrides buildClient — must be touched separately; QwenContentGenerator inherits via this provider) - packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts - packages/core/src/core/geminiContentGenerator/index.ts (factory function, not the GeminiContentGenerator class — no signature change) End-to-end verification (local HTTP server in tmux): PASS: traceparent + X-Qwen-Code-Session-Id on every LLM request PASS: session id refreshes after simulated /clear (staleness regression guarded by llm-correlation-fetch.test.ts) PASS: OTLP upload traffic not traced (no feedback loop — PR A ignoreRequestHook working) Robot generated with Qwen Code https://github.com/QwenLM/qwen-code * fix(telemetry): R2 review fixes — critical correctness + tsc + boundary safety Adopts 7 review findings from wenshao on #4390 (+ duplicates from now-closed #4393). Critical bugs first, polish second. CRITICAL: 1. tsc TS2322 — wrapper return type incompatible with Anthropic SDK Fetch. `typeof fetch` (Node WHATWG, 2 overloads) is not structurally assignable to Anthropic's narrower `Fetch = (input: RequestInfo, init?) => ...`, even though they're call-compatible at runtime. Make wrapper generic `<TFetch extends FetchLikeLoose>` so callers preserve their exact fetch signature; cast the Anthropic call site through `unknown` with a comment explaining why. 2. tsc TS2352 / TS2493 — `baseFetch.mock.calls[0]![1] as RequestInit` was out-of-bounds when wrapped was called with no init arg. Replaced with a `makeFetchMock()` helper returning typed accessors. 3. normalizeOtlpPrefix catch fallback was DANGEROUS — a config of `"http"` produced prefix `"http"` which `startsWith`-matched every outbound HTTP request → silently disabled ALL instrumentation (no client spans, no correlation header — defeats the entire feature). Fixed: catch returns undefined + diag.warn. Misconfigured endpoint loses its feedback-loop guard (acceptable) instead of disabling all guards (catastrophic). 4. `url.startsWith(prefix)` matching was NOT boundary-safe — port collision (`:4318` matches `:43180`), hostname suffix collision (`otlp.example.com` matches `otlp.example.com.evil.net`), path-segment collision (`/v1` matches `/v1foo/x`). Replaced with origin-equality + path-prefix + boundary-char check (next char must be `/`, `?`, `#`, or end-of-string). 5. HttpInstrumentation also lacked the OTLP feedback-loop guard. The OTLP HTTP exporter (`@opentelemetry/exporter-trace-otlp-http`) uses node:http (patched by HttpInstrumentation, NOT undici). Without this, every OTLP upload batch creates a parasitic client span → feedback loop. Added `ignoreOutgoingRequestHook` that reuses the same `matchesOtlpPrefix` / `stripPathSuffix` helpers as the undici instrumentation. SAFETY: 6. Request input + undefined init dropped the Request's own headers (Authorization etc.) because `new Headers(undefined)` → `{...init, headers}` replaced them with just our session header. Fix: when input is a Request and init.headers is unset, seed from input.headers before adding ours. 7. Wrapped fetch had no try/catch — a throwing Config getter or Headers constructor would propagate as TypeError and break the LLM request path. Wrapped header construction in try/catch; on failure, fall through to baseFetch with original init (no header) + diag.warn. Telemetry must never break the model call. COVERAGE: - 3 new sdk.test.ts boundary tests (port/host/path) - 1 new sdk.test.ts normalizeOtlpPrefix catch-branch coverage - 1 new sdk.test.ts HttpInstrumentation OTLP guard test - 1 new sdk.test.ts proxy-mode wrapped-fetch test (default.test.ts) - 1 new anthropic test asserting wrapped fetch installed on Anthropic SDK - 2 new llm-correlation-fetch.test.ts (Request-headers preservation + try/catch fall-through) All 668 tests pass (1 pre-existing Anthropic User-Agent failure on main is unrelated). tsc clean. Declined: #10 DRY-refactor of baseFetch extraction across 3 sites — the duplication was pre-existing (default/dashscope buildClient was already near-identical), refactoring is a separate cleanup PR not gated by this feature. Will reply on the thread. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * chore(deps): allow patch updates for @opentelemetry/instrumentation-undici Switch from exact pin `0.14.0` to `^0.14.0` for consistency with the rest of the `@opentelemetry/*` deps in this block (all carated). For 0.x semver, npm treats `^0.14.0` as `>=0.14.0 <0.15.0`, so patch updates within the 0.14.x line — which are tied to the same `@opentelemetry/instrumentation@0.203.x` peer — flow in via `npm update` without requiring a manual package.json edit. A bump across the 0.x minor (e.g. 0.15.x) would shift the instrumentation peer compatibility and still requires explicit attention, which the caret correctly blocks. Per review feedback on #4390 (wenshao). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * test(telemetry): stub getTelemetryEnabled + getSessionId in Gemini factory tests The X-Qwen-Code-Session-Id commit added a `staticCorrelationHeaders(gcConfig)` call inside the Gemini content generator factory. That helper reads `gcConfig.getTelemetryEnabled()` and `gcConfig.getSessionId()` per request. Both pre-existing Gemini tests in `contentGenerator.test.ts` build a minimal partial Config stub via `as unknown as Config` and only stub the methods the factory used to need. The new call path now hits the unstubbed methods at runtime, surfacing as `TypeError: config.getTelemetryEnabled is not a function` on all three CI platforms. Add the two missing stubs to both test cases. The Gemini factory continues to ignore the values when telemetry is off — these stubs only have to exist, not return anything in particular. Local check ran the full test suite for the four directories `/loop` covers plus `src/core/contentGenerator.test.ts` itself; all green. Also re-ran the other test files that build partial Config mocks via the same idiom (`client.test.ts`, `config.test.ts`, `nextSpeakerChecker.test.ts`, `content-generator-config.test.ts`) — none exercise the new code path. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): R3 review fixes — port + protocol + quote + safety Four issues found by wenshao reviewing the R2 boundary-safety pass on PR #4390. All four close gaps where the OTLP feedback-loop guard or the correlation-header path could fail silently. 1. **Port normalization mismatch** (sdk.ts ignoreOutgoingRequestHook): `normalizeOtlpPrefix` builds prefixes via `URL.origin`, which strips default ports (`:80` for http, `:443` for https). The hook reconstructed request origin manually as `${proto}://${host}${portPart}`, keeping the port. Result: prefix `http://collector` (no explicit port) didn't match a request to `http://collector:80/v1/traces` because their `.origin` differed → guard bypassed → feedback loop. Now the reconstructed origin is also routed through `URL` so both sides apply the same default-port stripping. 2. **HTTPS proto silent fallback** (sdk.ts ignoreOutgoingRequestHook): The `(req.protocol && ...) || 'http'` fallback would silently mis-bucket HTTPS requests as HTTP when `req.protocol` was unset, so HTTPS OTLP endpoints couldn't match their prefix. Changed to fail open: when proto can't be determined, return false (request gets instrumented). Worst case is a parasitic client span — observable, recoverable — versus the previous unbounded silent feedback loop. Picked fail-open over the bot's port-based heuristic because non-standard HTTPS ports break the heuristic but not fail-open. 3. **Quote-stripping divergence** (sdk.ts normalizeOtlpPrefix): `parseOtlpEndpoint` (line 109) uses `/^["']|["']$/g` which strips asymmetric leading/trailing quotes; `normalizeOtlpPrefix` previously only stripped symmetric pairs. A settings.json typo like `"value'` would let the exporter connect (parseOtlpEndpoint trims) but leave the guard returning `undefined` (normalizeOtlpPrefix rejected) → parasitic loop. Aligned `normalizeOtlpPrefix` to the same lenient regex. 4. **`staticCorrelationHeaders` missing try/catch** (llm-correlation-fetch.ts): `wrapFetchWithCorrelation` already catches all internal exceptions and falls through to baseFetch — same "telemetry must never break LLM path" contract was missing on the static-headers helper. A throw here would propagate up to the Gemini content-generator factory and crash content-generator init for the whole session. Wrapped the body in try/catch with `diag.warn` fall-through to `{}`. Tests: added 4 regression tests covering each scenario: - default-port HTTP request matched against portless prefix (1) - hook returns false when req.protocol missing on https endpoint (2) - asymmetric-quoted endpoint normalizes for guard parity (3) - staticCorrelationHeaders returns {} when config getter throws (4) 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(telemetry): fix misleading "BOTH" wording in wrapFetchWithCorrelation The comment described the header-seeding logic as merging "BOTH the init.headers AND the Request's own headers", but the two branches are mutually exclusive — `new Headers(init?.headers)` runs unconditionally (empty Headers when init.headers is undefined), and the Request-headers copy only runs when init.headers is undefined. So in practice it's either-or, not BOTH. Reworded to match the actual logic per #4390 review feedback (wenshao). Behavior unchanged. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): strip port from req.host fallback + document undici scope Two issues found by wenshao reviewing the R3 boundary-safety fixes on PR #4390. 1. **`req.host` may already include `:port`** (sdk.ts ignoreOutgoingRequestHook): When `req.hostname` is absent and `req.host` is the fallback, the value may already be `"collector:4318"`. Naively appending `:${req.port}` produced `"http://collector:4318:4318"` → `new URL()` rejects → catch returns false → silent guard bypass for that request. Currently unreachable because `@opentelemetry/otlp-exporter-base` always sets `hostname` from WHATWG URL parsing, but the fallback exists in the code and must be correct — a future OTLP transport that emits `host` without `hostname` would silently trigger the feedback loop. Strip the port when falling back; bracketed IPv6 literals like `"[::1]:443"` keep their bracketed host intact. 2. **Undici scope honesty** (telemetry.md): Previous docs framed the propagation as "outbound LLM requests", but `UndiciInstrumentation` actually patches `globalThis.fetch` for the whole process — `WebFetch`, MCP clients, IDE extension calls all get spans + `traceparent` injection too. Added a "Scope: all fetch() calls, not just LLM" subsection covering: (a) trace ID leakage to third-party URLs (the user-supplied destinations of `WebFetch` see our trace ID; not secret per W3C but worth knowing); (b) non-LLM span volume inflating OTLP batches with a workaround tip. Per-destination scoping toggle deferred as a follow-up — out of scope for this PR. Added regression test for the host:port-fallback path. Test exercises the previously broken combination (hostname absent, host carries port) through the existing test harness. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * feat(telemetry): scope X-Qwen-Code-Session-Id to first-party hosts by default Address LaZzyMan's REQUEST_CHANGES review of PR #4390. The original design injected `X-Qwen-Code-Session-Id` on every outbound LLM request gated only by `telemetry.enabled`. Review caught that this broadcasts a stable cross-request client identifier to every configured third-party provider (OpenAI, Anthropic, OpenRouter, MiniMax, ModelScope, Mistral, vanilla Gemini, ...), which the claude-code precedent does NOT justify — claude-code is a first-party Anthropic→Anthropic flow; qwen-code is an open-source CLI connecting to many providers. Fix: add a host allowlist with a deliberately narrow default. The header is now only attached to destinations whose hostname matches: dashscope.aliyuncs.com dashscope-intl.aliyuncs.com *.dashscope.aliyuncs.com *.dashscope-intl.aliyuncs.com *.alibaba-inc.com *.aliyun-inc.com This is exactly the set where the LLM provider, the upstream telemetry backend (ARMS Tracing), and qwen-code itself are the same legal entity — mirroring the first-party claude-code pattern and preserving the real product value (server-side trace stitching against DashScope) without exposing the session id to third parties. Operators with broader correlation requirements override via: "telemetry": { "sessionIdHeaderHosts": ["*"] // restore broadcast "sessionIdHeaderHosts": [] // fully disable "sessionIdHeaderHosts": ["api.example.com", "*.foo"] // custom allowlist } Implementation: - NEW `telemetry/trusted-llm-hosts.ts`: `DEFAULT_SESSION_ID_HEADER_HOSTS` + `matchesTrustedHost(hostname, patterns)` + `extractRequestHost(input)`. Pattern syntax is intentionally tiny (bare hostname OR `*.suffix`, dot-anchored to reject `evil-alibaba-inc.com` style attacks). Unit-tested in dedicated test file including TLD/sub-domain attack vectors. - `wrapFetchWithCorrelation` (openai + anthropic providers): resolves the allowlist at wrap time (Config snapshot), inspects each request's destination URL inside `correlationFetch`, falls through to baseFetch for non-trusted destinations. Wildcard escape hatch via `["*"]`. - `staticCorrelationHeaders` (Gemini factory): now takes an optional `destinationUrl` and applies the same host gate. The Gemini SDK default endpoint `generativelanguage.googleapis.com` is NOT on the default allowlist, so vanilla Gemini calls receive no header — matching the "first-party only" scope. Operators who put the Gemini SDK on a DashScope-compatible endpoint via `baseUrl` get the header naturally. - `Config.getTelemetrySessionIdHeaderHosts()` getter + `TelemetrySettings.sessionIdHeaderHosts` interface field + JSON schema entry in `settingsSchema.ts`. Wired through `resolveTelemetrySettings`. - Defensive optional-chaining + try/catch on the Config getter call at wrap time so partial test mocks (or pre-getter Config implementations) fall back to the default allowlist rather than crashing buildClient. Tests: 12 new cases covering host match/skip on default allowlist, sub-domain handling, TLD-suffix attack rejection, `["*"]` broadcast override, `[]` full-disable, custom operator allowlist, unparseable destination (fail closed), and the three Gemini factory paths (googleapis.com default → omit; DashScope `baseUrl` → inject; custom allowlist → inject). Docs updated in `docs/developers/development/telemetry.md` Session correlation header section, including override examples and the new Gemini host-gate semantics. Closes the LaZzyMan REQUEST_CHANGES blocker. The cross-vendor fingerprint-broadcast failure mode is now opt-in rather than default, restoring the first-party-only semantics that make the claude-code precedent applicable. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): R5 review fixups — Vertex destination + ["*"] trim + docs Self-review pass on commit 1c8528a56 (host-scoped session-id header): 1. **Vertex AI destination guessing** (geminiContentGenerator/index.ts) `@google/genai` routes to `{region}-aiplatform.googleapis.com` (not `generativelanguage.googleapis.com`) when `vertexai: true` and no `baseUrl`. The previous "guess generativelanguage" default would have mis-bucketed Vertex traffic under any operator-supplied allowlist that covered the public Gemini endpoint but not the Vertex one. Today invisible (both off the default allowlist), but a latent gotcha for operators tuning `telemetry.sessionIdHeaderHosts`. Fix: pass `undefined` when `config.baseUrl` is unset (fail-closed — no header). Operators who want correlation against Google endpoints must set `baseUrl` explicitly, which is also the SDK's input for destination resolution. 2. **`["*"]` broadcast escape hatch tolerates whitespace** (llm-correlation-fetch.ts) `[" * "]` (a settings.json hand-edit with a stray space) previously silently fell back to "no host matches" — the opposite of operator intent. Now `.trim()` before comparing, so common whitespace mistakes still trigger broadcast. 3. **Doc note on wrap-time allowlist snapshot** (llm-correlation-fetch.ts JSDoc) The session id is read live per-request, but `trustedHosts` is snapshotted once at `wrapFetchWithCorrelation` call time. Spell this out in the JSDoc so a future maintainer doesn't read the live `getSessionId()` and assume the allowlist is the same shape. 4. **Defensive test coverage** (trusted-llm-hosts.test.ts, llm-correlation-fetch.test.ts) Added: extractRequestHost with explicit port / userinfo / query / fragment / IPv6 bracket form. Whitespace `[" * "]` broadcast test. IPv6 case documents the "bracketed → never matches" behavior is intentional fail-closed for the named-host allowlist scope. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * chore: regenerate settings.schema.json for sessionIdHeaderHosts Lint check `Check settings schema is up-to-date` failed because the checked-in `packages/vscode-ide-companion/schemas/settings.schema.json` wasn't regenerated after adding `telemetry.sessionIdHeaderHosts` to `settingsSchema.ts` in commit 1c8528a56. Regenerated via `npm run generate:settings-schema`. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(design): update telemetry-outbound-propagation design for R3 host-allowlist scoping Adds a "修订历史" header table at the top and a new §11 "R3 修订 — Host-Allowlist Scoping for X-Qwen-Code-Session-Id" capturing what changed after LaZzyMan's REQUEST_CHANGES review, why, and how. Inline pointers added at §3.1, §3.2, §4.3, §4.4, §9 (claude-code comparison table) to point readers at §11 — original prose preserved as a record of the decision path rather than rewritten in place. Concretely §11 covers: - The three-step LazzyMan critique and why R1's "broadcast to all providers" was structurally wrong for an open-source multi-provider CLI - The default allowlist (`DEFAULT_SESSION_ID_HEADER_HOSTS`) and its semantic alignment with the DashScope provider detector - Pattern grammar (bare hostname / `*.suffix` dot-anchored), the TLD-suffix attack vectors it rejects, why no regex / port-aware globbing - `wrapFetchWithCorrelation` host gate, wrap-time vs request-time semantics, `[" * "]` whitespace tolerance - `staticCorrelationHeaders` `destinationUrl` parameter, Gemini factory's fail-closed treatment of unset `baseUrl` (avoids the Vertex vs `generativelanguage.googleapis.com` ambiguity) - All R3 file changes mapped to the original §5 file-change list - Mapping of LazzyMan's three concerns to R3's responses - §10 future-work additions: `traceparent` per-destination toggle, `X-Qwen-Code-Request-Id`, IPv6 allowlist syntax No code changes; documentation only. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(telemetry): defensive allowlist normalization + positive proxy test Three issues found by wenshao reviewing the R3 host-allowlist scoping. 1. **[Critical] `broadcastAll` outside safety try/catch** (llm-correlation-fetch.ts wrapFetchWithCorrelation) The try/catch only fires when `getTelemetrySessionIdHeaderHosts()` throws. If it returns a malformed value — a bare string (settings.json typo `"sessionIdHeaderHosts": "host"` instead of `["host"]`), an array containing `null`/`undefined`/number entries, or whitespace-padded entries — `.some((p) => p.trim() === '*')` throws TypeError at buildClient time, bricking the LLM session before the first prompt. `staticCorrelationHeaders` already handled this via its end-to-end try/catch but the sister helper diverged. Settings loader does no runtime schema validation so this is reachable via a single typo. Fix: normalize the allowlist at wrap time: 1. catch a throwing getter (existing) 2. reject non-array → default allowlist (NEW — bare string typo) 3. filter out non-string elements (NEW — [null, ...] typo) 4. trim every surviving entry uniformly (NEW — see #2 below) Then `trustedHosts.includes('*')` instead of `.some((p) => p.trim() === '*')`, since patterns are already pre-trimmed. 2. **Trim asymmetry between `*` detection and host-pattern match** (llm-correlation-fetch.ts) `[" * "]` was tolerated (trimmed before `===` compare) but `[" dashscope.aliyuncs.com "]` silently never matched. The normalization above fixes this by trimming uniformly upstream. 3. **Proxy fetch test: only negative assertions** (openaiContentGenerator/provider/default.test.ts) The test asserted `callArg.fetch !== proxyFetch` and `!== globalThis.fetch` but both passed for ANY wrapper, including a buggy one that accidentally wraps globalThis.fetch instead of proxyFetch. Added a positive assertion: call the wrapped fetch and verify proxyFetch was the delegation target. Tests: 4 new cases — whitespace-padded host pattern, bare-string malformed config (both wrapper and static), null/number-containing array malformed config (both wrapper and static), positive proxy fetch delegation. All pass; pre-existing Anthropic User-Agent failure unrelated. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * refactor(telemetry): split outbound correlation out of telemetry scope (R4) Address LaZzyMan round-8 follow-up review on PR #4390: even though R3's host allowlist made the default behavior safe, the meta-architectural concern remains: telemetry's namespace and consent flow shouldn't quietly extend to wire-level behavior aimed at third-party LLM provider request streams. The recipient sets differ; the consent decisions differ; they deserve separate namespaces, separate threat models, separate PRs. This commit (called R4 in the design doc) collapses the PR scope so it lands ONLY telemetry observability work: REMOVED from this PR: - packages/core/src/telemetry/llm-correlation-fetch.ts(.test.ts) - packages/core/src/telemetry/trusted-llm-hosts.ts(.test.ts) - telemetry.sessionIdHeaderHosts setting + Config getter + resolveTelemetrySettings wiring + settingsSchema entry - wrapFetchWithCorrelation usage from four provider construction points (default.ts, dashscope.ts, anthropicContentGenerator.ts, geminiContentGenerator/index.ts) - All session-id provider tests across the four providers + the contentGenerator.test.ts mock stub - "Session correlation header" section in telemetry.md ADDED: - OutboundCorrelationSettings interface in packages/core/src/config/config.ts, standalone top-level namespace separate from TelemetrySettings — SECURITY-RELEVANT label, all defaults off - Config.getOutboundCorrelationPropagateTraceContext() getter - outboundCorrelation top-level entry in settingsSchema.ts with propagateTraceContext: { default: false } and explicit SECURITY-RELEVANT framing in the description - CLI config-load pipeline passes settings.outboundCorrelation into ConfigParameters - NOOP_PROPAGATOR (TextMapPropagator no-op) in sdk.ts, conditionally installed on NodeSDK when propagateTraceContext is false (default). When true, omits textMapPropagator from NodeSDK options so the SDK keeps its default W3C composite propagator - 2 new sdk.test.ts cases covering the propagator gate behavior UNCHANGED: - UndiciInstrumentation registration + OTLP feedback-loop guard + HttpInstrumentation OTLP guard from R2/R3 stay intact — they are pure telemetry (client HTTP spans into the operator's own OTLP collector), no wire-level data egress - Documentation rewrites telemetry.md to split "client-side HTTP span on outbound fetch" (telemetry) from a new "Outbound correlation (SECURITY-RELEVANT)" top-level section - design doc gets R4 revision row + new §12 "R4 Scope Conflation Split" capturing the rationale and follow-up PR outline The session-id apparatus (R3 code) lives in git history at commits 1c8528a56 / cb162e716 / 7a1b4f8d0 / 40e1efc1f / 106598ca2; the follow-up PR can cherry-pick or restore those files under the new outboundCorrelation.* namespace as LazzyMan suggested. Vscode-ide-companion settings.schema.json regenerated. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(telemetry): disclose telemetry.enabled dependency on propagateTraceContext Self-review pass on R4 commit 9bdd3bd6f flagged one footgun: both `docs/developers/development/telemetry.md` and the settingsSchema.ts description for `outboundCorrelation.propagateTraceContext` describe the toggle's behavior without noting that the flag is a silent no-op when `telemetry.enabled` is false. An operator who sets only `outboundCorrelation.propagateTraceContext: true` and forgets the telemetry switch gets zero behavior change — no error, no warning, no traceparent. Fix: add the dependency disclosure to both surfaces, plus a JSON example showing both flags wired together for the ARMS+DashScope cross-process trace continuation use case. Also fix a minor comment accuracy nit at `sdk.test.ts:683`: said the SDK installs `W3CTraceContextPropagator` instance when opt-in is true, but the actual default is `CompositePropagator(W3CTraceContextPropagator + W3CBaggagePropagator)` per `@opentelemetry/sdk-node` source. Vscode-ide-companion settings.schema.json regenerated to reflect the expanded description string. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * test(config): cover getOutboundCorrelationPropagateTraceContext defaults R4 (commit 9bdd3bd6f) added the getter but the test file didn't grow a corresponding describe block — sibling telemetry getters all have unit tests but this new one was missed. Add 4 cases covering the security-relevant default-to-false invariant and explicit-set behavior: - omitted outboundCorrelation → false - empty outboundCorrelation: {} → false (the `?? false` collapse on the getter, complementing the same on the constructor) - explicit true → true - explicit false → false PR #4390 review (wenshao). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(design): reflect post-R4 polish commits in §12 R4 (commit 9bdd3bd6f) was followed by two polish commits that the design doc §12 didn't track: - 0be0df270 (docs): telemetry.enabled dependency disclosure on propagateTraceContext — added to telemetry.md + settingsSchema description because a self-review pass identified the silent-no-op footgun (operator sets propagateTraceContext: true but forgets telemetry.enabled: true, sees zero behavior change with no error). - c0352fd5b (test): 4 config.test.ts cases covering the getOutboundCorrelationPropagateTraceContext default-false invariant (omitted / {} / explicit true / explicit false) — wenshao review flagged the test gap. Updates §12.4 with a new "Hidden dependency: telemetry.enabled" sub- section explaining the gating relationship and pointing forward at the follow-up PR (future outboundCorrelation.* settings inherit the same dependency). Updates §12.5 implementation table to add the config.test.ts row and clarify the telemetry.md / vscode-schema rows were touched again in the polish pass. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * refactor: simplify post-R4 polish per /simplify review /simplify review pass on commits 0be0df270 + c0352fd5b + 62cf6b4ee flagged 4 concerns. Fix all 4: 1. **settingsSchema.ts description**: footgun warning ("Depends on telemetry.enabled: true") was at char 600+ of a 650-char description. VS Code settings UI truncates to ~300 chars inline → the most important warning was hidden in the most-glanced view. Hoist to first sentence ("Requires telemetry.enabled: true."). 2. **config.test.ts**: drop the task-narration comment ("PR #4390 R4: keep wire-level toggle out of telemetry namespace.") that just restated the change context. The remaining 2-line comment explaining WHY default-to-false is security-relevant survives. 3. **config.test.ts**: collapse 4 separate `it()` blocks into a single `it.each([...])` covering the same 4 precondition × expectation combinations. Removes boilerplate (`new Config({...baseParams, ...})` repeated 4×) without losing assertion power; case-3 ("explicit false") was a weak duplicate of case-2 ("empty object") since both hit the same `?? false` branch, but keeping all 4 in the parametric table documents intent more clearly than dropping case-3. 4. **design doc §12.4 + §12.5**: strip specific commit SHAs (`0be0df270`, `c0352fd5b`) — design docs should be evergreen, not doubled-up commit logs (those live in `git log`). Keep the design intent ("two panels both document the dependency" / "test block added") without naming the specific commits. Regenerated vscode-ide-companion/schemas/settings.schema.json to reflect the hoisted description sentence. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)2 天前
chore(release): v0.16.1 [skip ci] Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>4 天前
Explict imports & exports with `type` modifier (#3774) 8 个月前
test(channels): add comprehensive test suites for channel adapters - Add ChannelBase, GroupGate, SenderGate, SessionRouter tests - Add DingTalk markdown utility tests - Add Weixin media and send helper tests - Add CLI channel config-utils and pidfile tests - Configure vitest for all channel packages - Exclude test files from TypeScript build Tests cover attachment handling, block streaming, gating policies, session routing, markdown conversion, config parsing, and service management. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> 1 个月前

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

一款开源的终端 AI 智能体。

中文 | Deutsch | français | 日本語 | Русский | Português (Brasil)

🎉 最新动态

  • 2026-04-15:Qwen OAuth 免费额度已终止服务。若要继续使用 Qwen Code,请切换至 阿里云 Coding PlanOpenRouterFireworks AI,或使用您自己的 API 密钥。运行 qwen auth 进行配置。

  • 2026-04-13:Qwen OAuth 免费额度政策更新:每日请求限额调整为 100 次/天(原限额为 1,000 次/天)。

  • 2026-04-02:Qwen3.6-Plus 现已上线!通过 阿里云 ModelStudio 获取 API 密钥,即可通过兼容 OpenAI 的 API 访问该模型。

  • 2026-02-16:Qwen3.5-Plus 现已上线!

为什么选择 Qwen Code?

Qwen Code 是一款面向终端的开源 AI 代理,针对 Qwen 系列模型进行了优化。它能帮助您理解大型代码库、自动化繁琐工作,从而加快开发进度。

  • 多协议支持,灵活的服务提供商:可使用 OpenAI / Anthropic / Gemini 兼容的 API、阿里云 Coding PlanOpenRouterFireworks AI,或使用您自己的 API 密钥。
  • 开源协作,共同进化:框架与 Qwen3-Coder 模型均为开源项目,两者同步发布、共同演进。
  • 智能代理工作流,功能丰富:内置丰富工具(Skills、SubAgents),支持完整的智能代理工作流,带来类似 Claude Code 的使用体验。
  • 终端优先,IDE 友好:专为习惯命令行操作的开发者打造,同时可选集成 VS Code、Zed 和 JetBrains IDE。

安装

快速安装(推荐)

Linux / macOS

bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)"

Windows(以管理员身份运行)

在命令提示符和 PowerShell 中均适用:

powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"

注意:建议在安装后重启终端,以确保环境变量生效。

手动安装

前提条件

确保已安装 Node.js 22 或更高版本。可从 nodejs.org 下载。

NPM

npm install -g @qwen-code/qwen-code@latest

Homebrew(macOS、Linux)

brew install qwen-code

快速开始

# Start Qwen Code (interactive)
qwen

# Then, in the session:
/help
/auth

首次使用时,系统会提示您登录。您可以随时运行 /auth 来切换身份验证方式。

示例提示词:

What does this project do?
Explain the codebase structure.
Help me refactor this function.
Generate unit tests for this module.
点击观看演示视频 您的浏览器不支持 video 标签。

🦞 在 Claw 中使用 Qwen Code 完成编码任务

复制下方提示词并粘贴到您的智能体中:

Use Qwen Code as a coding agent. First, fetch and read this skill reference:
https://raw.githubusercontent.com/QwenLM/qwen-code/refs/heads/main/.qwen/skills/qwen-code-claw/SKILL.md
Then follow the instructions inside to install, authenticate, and use Qwen Code via acpx for all coding tasks.

身份验证

Qwen Code 支持以下身份验证方式:

  • API Key(推荐):使用来自阿里云 Model Studio(北京 / 国际)或任何受支持提供商(OpenAI、Anthropic、Google GenAI 及其他兼容端点)的 API 密钥。
  • Coding Plan:订阅阿里云 Coding Plan(北京 / 国际),享受固定月费及更高配额。

⚠️ Qwen OAuth 已于 2026 年 4 月 15 日停止服务。 如果您之前使用 Qwen OAuth,请切换至上述任一方式。运行 qwen 然后输入 /auth 进行重新配置。

API Key(推荐)

使用 API 密钥连接阿里云 Model Studio 或任何受支持的提供商。支持多种协议:

  • OpenAI 兼容:阿里云 ModelStudio、ModelScope、OpenAI、OpenRouter 及其他 OpenAI 兼容提供商
  • Anthropic:Claude 模型
  • Google GenAI:Gemini 模型

配置模型和提供商的推荐方式是编辑 ~/.qwen/settings.json(如果不存在则创建该文件)。此文件允许您在一个位置定义所有可用模型、API 密钥和默认设置。

三步快速设置

步骤 1: 创建或编辑 ~/.qwen/settings.json

以下是一个完整示例:

{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.6-plus",
        "name": "qwen3.6-plus",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "description": "Qwen3-Coder via Dashscope",
        "envKey": "DASHSCOPE_API_KEY"
      }
    ]
  },
  "env": {
    "DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.6-plus"
  }
}

步骤 2: 了解每个字段

字段 作用说明
modelProviders 声明可用的模型以及如何连接到这些模型。openaianthropicgemini 等键代表 API 协议。
modelProviders[].id 发送到 API 的模型 ID(例如 qwen3.6-plusgpt-4o)。
modelProviders[].envKey 存储 API 密钥的环境变量名称。
modelProviders[].baseUrl API 端点 URL(非默认端点时必填)。
env 存储 API 密钥的备用位置(优先级最低;对于敏感密钥,建议使用 .env 文件或 export 命令)。
security.auth.selectedType 启动时使用的协议(openaianthropicgeminivertex-ai)。
model.name Qwen Code 启动时使用的默认模型。

步骤 3: 启动 Qwen Code — 您的配置将自动生效:

qwen

随时使用 /model 命令在所有已配置的模型之间切换。

更多示例
编码方案(Alibaba Cloud ModelStudio)—— 固定月费,更高配额
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.6-plus",
        "name": "qwen3.6-plus (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "qwen3.6-plus from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY"
      },
      {
        "id": "qwen3.5-plus",
        "name": "qwen3.5-plus (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "qwen3.5-plus with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      },
      {
        "id": "glm-4.7",
        "name": "glm-4.7 (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "glm-4.7 with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      },
      {
        "id": "kimi-k2.5",
        "name": "kimi-k2.5 (Coding Plan)",
        "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
        "description": "kimi-k2.5 with thinking enabled from ModelStudio Coding Plan",
        "envKey": "BAILIAN_CODING_PLAN_API_KEY",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      }
    ]
  },
  "env": {
    "BAILIAN_CODING_PLAN_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.6-plus"
  }
}

订阅 Coding Plan 并在 Alibaba Cloud ModelStudio(Beijing)Alibaba Cloud ModelStudio(intl) 获取您的 API 密钥。

多提供商(OpenAI + Anthropic + Gemini)
{
  "modelProviders": {
    "openai": [
      {
        "id": "gpt-4o",
        "name": "GPT-4o",
        "envKey": "OPENAI_API_KEY",
        "baseUrl": "https://api.openai.com/v1"
      }
    ],
    "anthropic": [
      {
        "id": "claude-sonnet-4-20250514",
        "name": "Claude Sonnet 4",
        "envKey": "ANTHROPIC_API_KEY"
      }
    ],
    "gemini": [
      {
        "id": "gemini-2.5-pro",
        "name": "Gemini 2.5 Pro",
        "envKey": "GEMINI_API_KEY"
      }
    ]
  },
  "env": {
    "OPENAI_API_KEY": "sk-xxxxxxxxxxxxx",
    "ANTHROPIC_API_KEY": "sk-ant-xxxxxxxxxxxxx",
    "GEMINI_API_KEY": "AIzaxxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "gpt-4o"
  }
}
启用思考模式(适用于 qwen3.5-plus 等支持的模型)
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3.5-plus",
        "name": "qwen3.5-plus (thinking)",
        "envKey": "DASHSCOPE_API_KEY",
        "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "generationConfig": {
          "extra_body": {
            "enable_thinking": true
          }
        }
      }
    ]
  },
  "env": {
    "DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3.5-plus"
  }
}

提示: 您也可以通过 shell 中的 export 命令或 .env 文件设置 API 密钥,其优先级高于 settings.json 中的 env 配置。完整详情请参阅 身份验证指南

安全注意事项: 切勿将 API 密钥提交到版本控制系统中。~/.qwen/settings.json 文件位于您的主目录下,应保持私密。

本地模型设置(Ollama / vLLM)

您也可以在本地运行模型——无需 API 密钥或云账户。这并非一种身份验证方法,而是通过 modelProviders 字段在 ~/.qwen/settings.json 中配置本地模型端点。

在匹配的提供程序条目内设置 generationConfig.contextWindowSize,并将其调整为您本地服务器上配置的上下文长度。

Ollama 设置
  1. ollama.com 安装 Ollama
  2. 拉取模型:ollama pull qwen3:32b
  3. 配置 ~/.qwen/settings.json
{
  "modelProviders": {
    "openai": [
      {
        "id": "qwen3:32b",
        "name": "Qwen3 32B (Ollama)",
        "baseUrl": "http://localhost:11434/v1",
        "description": "Qwen3 32B running locally via Ollama",
        "generationConfig": {
          "contextWindowSize": 131072
        }
      }
    ]
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "qwen3:32b"
  }
}
vLLM 配置
  1. 安装 vLLM:pip install vllm
  2. 启动服务器:vllm serve Qwen/Qwen3-32B
  3. 配置 ~/.qwen/settings.json
{
  "modelProviders": {
    "openai": [
      {
        "id": "Qwen/Qwen3-32B",
        "name": "Qwen3 32B (vLLM)",
        "baseUrl": "http://localhost:8000/v1",
        "description": "Qwen3 32B running locally via vLLM",
        "generationConfig": {
          "contextWindowSize": 131072
        }
      }
    ]
  },
  "security": {
    "auth": {
      "selectedType": "openai"
    }
  },
  "model": {
    "name": "Qwen/Qwen3-32B"
  }
}

使用方法

作为一款开源终端智能体,您可以通过以下五种主要方式使用 Qwen Code:

  1. 交互模式(终端界面)
  2. 无头模式(脚本、CI)
  3. IDE 集成(VS Code、Zed)
  4. 软件开发工具包(TypeScript、Python、Java)
  5. 守护进程模式 —— qwen serve 通过 HTTP+SSE 暴露 ACP,以便多个客户端共享一个智能体(实验性)

交互模式

cd your-project/
qwen

在项目文件夹中运行 qwen 以启动交互式终端界面。使用 @ 引用本地文件(例如 @src/main.ts)。

无头模式

cd your-project/
qwen -p "your question"

使用 -p 可在无交互式界面的情况下运行 Qwen Code,非常适合脚本、自动化及 CI/CD 场景。了解更多:Headless mode

IDE 集成

在编辑器(VS Code、Zed 和 JetBrains IDEs)中使用 Qwen Code:

守护进程模式(qwen serve,实验性)

cd your-project/
qwen serve
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge)

将 Qwen Code 作为本地 HTTP 守护进程运行,这样 IDE 插件、Web UI、CI 脚本和自定义 CLI 都能通过 HTTP+SSE 共享一个智能体会话,而不是各自生成独立的子进程。默认情况下,环回绑定不进行身份验证(即使在环回上,设置 QWEN_SERVER_TOKEN 也可启用持有者身份验证);远程绑定(--hostname 0.0.0.0必须使用令牌——若未提供令牌,启动将失败。请参阅:

SDK

借助可用的 SDK 基于 Qwen Code 进行构建:

Python SDK 示例:

import asyncio

from qwen_code_sdk import is_sdk_result_message, query


async def main() -> None:
    result = query(
        "Summarize the repository layout.",
        {
            "cwd": "/path/to/project",
            "path_to_qwen_executable": "qwen",
        },
    )

    async for message in result:
        if is_sdk_result_message(message):
            print(message["result"])


asyncio.run(main())

命令与快捷键

会话命令

  • /help - 显示可用命令
  • /clear - 清除对话历史
  • /compress - 压缩历史记录以节省 tokens
  • /stats - 显示当前会话信息
  • /bug - 提交错误报告
  • /exit/quit - 退出 Qwen Code

键盘快捷键

  • Ctrl+C - 取消当前操作
  • Ctrl+D - 退出(在空行时)
  • 上/下方向键 - 浏览命令历史

了解更多关于 Commands

提示:在 YOLO 模式(--yolo)下,检测到图片时会自动切换视觉模式,无需提示。了解更多关于 Approval Mode

配置

Qwen Code 可通过 settings.json、环境变量和 CLI 标志进行配置。

文件路径 作用范围 描述
~/.qwen/settings.json 用户(全局) 适用于所有 Qwen Code 会话。推荐在此配置 modelProvidersenv
.qwen/settings.json 项目 仅在当前项目中运行 Qwen Code 时生效。会覆盖用户级别的设置。

settings.json 中最常用的顶级字段:

字段 描述
modelProviders 按协议(openaianthropicgeminivertex-ai)定义可用模型。
env 备用环境变量(如 API 密钥)。优先级低于 shell 的 export 命令和 .env 文件。
security.auth.selectedType 启动时使用的协议(如 openai)。
model.name Qwen Code 启动时使用的默认模型。

完整的 settings.json 示例请参见上文的 Authentication 部分,所有可用选项请参见 settings reference

基准测试结果

Terminal-Bench 性能

智能体 模型 准确率
Qwen Code Qwen3-Coder-480A35 37.5%
Qwen Code Qwen3-Coder-30BA3B 31.3%

生态系统

需要图形界面?

  • AionUi 适用于命令行 AI 工具(包括 Qwen Code)的现代 GUI
  • Gemini CLI Desktop 适用于 Qwen Code 的跨平台桌面端/网页端/移动端界面

故障排除

如遇问题,请查看 故障排除指南

常见问题:

  • Qwen OAuth free tier was discontinued on 2026-04-15:Qwen OAuth 已停止服务。运行 qwen/auth 并切换至 API Key 或 Coding Plan。设置说明请参见上文的 身份验证 部分。

如需从 CLI 内报告错误,请运行 /bug 并包含简短标题和复现步骤。

联系我们

致谢

本项目基于 Google Gemini CLI 开发。我们对 Gemini CLI 团队的出色工作表示认可与感谢。我们的主要贡献集中在解析器层面的适配,以更好地支持 Qwen-Coder 模型。