* 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>
|
||
|---|---|---|
| .github | ||
| .husky | ||
| .qwen | ||
| .vscode | ||
| docs | ||
| docs-site | ||
| eslint-rules | ||
| integration-tests | ||
| integrations/external-context | ||
| packages | ||
| patches | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .yamllint.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| esbuild.config.js | ||
| eslint.config.js | ||
| eslint.legacy-filenames.mjs | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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.
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.
