Find a file
易良 bb45a09c06
fix(core): reject upstream fail-fast placeholder responses (#8938)
* feat(core): reject upstream fail-fast placeholder responses

Upstream endpoints occasionally fail fast with an HTTP 200 whose entire
body is the placeholder text '(request timeout)'. The response passes
all existing stream validation and gets persisted as a normal assistant
reply, polluting subsequent request context and surfacing to users as a
lost reply.

- Throw InvalidStreamError('UPSTREAM_DEGRADED_RESPONSE') at stream end
  when the whole response body is exactly the placeholder, reusing the
  transient retry budget so the turn is rolled back and re-attempted.
- Treat placeholder-only model turns as invalid in extractCuratedHistory
  so already-polluted sessions (including --resume) stop replaying them.

Both checks are exact whole-turn matches to avoid false positives on
legitimate mentions of the text.

Fixes #8916

* fix(core): prevent placeholder text from reaching display before retry (#8938)

- Defer the first chunk with a finishReason in non-continuation turns so
  post-stream validation (placeholder check, empty-text check) can reject
  it before consumers see the content.  Without this a single-chunk
  fail-fast placeholder (text + finishReason: 'STOP') was yielded to the
  TUI / headless adapter before the throw, and neither consumer rolled
  back the committed output.
- Add exhaustion test for UPSTREAM_DEGRADED_RESPONSE retry budget.
- Add curation test for whitespace-padded placeholder (.trim() guard).
- Add curation test for placeholder turn carrying a functionCall.
- Assert the placeholder text is absent from emitted stream events.

* fix(core): block degraded continuation leaks

* fix(core): catch split degraded placeholders

* fix(core): withhold degraded placeholder chunks

* fix(core): preserve current image payloads after placeholder curation

* fix(core): preserve deferred stream chunks safely

* fix(core): fold held placeholder-prefix text into continuation accounting

When a stream is cut while degraded-placeholder-prefix chunks are held
back (never yielded), the send loop's delivery accounting saw nothing
delivered and replayed fresh — letting a placeholder split across the
cut ('(request ' + 'timeout)') slip through as the harmless-looking
fragment. Stage the held text on the pipeline's error path and fold it
in the attempt's catch so the continuation gate resumes from the prefix
and the reassembled text is still rejected as a completed placeholder.
A completed placeholder is the exception: there is nothing honest to
resume from, so that recovery stays a fresh replay.

Fixes the split-placeholder regression test; all 332 geminiChat tests
green.

* fix(core): latch deferredFirstChunk so duplicate finishReason chunks are not dropped

A second finishReason-bearing chunk arriving before any yield overwrote
deferredFirstChunk (plain assignment at both deferral sites), so only the
slot's FINAL value was yielded post-stream while every overwritten chunk's
parts still landed in allModelParts -> history/JSONL: text persisted but
never displayed, or a functionCall persisted but never dispatched. Latch
the first deferred chunk with ??= at both sites (R8-1).

* fix(core): judge deferred chunk against accumulated text on stream error

The error-path escape check judged the deferred chunk by its own parts
only. A placeholder split into a held prefix and a finishReason-bearing
tail ('(request ' + 'timeout)') failed that per-chunk prefix check on
the tail alone: the fragment leaked to consumers while the held prefix
was staged after it, scrambling the continuation handoff. Judge the
deferred chunk against the accumulated attempt text — the in-loop hold's
own expression — and hand the combined text off in delivery order.

Also drop dead stores: the yieldedAnyChunk write after the loop's last
read and the trailing local resets after the final yields.

* fix(core): deliver withheld placeholder-defense chunks in arrival order, drop the invisible continuation handoff

Three review findings on the placeholder-defense machinery:

1. The deferred first chunk reached consumers out of arrival order — the
   post-loop flush yielded held chunks first, and any inline-yielded chunk
   preceded it, so displayed text diverged from arrival-order history/JSONL
   (and consolidation's in-place part merge rewrote the shared part the
   deferred chunk carried, duplicating text on delivery).

2. The continuation handoff staged never-delivered held text into the
   transport-continuation buffer; a continuation that diverged from
   completing the placeholder persisted the invisible prefix into
   history/JSONL while the UI showed only the remainder.

3. The staged handoff survived the turn through the MAX_TOKENS recovery
   wrapper (which rethrows non-InvalidStreamError without the folding
   catch) and could be folded into a later turn's continuation buffer.

Changes:
- Unify the deferred chunk and held placeholder-prefix chunks into one
  arrival-ordered pendingPreValidationChunks list; every flush (in-loop
  divergence release, stream-error delivery, post-loop flush) yields it in
  arrival order, so deferral can no longer reorder delivery.
- Snapshot the pending chunks with detachChunkParts before history
  consolidation merges adjacent text parts in place, so post-consolidation
  flushes deliver what actually arrived instead of rewritten text.
- On a stream error, deliver the pending chunks (mirrored by the send loop
  into the continuation buffer, keeping delivery and persistence aligned)
  unless they complete the placeholder — a completed placeholder is still
  dropped silently for a clean fresh replay. This replaces the
  pendingHeldPlaceholderText field, its staging, the send-loop fold and the
  entry reset: never-delivered text no longer enters the continuation
  buffer, which also closes the cross-turn leak.

Tests: arrival-order delivery of a deferred chunk + later held chunks;
in-order delivery of a diverged placeholder prefix without retry; a lone
placeholder-prefix chunk delivered and persisted without rejection;
delivery/persistence alignment when a held-prefix continuation diverges.
Existing split-placeholder continuation coverage still green (340/340).

* fix(core): propagate image eviction to durable history, harden placeholder gates

Round-10 review findings:

- replaceImagePayloadsInPlace now rewrites the shared Part object itself
  (part.text = reference; part.inlineData = undefined) instead of only
  replacing the content.parts[i] slot: when placeholder curation makes two
  user turns adjacent, appendCuratedContent merges them into a fresh
  Content whose parts array reuses the durable history's Part objects, and
  a slot-only swap left the inline base64 payload alive in history — the
  eviction pass re-ran every request (countAllInlineImages stayed at the
  threshold) and store.put() re-hashed the multi-MB payload each time, so
  the memory this mechanism exists to shed never left history. Nested
  functionResponse parts get the same treatment (getFunctionResponseParts
  returns the live array). Regression test pins the durable side.
- Extract nonThoughtText(parts) and use it at both placeholder gates (the
  per-chunk hold and the stream-error completed-placeholder drop) so the
  two decisions cannot drift when the text-accumulation semantics change.
- Log the one deliberate discard: the completed-placeholder drop on the
  stream-error path now emits a debugLogger.warn naming the withheld chunk
  count, so a benign transport-cut recovery is distinguishable from lost
  output after the fact.
- Comment at the eviction call site names skipParts as the load-bearing
  protection for the current message's images (the identity skipEntry find
  misses whenever curation merged the user turns into a fresh object).

* fix(core): preserve deferred stream metadata

* fix(core): close round-12 findings on degraded-placeholder defense

- XML tool-call recovery now aligns the withheld pre-validation chunks
  with the rewritten turn: their plain text is rewritten to the stripped
  remainder so consumers no longer receive the raw `<invoke>` markup
  while history persists the recovered shape (fixes the deterministic
  'retains a short text prefix' failure red-lining CI)
- completed-placeholder detection is now cross-attempt: a placeholder
  that completes across a transport cut (prefix before the cut,
  remainder before the error) is discarded instead of persisting via
  the continuation-prefix fold, and the send loop refuses to continue
  from a delivered text that equals the completed placeholder — it
  routes through the invalid-stream budget and retries fresh
- mid-stream flush of held chunks yields detached copies so post-stream
  consolidation cannot rewrite already-delivered part objects
- image reattach now resolves `Image #<id>` references from the store
  even below the eviction threshold, so fallback/recovery rebuilds
  after an in-place eviction pass don't carry markers without pixels

* fix(core): close round-13 findings on degraded-placeholder defense

R13-1/R13-4: the completed-placeholder conversion no longer throws
from inside the send loop's catch (which escaped the loop and skipped
the invalid-stream budget — zero retries, no telemetry, no fallback
chain). Both conversion sites now fall through to the budget handler:
the send-loop guard converts when the folded buffer completes the
placeholder, and the stream-error discard converts before rethrowing,
so a placeholder completed across a transport cut retries fresh with
the continuation reset instead of continuing from the garbage prefix.

R13-2: the fork boundary (copyHistoryContainers) now shallow-clones
Part objects — including nested functionResponse.parts — so a forked
chat's in-place image eviction can no longer strip payloads out of the
MAIN conversation's durable history (the snapshot chain shares part
objects by reference; the ids would exist only in the discarded fork's
store). The stale contract test is updated to pin the new isolation.

R13-3: the cross-attempt discard merges the UNTRIMMED attempt text
(trim only after the merge), matching the persistence merge so a
remainder opening with whitespace cannot slip past the discard.

R13-8: the below-threshold reference scan is skipped entirely when the
image payload store is empty (the common steady state).

R13-10: reattach candidates exclude ids already carried inline in the
same request (content-hash ids would otherwise ship the same payload
twice per send).

R13-15: the per-chunk placeholder hold compares the MERGED delivered
text when a continuation prefix is in flight, so a remainder that
finishes the placeholder over several chunks is withheld instead of
yielded inline before the post-stream gate throws.

Tests: double-cut regressions for both remainder shapes (with/without
finish frame), e2e below-threshold reattach, fork-boundary isolation
(top-level + nested), inline-id dedupe, split-tail converted to the
retry path, plus the R13-7/9/11/12/13/14 assertion strengthenings.

* test(core): pin full history in the second tool-result continuation test

R13-14 follow-up: the 'deliver every deferred chunk in a tool result
continuation' test also pinned history only via at(-1); replace with the
full expected history so a drifted partial-turn pop orphaning the
functionResponse turn cannot hide.

* fix(core): read the converted placeholder error type in budget telemetry

Round-13 follow-up: the invalid-stream budget handler's debug log and
ContentRetryEvent still read (error as InvalidStreamError).type, but on
the converted-placeholder path `error` is the raw transport error and
the InvalidStreamError lives in budgetError — the retry log printed
[undefined] and the telemetry event carried a wrong type. Read
budgetError.type at both sites.

* fix(core): exempt last-referenced images from reattach cap + harden tests

Round-14 review of the degraded-placeholder defense (#8938).

Critical fix (image-payload-references): buildReattachParts ranked ids
referenced in the user's LAST message by their original eviction-marker
position, so under the maxRecentImages cap an explicitly-referenced
older image lost its slot to a stale, never-requested marker — the
reference-resolution feature silently failed in its primary scenario.
Ids referenced in the last content are now collected separately and
reattached unconditionally, outside the recency cap, mirroring
prepareImagePayloadsForRequest's unconditional referencedIds reattach.

Doc (forkedAgent): the CacheSafeParams.history contract now states the
shallow-clone behavior — consumers may rewrite part objects in place,
but deeper payload objects (inlineData, functionResponse.response)
remain shared and must not be mutated.

Test-efficacy hardening (all mutation- or trace-verified gaps from the
review): nested functionResponse inline-twin dedupe; cap-exempt
referenced-image regression; marker-present + placeholder-absent pins
on the eviction test; below-threshold e2e cap-binding, all-images
eviction, post-send-2 marker state, exactly-once reattach; mid-history
and consecutive-run placeholder curation semantics; shape-A double-cut
record/history agreement + corrected mechanism comment; diverged
continuation second-half delivery; plain-RETRY-after-rejection pin;
UPSTREAM_DEGRADED_RESPONSE routing pin on cut-before-finish;
deferred-chunk ORDER pin (finish chunk must be last); retry-exhaustion
no-CHUNK-to-consumer pin (via an expectStreamExhaustion collector);
and a hold-liveness test pinning that non-placeholder text is released
inline before stream end.

* refactor(core): dedupe placeholder-error and reattach-append sites

Ponytail review follow-ups on the degraded-placeholder defense (#8938):

- Extract a degradedPlaceholderError() factory: four call sites
  (send-loop catch-convert, stream-error completed-placeholder discard,
  and the two post-stream gates) constructed the identical
  InvalidStreamError literal; the factory keeps the message/type strings
  from drifting between sites.
- Extract appendReattachParts(): both request-history branches ended in
  the same 7-line "append onto last user turn, else push a fresh one"
  block; the shared helper keeps the append shape from drifting.

* fix(core): gate placeholder conversion on !streamYieldedFunctionCall (#8938)

- The send-loop placeholder->InvalidStreamError conversion was the only
  placeholder-retry producer that could fire after a functionCall chunk
  had been delivered: a folded '(request timeout)' completed on the same
  attempt that emitted a tool call would schedule a fresh retry after
  the consumer received the call, orphaning the tool_use/tool_result
  pairing. Add the same point-of-no-return guard every sibling path
  enforces; the original transport error now propagates instead.
- Regression test: folded placeholder + delivered functionCall => no
  third attempt, stream terminates, last chunk is the tool call.
- Test pins from review: budget-routing assertions (mockLogContentRetry
  with UPSTREAM_DEGRADED_RESPONSE) on both double-cut tests, a
  non-vacuous retry guard on the prefix+remainder rejection test, and
  the throwing structuredClone spy on the getHistoryLength no-clone
  contract (matching the sibling walk-only accessors).

* test(core): attach the rejection handler before timer flush (#8938)

The folded-placeholder/functionCall regression test rejects the consumer
stream mid timer-advance; attaching the expect().rejects handler only
after advancing left a handlerless rejection at the timer checkpoint,
which vitest's unhandled-error gate turns into a red run under parallel
CI load (all 20094 core tests passed; exit 1 came from the single
unhandled rejection). Attach the expectation before advancing timers so
the handler is on the promise regardless of when the send loop rejects.

* test(core): satisfy vitest/valid-expect on the pre-timer rejection handler (#8938)

The previous fix assigned expect().rejects to a variable, which
eslint's vitest/valid-expect rule rejects (async assertions must be
awaited or returned) — CI lint failed before the suite even ran. Keep
the unhandled-rejection protection by registering a plain catch handler
before advancing timers, and leave the awaited expect().rejects
assertion as the lint-compliant pin.

* fix(core): scope placeholder run invalidation to the degraded turns (#8938)

R16-1: extractCuratedHistory dropped the WHOLE consecutive model run
when any turn was a degraded placeholder, so a sibling functionCall
turn was dropped while the following user(functionResponse) survived —
the outgoing request carried an orphaned functionResponse, exactly the
invalid pairing the continuation gate and the MAX_TOKENS hasFunctionCall
check exist to avoid (pre-PR the run stayed whole and provider-valid).

- curation now drops only the placeholder turns of a run and keeps
  valid siblings (invalid-content runs keep the pre-existing
  whole-run semantics)
- repairOrphanedToolUseTurns scan no longer lets placeholder turns
  split the model<->user pairing adjacency: an fr right after a
  placeholder sibling counts as adjacent (the mirror run
  [functionCall, placeholder] + fr previously synthesized a duplicate
  error-fr because the placeholder broke adjacency)
- the whole-run drop test is rewritten to the revised per-turn
  semantics; new regression tests pin both run directions through the
  public API (mutation-verified: the old whole-run drop fails them).
350/350 green, tsc + eslint clean.

* refactor(core): narrow degraded placeholder defense

* fix(core): preserve placeholder-aware tool adjacency

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-19 10:34:52 +00:00
.github fix(triage): diff the flake-gate file list against the pinned base OID (#9464) 2026-08-19 07:09:49 +00:00
.husky Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
.qwen feat(ci): post autofix failure-path handoff comments bilingually (#9386) 2026-08-19 00:17:35 +00:00
.vscode Merge branch 'main' into feat/sandbox-config-improvements 2026-03-06 14:38:39 +08:00
docs feat(extensions): support authenticated HTTPS Git installs (#9458) 2026-08-19 09:06:54 +00:00
docs-site Hide internal docs from docs site (#4357) 2026-06-01 15:55:14 +08:00
eslint-rules pre-release commit 2025-07-22 23:26:01 +08:00
integration-tests feat(extensions): support authenticated HTTPS Git installs (#9458) 2026-08-19 09:06:54 +00:00
integrations/external-context feat(external-context): Add provider extension profile (#9068) 2026-08-16 15:50:45 +00:00
packages fix(core): reject upstream fail-fast placeholder responses (#8938) 2026-08-19 10:34:52 +00:00
patches feat(cli): add TUI image display tool (#8217) 2026-08-01 12:39:52 +00:00
scripts feat(review): wire --resume through /review and the review run subcommand (#9153) 2026-08-19 05:13:09 +00:00
.dockerignore fix(cli): skip stdin read for ACP mode 2026-03-27 11:47:01 +00:00
.editorconfig pre-release commit 2025-07-22 23:26:01 +08:00
.gitattributes feat(installer): add standalone hosted install and uninstall flow (#3828) 2026-05-21 11:57:10 +08:00
.gitignore chore(ci): Add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and Scorecard (#9008) 2026-08-14 01:22:53 +00:00
.npmrc chore: remove google registry 2025-08-08 20:45:54 +08:00
.nvmrc chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
.prettierignore feat(acp): support /cd command in ACP sessions (#5903) 2026-06-27 14:47:40 +00:00
.prettierrc.json pre-release commit 2025-07-22 23:26:01 +08:00
.yamllint.yml feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
AGENTS.md fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149) (#9171) 2026-08-18 13:19:09 +00:00
CHANGELOG.md chore(release): v0.21.11 (#9054) 2026-08-13 05:41:43 +00:00
CLAUDE.md docs: rewrite CLAUDE.md to point to AGENTS.md as authoritative source (#5138) 2026-06-15 15:23:26 +08:00
CONTRIBUTING.md revert: remove local PR verification gate (#7031) 2026-07-16 11:24:38 +00:00
Dockerfile perf(ci): cut the E2E suite from ~40min to ~24min (#7798) 2026-07-28 12:56:34 +00:00
esbuild.config.js feat(core): add full-resolution image zoom tool (#7809) 2026-07-27 23:40:26 +00:00
eslint.config.js refactor(cli): stop utils/ importing serve/ and the UI layer (#9146) (#9147) 2026-08-14 10:30:40 +00:00
eslint.legacy-filenames.mjs feat(workflows): add cooperative pause and resume (#8320) 2026-08-08 04:21:21 +00:00
LICENSE Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
Makefile feat: update docs 2025-12-22 21:11:33 +08:00
package-lock.json feat(external-context): Add provider extension profile (#9068) 2026-08-16 15:50:45 +00:00
package.json chore(deps): bump sharp to ^0.35.0 to resolve GHSA-f88m-g3jw-g9cj (#8952) 2026-08-13 06:56:10 +00:00
README.md docs(readme): add Korean to the documentation language bar (#8836) 2026-08-10 07:34:11 +00:00
SECURITY.md fix: update security vulnerability reporting channel 2026-02-24 14:22:47 +08:00
tsconfig.json # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
vitest.config.ts feat(channel): add QQ Bot (QQ机器人) channel adapter (#5202) 2026-06-19 06:32:52 +08:00

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

The open-source AI coding agent that lives in your terminal.

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

Why Qwen Code?

  • Agentic out of the box — Auto-Memory, Auto-Skills, SubAgents, Agent Teams, and MCP. Dynamic workflows, zero setup.
  • Open-source, inside and out — The framework and the Qwen models are open-source. They evolve together. No vendor lock-in.
  • Multi-protocol — Supports OpenAI, Anthropic, Gemini, and Qwen APIs. Any third-party provider or local model (Ollama / vLLM). Switch at runtime.
  • Beyond the terminal — IDE plugins, Desktop app, daemon mode, SDKs, and IM bots (Telegram / DingTalk / WeChat / Feishu).

Tip

Qwen Code is actively iterating on itself — using its own agent and models to file issues, submit PRs, review code, and run tests. Powered by the community, driven by AI.

Installation

Linux / macOS:

curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh | bash

Windows:

irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex

Restart your terminal after installation to ensure environment variables take effect.

NPM / Homebrew

NPM (requires Node.js 22+):

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

Homebrew (macOS / Linux):

brew install qwen-code

Quick Start

qwen          # Launch interactive terminal UI
# Inside the session:
/auth         # Configure your provider and API key

See the Authentication Guide and Settings Reference for detailed setup.

Qwen Code

How to Use Qwen Code

Mode Command Use Case
Interactive qwen Terminal UI with rich rendering, @file references, slash commands
Headless qwen -p "..." Scripts, CI/CD, batch processing — no UI
IDE VS Code, Zed, JetBrains
Desktop Qwen Code Desktop — GUI for macOS, Windows, Linux
Daemon qwen serve Shared agent session over HTTP+SSE (ACP). Multiple clients, one agent. (experimental) Docs
SDK TypeScript, Python, Java
IM Bot qwen channel Connect to Telegram, DingTalk, WeChat, or Feishu
SDK example (Python)
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())

Capabilities

If you know Claude Code, you already know Qwen Code — and then some. We've put significant effort into bringing Qwen Code to feature parity with Claude Code, improving both breadth and reliability across the board.

Feature Qwen Code Claude Code
SubAgents, Agent Teams, Dynamic Workflows
Auto-Memory, Auto-Skills, Hooks
Built-in Skills (/review, /batch, /loop, /bugfix…)
MCP, Plan Mode, LSP Integration
Auto Mode, Sandbox, Git Worktrees
Computer Use (desktop automation)
IDE Plugins (VS Code / JetBrains / Zed)
SDK
Headless Mode, Session Management
Open-source — model and framework
Multi-protocol (OpenAI / Anthropic / Gemini / Qwen + any provider)
Agent Arena (multi-model head-to-head on same task)
Daemon Mode — qwen serve (multi-client shared agent)
IM Channels (Telegram / DingTalk / WeChat / Feishu)

Ecosystem

  • Qwen Code Desktop — Official desktop app for macOS, Windows, and Linux

  • AionUi — A modern GUI for command-line AI tools including Qwen Code

  • Gemini CLI Desktop — A cross-platform desktop/web/mobile UI for Qwen Code

  • 🦞 Qwen Code Claw — Let other agents (Claude, Codex, etc.) delegate coding tasks to Qwen Code via ACP. Paste this prompt into your agent:

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.
  • Aliyun Model Studio CLI — Official CLI for Aliyun's AI platform (bailian-cli). Extends Qwen Code with image/video generation, knowledge retrieval, app orchestration, and model deployment

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Acknowledgments

This project was originally based on Google Gemini CLI v0.8.2. We gratefully acknowledge the Gemini CLI team's excellent work. Starting from Qwen Code v0.1, we stopped syncing with upstream and began independent development as a multi-protocol, multi-platform agent framework with deep integrations for Qwen models and beyond.