Commit graph

8399 commits

Author SHA1 Message Date
Zqc
8c90697ace
test(memory): Cover context refresh marker carry-over turns (#8809)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* test(memory): cover context refresh carry-over turns

* test(memory): pin continuation turns and dedupe carry-over test helpers

* test(memory): drop mockClear from shared carry-over marking helper

* test(memory): guard against unregistered onComplete in tool-write helper

---------

Co-authored-by: 俊良 <zzj542558@alibaba-inc.com>
2026-08-10 17:00:55 +00:00
qwen-code-ci-bot
bdb7e418ba
chore(release): v0.21.9 (#8886)
* chore(release): v0.21.9

* docs(changelog): sync for v0.21.9

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 15:27:45 +00:00
Shaojin Wen
9bbb11ac32
fix(review): make the posted review body readable (#8825)
* fix(review): make the posted review body readable

The posted body on a capped COMMENT joined every clause with spaces: on
#8388 that ran 31 unresolved existing Criticals and seven disclosures into
one paragraph, each entry restating the same truncation reason, every
comment id a bare number, and the Chinese fold duplicating the whole
untranslated wall.

- Render the COMMENT body as paragraphs: the short verdict sentences stay
  one opener paragraph, every disclosure gets its own (matching the
  REQUEST_CHANGES branch).
- Render the unresolved-Critical block as a Markdown list, grouping entries
  that share the exact reason after their first " — " so the reason is
  stated once over a sub-list of subjects. Nothing is dropped: every
  subject and every distinct reason still renders.
- Linkify "comment <id>" / "issue-level comment <id>" in those entries to
  their #discussion_r / #issuecomment anchors when the plan names the PR.
- The Chinese fold no longer duplicates the untranslated English list; it
  carries the count and points at the English half.
- budgetGapDisclosures: a parenthesized placeholder ("(none — all planned
  checks completed)") now reads as a placeholder — the leading "("
  defeated the leading-token match, and #8388's body disclosed it as a
  self-contradictory "Not explored to full depth" entry.

* fix(review): harden the readable review body per review feedback

- budget disclosures: the paren strip now drops only the bare
  placeholder and the completion idiom, so a real gap that merely
  starts with a placeholder token ("(none of the chunk-2 checks ran —
  the runner died)") survives instead of being silently dropped before
  the coverage gate; duplicate folding keys on the paren-stripped text
  so a gap restated with and without parens discloses once
- compose-review: one shared plan-identity rule for the bilingual
  recovery and the comment anchors (the two copies had diverged);
  anchors follow the plan's recorded GHE host with a relaxed id floor
  off github.com; cannot-tell entries flatten embedded newlines and a
  dangling " — " reads as reasonless instead of an empty group key
- tests pin all of the above, including the previously untested
  already-linked passthrough, the reasonless shape, and the bilingual
  fold count beyond N=1

* fix(review): keep genuine budget gaps and harden comment anchors

- budget disclosures: the completion idiom now requires an actual
  completion predicate the text ends with — a head word alone ("none —
  all 5 Windows checks failed to start") no longer reads as done — and
  the template/dash alternatives are end-anchored, so an inner text
  merely starting with "<…>" or a dash run keeps. One strict classifier
  judges the paren-stripped text, bare and wrapped alike, ending the
  divergent two-parser treatment of identical content ("none of the
  chunk-2 checks ran — the runner died" was kept wrapped, dropped bare).
- budget parse linearity: the placeholder classifier, the gap-line
  regex, and the trailing-punctuation strips no longer carry
  overlapping quantifiers — a token followed by a long whitespace run
  was measured quadratic (seconds at 40k spaces); every shape is now
  linear by construction and pinned by the linearity test.
- comment anchors: the effective host is normalized (case and the
  implicit :443) before the github.com short-id floor comparison;
  "issue-level" reads case-insensitively, matching the pipeline's own
  capitalized label; a plan ownerRepo carrying "."/".." segments is
  rejected before it rides into a posted URL.
- tests: the plan fixture carries the PR identity instead of four
  pasted read/modify/write backdate dances; the anchor suite is
  hermetic against an exported GH_HOST and now pins the host-fallback,
  host-rejection, ownerRepo-rejection, routed-host, prefixed-cannot-
  tell, and cased-Issue-level branches; the Chinese fold assertion
  covers the pointer, not just the count.

* fix(review): address round-3 review findings on gap classification and anchors

- End-anchor the stayed-under-budget idiom in PLACEHOLDER_GAP_RE: text
  continuing past "budget" discloses skipped work and must keep (R3-1).
- Require the completion word to be affirmed — a negated tail ("none
  completed", "nothing finished") is a failure report, not completion
  (R3-2) — and temper the completion spans so an exception quantifier
  ("all but X completed") keeps the gap (R3-3).
- Close the drop-side gaps: "everything" as a completion head, the
  found / to-report non-answer idioms, and inner paren padding (R3-5);
  pin the whole drop vocabulary and the new keep shapes (R3-6).
- Collapse multi-line cannot-tell entries by split/join and guard
  stripReviewFooter on the footer marker: both regex paths scanned
  quadratically on long whitespace runs in model-written entries with no
  length cap (R3-4; the footer strip measured ~20 s at 80k characters).
- Decide the anchor family per entry, not per match: an issue-level
  entry echoing pr-context's own header shape carries its id apart from
  the phrase, and adjacency-routing minted #discussion_r anchors in the
  wrong id space; link text now keeps the entry's casing (R3-8).
- Normalize www./trailing-dot/zero-padded-port variants of github.com
  before the short-id floor comparison (R3-9).
- Lift the dot-segment-aware owner/repo rule into lib/gh.ts as
  isOwnerRepo — submit's --repo check and the plan identity now share
  one copy (R3-10) — and revert cleanup.ts's OWNER_REPO_RE export.
- Pin the BUDGET_GAP_LINE_RE rewrite's linearity and bullet handling
  (R3-7), the corrupt-plan degradation (R3-11), and the numeric
  prNumber arm of the plan identity (R3-12).

* test(review): pin the gap-line linearity gate with a 160k-space probe

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 14:23:45 +00:00
qwen-code-dev-bot
fbccb3eac0
fix(cli): 'later' on provider update persists a cooldown instead of re-prompting every launch (#8829)
* fix(cli): 'later' on provider update persists a cooldown instead of re-prompting every launch

Choosing "Remind me later" (or pressing Esc, which maps to 'later')
previously persisted nothing, so the Built-in Provider Update prompt
reappeared on every startup whenever the built-in model list version
differed from the stored metadata version. Users upgrading to a
release that changes the Token Plan model list were nagged each launch.

Now 'later' persists postponedVersion + postponedAt. findAllPendingUpdates
skips re-prompting while the 24h cooldown is active for the same version.
A new model-list version (postponedVersion mismatch) still re-prompts
immediately, preserving the "remind me later" meaning. 'skip' (per-version
ignore) and 'update' behavior are unchanged.

* fix(cli): batch the 'later' cooldown persist and cover the read-side guard (#8829)

* fix(cli): expire the 'later' cooldown on backward clock jumps (#8829)

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
2026-08-10 14:22:49 +00:00
易良
b4f104fb6f
[webshell]retry session switch while closing (#8864)
* fix(webui): retry session load while closing

* fix(webui): settle closing-session retry races
2026-08-10 14:13:16 +00:00
Shaojin Wen
b2404836ba
fix(cli): give sandbox containers collision-proof names (#8880)
The regular-run container name was allocated by counting docker ps -a
and taking the first free index — a check-then-run window with nothing
holding the name between the count and `docker run --name`. That was
survivable while launches on a shared docker daemon were rare, but the
CI pool packs several runner registrations per host, and a dense
autofix scan launches sandboxes on sibling registrations within the
same second: in run 31389561905, SEVEN of fourteen legs lost the race —
each got `docker: Conflict. The container name "/qwen-code-0.21.8-0" is
already in use` (exit 125) one second into its agent step, burned a
round, and pushed items toward their round caps.

A random 8-hex suffix replaces the counter, the same scheme the
integration-test branch has always used. Nothing predicts the name:
run-agent.mjs parses it from the `ContainerName (regular):` line, and
the stale-container reaper matches on the image-name prefix, which the
suffix preserves. The docker ps call goes with the counter — one less
execSync on the launch path.

Co-authored-by: verify <verify@local>
2026-08-10 14:07:54 +00:00
Shaojin Wen
5dc98240c7
fix(ci): port the verify gate's remaining hardening from #8765 (#8878)
#8816's branch accidentally carried #8765's early commits, and the
takeover loop evolved the gate further there (subset identity via
comm -23, the retryable third arg, subset fixtures) — so #8765 closes
as subsumed, and this PR ports what main still lacks: the two
improvements its reviewers named for porting, plus the open round-6/7
findings that survive on main's gate.

- Pre-detach short-circuit: an empty head signature (vite/esbuild/
  crash — the KNOWN LIMIT class) fails closed regardless of the
  baseline, so decide it BEFORE paying the detach + full baseline
  re-run + restore.
- Build-dirt guard: the A/B'd build REWRITES a tracked file (the
  vscode companion settings schema), and the undiscarded rewrite makes
  either checkout refuse — degrading a real verdict into the crash
  path. `git restore -- .` before both checkouts; tracked-only, and
  the tree was asserted clean before the deterministic checks.
- Restore-failure semantics: a plain outcome=failed is an EVALUATED
  rejection — the watermark advances and a transient git failure
  strands the item as a permanent human handoff. The gate now leaves
  outcome unset (the gate-crashed path retries next scan) and still
  writes the detail document so the crash comment explains itself.
- The dist-rebuilt steering note seeds the repair feedback on both
  retryable A/B exits — the repair agent's only warning that dist/
  holds baseline-built artifacts.
- The stale-base retry handoff prefixes its embedded rejection with a
  the-base-has-moved note, so the retry agent is not steered toward
  no-action by framing written before the auto-update.
- The two A/B side logs joined the repair step's cleanup list.
- Tests: identity-less short-circuit, tracked-dirt survival,
  verdict-less restore crash, long-preamble render cap, PREEXISTING
  clause selection through the executable report harness, and the
  stale-framing note pin.

Mutation-tested, 5 of 5 caught: short-circuit dropped, restore guards
dropped, restore-failure reverted to the evaluated rejection, dist
note dropped, stale-framing note dropped.

Co-authored-by: verify <verify@local>
2026-08-10 13:58:27 +00:00
Shaojin Wen
95e17691a9
chore(serve): remove the /demo debug page (#8805)
* chore(serve): remove the /demo debug page

The daemon has shipped a real browser UI for a while: `resolveWebShellDir()`
finds the bundled Web Shell assets and `mountWebShellAssets()` serves them at
`/`, so `qwen serve` already opens onto a full client. `/demo` stayed behind as
a 663-line inline-HTML console covering the same ground with none of the
reach — nobody drives the daemon through it, and `npm run dev:daemon` starts
the Web Shell dev server rather than the demo page.

Keeping it around costs more than the dead code. It is the only file in the
tree that pairs an event log with daemon HTTP, so work that starts as a Web
Shell observation lands there instead: #8762 was found while running `/review`
through the Web Shell and was fixed entirely inside the demo page's rendering,
with "no Web Shell changes" in its own risk note. Deleting the page removes
that decoy.

Nothing is lost for protocol-level debugging: `GET /session/:id/events`
streams the same raw frames the Events tab printed.

`/health` shared `routes/health-demo.ts` with the demo handler, so the module
is now `routes/health.ts` / `createHealthRoutes()` and drops its `getPort`
dependency. The rate-limit exemption, the boot breadcrumb, and the daemon docs
lose their `/demo` arms; the loopback self-origin shim regression test already
asserted through `/health` and only needed its title corrected.

* test(serve): pin the removed /demo contract and the pre-auth surface

Review follow-up. Three of the removal hunks shipped ungated, and two doc
sentences the removal rewrote were describing the pre-auth surface wrong —
both before and after the edit.

Deleting the `/demo` route took its assertions with it, so nothing failed if
the handler came back: the Web Shell suite only exercised a generic deep link,
and the rate-limit exemption could be widened again with the suite still green.
`/demo` is now pinned as what it became — an ordinary unknown path: a
non-navigation request 404s, a browser navigation is answered by the SPA
fallback like any other deep link, and once a token is configured (with or
without `--require-auth`) that navigation is refused with 401, because the
fallback sits behind the bearer. The rate-limit test pins that `/health` is the
only exempt GET, so re-adding a second pre-auth page to the predicate fails
instead of silently escaping the limiter. Each new assertion was checked by
reverting the hunk it guards and confirming it goes red.

The `--allow-origin '*'` warning and both `--allow-origin` doc paragraphs
enumerated `/health` as the residual tokenless surface and said nothing about
the Web Shell static assets, which are mounted before the bearer in every
launch mode and stay reachable even under `--require-auth` — the enumeration
also claimed `/health` stays pre-auth on non-loopback binds, where it is
registered behind the bearer and 401s. A probe across all three launch modes
established the actual matrix; the warning and the docs now match it and name
`--no-web` as the way to remove the residual browser surface. The warning text
is asserted by a test for the first time.

* fix(serve): correct Web Shell doc claims and re-pin the pre-auth CORS wall

Review follow-up. The removal rewrote the daemon docs around the Web
Shell, and three of the rewritten claims did not match what the runtime
actually does: §1 never said how the bearer reaches the browser (with
auth on, the plain URL loads a shell whose every API call 401s), §8
called the shell writable on any bind (on a non-loopback bind without
`--allow-origin` its POSTs hit the CORS wall and 403), and §8 served
`/session/:id` without the document-navigation qualifier its own code
enforces. The §9 call-chain diagram also still listed the deleted
`/demo` route, the developer flag references had no `--web`/`--no-web`
row despite the new guidance pointing at the flag, and both design docs
listed the JSON body parser ahead of post-auth `/health` while
`createServeApp()` registers them the other way round.

The deleted `/demo` CORS test was also the only assertion that a
pre-auth page sits behind the Origin wall — every surviving Origin test
targets an API path. Re-pin it for the shell root so a mount-order
regression fails instead of exposing the pre-auth HTML surface
cross-origin.

* fix(serve): finish demo rename sweep and scope pre-auth shell claims to loopback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 13:31:15 +00:00
易良
4cf943219d
fix(cli): keep the selected model when refreshing another provider's template (#8868)
* fix(cli): keep the selected model when refreshing another provider's template

A provider template refresh reuses the first-time-install plan, which always
carries a default "select the first model" instruction. The refresh acted on it
whenever the current model was missing from the refreshed list — and a model
owned by a different provider is always missing from it, so accepting an update
silently rewrote model.name and cleared model.baseUrl.

Leave the selection alone when the current model is installed under the same
protocol but owned by another provider; a model that is not installed at all is
still migrated. Also stop reporting "Model switched to <old model>" when the
active model did not actually change.

Fixes #8863

* fix(cli): preserve auth and models on provider refresh
2026-08-10 12:41:09 +00:00
Shaojin Wen
7f61e03b82
perf(ci): make the triage budget operator-tunable and raise it (#8810)
* perf(ci): make the triage budget operator-tunable and raise it

The triage job's fixed 30-minute cap was killing every substantial PR's
triage at exactly the budget — five observed (8723, 8726, 8731, 8764,
8801), each a full agent run discarded — and the successful-run
distribution shows why: 22 jobs sampled ran median 5.8m / p90 22.3m /
max 29.5m. A max just under the cap is a distribution being clipped,
not one that fits.

Back the budget with QWEN_TRIAGE_TIMEOUT_MINUTES (fallback 60), the
same pattern as the autofix fleet caps in #8731 and probe-verified
there for expression-valued job fields. Triage is advisory: the worst
case a longer budget risks is one held runner, while a killed run
wastes the same runner and the work it was about to publish.

Pinned by a test asserting the variable-backed form with its fallback;
reverting to the fixed literal fails it.

* Address review: sanitize the knob, pin the active line

Both findings verified before fixing.

The knob was unvalidated, and the failure mode is nasty precisely
because the knob exists to be changed WITHOUT a PR: a malformed
repository variable — '60 minutes', '1h', a 6O typo, quotes, a decimal
— would make fromJSON throw at expression time, killing the triage job
before any step runs: no always() finalize, no status comment, a
silent stop whose run log blames fromJSON rather than the variable.
And '0' parses fine into an instantly-cancelled job. timeout-minutes
cannot be validated by the job that consumes it, so the authorize job
(which triage already needs, and which can run bash) sanitizes it:
bare-integer check, floor 10, ceiling 600, every rejection warning
with the VARIABLE's name. The triage job consumes the sanitized
output; || 60 covers the events where authorize is skipped.

The pin was an unanchored substring over raw YAML, satisfied by a
commented-out line while the job silently inherits GitHub's 360-minute
default — the review probe-verified that mutant surviving. The pin now
matches active lines only, and the sanitize step is replayed verbatim
against the review's whole enumeration: unset, sane, zero, runaway,
and five malformed shapes, each asserted to fall back and name the
knob.

Mutation-tested, 3 of 3 caught: the timeout line commented out, the
floor clamp dropped, the integer check bypassed.

* fix(ci): clamp over-long triage budgets before bash arithmetic (#8810)

A repository variable with more than 18 digits overflows bash's 64-bit
arithmetic, and the wrapped residue can land inside [10,600], silently
skipping both clamps with no warning — the one malformed shape where
the sanitizer's diagnostics go dark. Check the digit count before the
arithmetic and clamp such values to the ceiling with a warning.

Also pin the seams the replay test cannot see: which repository
variable feeds RAW, that the sanitize step is unconditional, the exact
clamp boundaries, the 10# decimal guard, and the overflow clamp.

* fix(ci): strip leading zeros from triage budget before the width guard (#8810)

* fix(ci): pin triage timeout seams on parsed YAML and runner shell flags (#8810)

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 12:40:21 +00:00
易良
c1a6b2daf5
fix(cli): require exact daemon marker before env scrub (#8811)
* fix(cli): require exact daemon marker before env scrub

* test(cli): isolate daemon marker cases
2026-08-10 12:29:34 +00:00
jinye
a292c89a3b
feat(cli): add background cleanup for OpenAI API logs (#8862)
* feat(cli): add background cleanup for OpenAI API logs

With model.enableOpenAILogging on, every API call appends a full
request/response JSON under logs/openai with no rotation — heavy usage
accumulates hundreds of thousands of files (tens of GB) within months.

Register a third cleaner in the existing background housekeeping
pipeline that sweeps openai-*.json files older than the new
model.openAILogRetentionDays setting (default 7 days). The
filename-embedded UTC date is used as a fast path to avoid one stat()
per file; the boundary day and unparseable names fall back to mtime.
Throttling is keyed on the resolved log dir, so both the default
per-CWD layout and a shared custom openAILoggingDir are swept at most
once a day. The sweep runs regardless of whether logging is currently
enabled, so residue from earlier debugging sessions still gets cleaned.

Scope note: housekeeping only starts for interactive sessions, so
headless (-p) / SDK processes are not covered yet.

* codex: address PR review feedback (#8862)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 12:20:33 +00:00
jinye
fa8cae5418
fix(serve): Allow approved external built-in text writes (#8852)
* fix(serve): allow approved external built-in text writes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): keep write provenance off startup bundle

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 12:19:58 +00:00
易良
4bc75c2bdb
fix(cli): list blocking background tasks in session-switch errors (#8742)
* fix(cli): list blocking background tasks in session-switch errors

When /clear, /resume, or /branch is blocked by running background work,
the error only said "stop your tasks" without naming them. Users who
forgot a long-running background shell (e.g. a dev server started a day
earlier) had no way to connect the block to a specific task unless they
already knew /tasks exists.

Enumerate the blocking entries (id, label, status, runtime) in the error
and point at /tasks or /workflows depending on which kinds block. The
gate itself is unchanged; the message falls back to the bare base text
if an entry settles between the gate check and enumeration.

* fix(cli): cap blocked-entry enumeration and trim redundant surface

Post-review polish for the #8741 change:
- drop the redundant count field (lines.length carries it)
- cap enumeration at 10 lines plus a "…and N more" overflow tail
- type the collected status with TaskStatus | WorkflowStatus
- collapse duplicated test assertions and narrating comments

* fix(cli): flatten newlines in blocking-entry labels

Review finding (security, P3): stripUnsafeCharacters preserves LF/CR,
so a multi-line label (heredoc shell commands, workflow meta from
files) could split one entry into forged extra lines inside the
blocked message. Flatten CR/LF to spaces when building each line.

* fix(cli): tighten blocking task details
2026-08-10 11:33:24 +00:00
Shaojin Wen
7c89665be1
fix(cli): extend the #8663 loader denylist and harden its scrub lifecycle (#8763)
* fix(cli): extend the #8663 loader denylist and harden its scrub lifecycle

Follow-up to #8663. Its inherited-env denylist closed the NODE_OPTIONS/
NODE_PATH class but left sibling code-execution and TLS-trust-anchor vars
that reach the same #8653 cross-workspace outcome — an untrusted workspace
`.env` is frozen into daemonRuntimeBaseEnv and distributed to every
workspace's session subprocesses.

Denylist additions, split by the PR's own tiering:

- Scrubbed loader tier (INHERITED_LOADER_ENV_KEYS — scrubbed from the
  inherited launch env and rejected from every `.env`/settings.env scope),
  for pure-injection vars with no legitimate operator-shell use:
  OPENSSL_CONF (startup dlopen of an attacker OpenSSL engine),
  NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module.

- Reject-from-project-`.env` tier (PROJECT_ENV_HARDCODED_EXCLUSIONS —
  rejected from project files, preserved from the shell / home `.env`), for
  vars with a legitimate operator-shell use whose only exposed vector is an
  untrusted project file:
  * TLS trust anchors SSL_CERT_FILE, SSL_CERT_DIR, CURL_CA_BUNDLE,
    REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO (siblings of NODE_EXTRA_CA_CERTS;
    an attacker CA MITMs a session's git/npm/pip/curl traffic).
  * git command-execution family GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF,
    GIT_CONFIG_GLOBAL/SYSTEM/COUNT and the numbered GIT_CONFIG_KEY_<n>/
    GIT_CONFIG_VALUE_<n> pairs (matched by prefix). core/utils/git-branches.ts
    already scrubs these from the repo's own git invocations.
  * node-gyp interpreter selection NODE_GYP_FORCE_PYTHON, npm_config_python,
    PYTHON (run as the build Python during native-addon installs).

Concurrency: the daemon's process.env scrub/restore and the loader-key
rejection reporter were process-global with no guard for concurrent embedded
daemons in one process (a documented supported config). The first daemon's
close() restored loader vars into the shared env, re-poisoning a still-live
sibling's sessions, and dropped its reporter. The scrub is now reference
counted (acquireInheritedLoaderEnvScrub — snapshot on first acquire, restore
only on last release) and the reporter is cleared only when still active.

Test hardening from the same review: pin the daemon-worker scrub breadcrumb
(not just key removal); pin the fast-path settings.env case-folded
hardcoded-exclusion gate; drain the module-global fast-path stash so the
accumulate assertion is order-independent. Docs updated for the new keys.

* fix(cli): keep the loader-scrub process.env access in the serve guard surface

The refcounted acquireInheritedLoaderEnvScrub read/wrote process.env from
config/shared-env-keys.ts, which the serve process.env guard does not scan —
moving the access out of run-qwen-serve.ts dropped its allowlisted count and
failed process-env-guard.test.ts. Pass the env into the coordinator instead so
run-qwen-serve.ts still owns the process.env reference (matching the existing
scrub helpers), and update the allowlist to the new count.

* fix(cli): block GIT_SSH and GIT_CONFIG_PARAMETERS in the project-env denylist

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): extend the project-env denylist across git exec, TLS, and rc-file tiers

Close the round-2 review findings: block the remaining git
command-execution siblings (GIT_EXEC_PATH, GIT_TEMPLATE_DIR, GIT_ASKPASS,
GIT_PROXY_COMMAND, GIT_EDITOR), the npm/pip TLS trust knobs
(npm_config_cafile, npm_config_ca, npm_config_strict_ssl, PIP_CERT,
GIT_SSL_CAPATH), and the curl/wget rc-file redirects (CURL_HOME, WGETRC)
from project .env files. Freeze the numbered GIT_CONFIG_KEY_/VALUE_ pairs
on reload together with GIT_CONFIG_COUNT, and sync the qwen-serve.md
loader-key enumeration with settings.md.

* fix(cli): harden the project-env denylist and nested scrub snapshot (#8763)

* fix(cli): merge the loader-env scrub snapshot into one pass (#8763)

acquireInheritedLoaderEnvScrub iterated process.env twice (a snapshot
pass, then the scrub); record the originals inside the scrub's single
pass instead. Drop the acquire-time snapshot clear, which the
release-time clear made unreachable defense, and add tests that kill
the previously surviving mutants on the release-time clear, the
test-only reset, and the undefined-value guard.

* fix(cli): block the round-4 exec-redirect env keys from project files (#8763)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 11:27:30 +00:00
顾盼
c0c0352e25
feat(cli): mirror Live Host releases through OSS (#8674)
* feat(cli): mirror Live Host releases through OSS

* fix(cli): harden Live Host OSS mirror fallback
2026-08-10 11:17:49 +00:00
Shaojin Wen
358091833b
fix(ci): watchdog silent sandbox hangs and reap the containers they leak (#8816)
* feat(ci): A/B deterministic gate rejections against the pre-round ref

A deterministic rejection in the autofix verification gate is only
chargeable to the round if the same check passes without the round's
commit. The gate charged every red to the fix unconditionally, and run
31276008548 measured what that costs when the premise is false: PR
8614's branch predated #8693's tsconfig guard while node_modules came
from the post-#8693 trusted base, so `npm run build` was equally red at
origin/<branch> — 63 minutes of accepted agent work discarded, an
18-minute repair burned on a failure the repair agent is forbidden to
touch (it may only amend the round's own fix), thirteen rounds in a
row, and the same again on the #8616 leg.

On rejection the gate now re-runs the failing check at origin/<branch>
(the branch as pushed, before the round) in the same environment:

- baseline green: today's path exactly — outcome=failed,
  retryable=true, the repair pass gets its chance.
- baseline red too: outcome=failed with preexisting=true and NO
  retryable. The repair step keys on retryable and is skipped — it
  cannot reach a failure outside the round's diff by construction —
  and gate-rejection.md says outright that the branch needs a base
  update (merge main), which flows into the failure comment as-is.

Fail-closed toward today's semantics: any A/B infrastructure problem
(missing ref, checkout failure) charges the fix as before, and a
restore failure after the baseline run rejects outright since the tree
can no longer be trusted. The round's work is still not pushed — this
changes the verdict's honesty and cost, not the push policy.

Tested by executing the real script in a real two-remote git repo with
an npm stub whose failures are keyed by commit SHA: round-caused red
(baseline green), pre-existing red (both red), and the untouched green
path. Mutation-tested, 3 of 3 caught: skipping the A/B, claiming
pre-existing without measuring, and dropping the tree restore.

* Address review: bound the A/B to checks it can honestly compare

All seven findings verified before fixing; the three Criticals were
each a way the A/B compared something other than the check that failed.

R1-1 — the contracts check feeds on stdin, which its first run drains;
the baseline leg re-ran against EOF and checked an empty file list.
R1-3 — the schema check's verdict rides on packages/core/dist, which
the core-rebuild guard built from ROUND sources and which, being
gitignored, survives the detach. Both checks are now A/B-exempt
(run_check_no_ab): their baseline verdicts prove nothing, and their
rejections stay where the repair agent can actually act on them.

R1-2 — a workspace the round ADDS does not exist at the baseline, and
npm exits 1 there with "No workspaces found" (measured; --if-present
forgives a missing script, not a missing workspace) — a round-caused
failure misread as pre-existing, skipping the one repair that can fix
the round's own package. The per-package loop now A/Bs only when the
workspace exists at origin/<branch>.

R1-4 — a chatty PASSING baseline used to flood the tail -c 3000
evidence window and push the actual failure text out of
gate-rejection.md, the sole carrier into the repair feedback, the PR
comment, and the next round's LAST_REJECTION. The baseline transcript
now goes to a side log and only a FAILING tail is merged back, where it
is the evidence.

R1-5 — the pre-existing paragraph pushed gate-rejection.md past the
report's head -c 3500 cap, truncating the closing fence for branch
names past 44 characters. Cap raised to 3900, invariant comment
updated with the new arithmetic.

R1-6 — preexisting=true had no read site. It now flows verify →
Finalize verification → the failure report, whose headline swaps the
generic gate clause for "PRE-EXISTING failure … needs a base update
(merge main)".

R1-7 — the no-round-commit guard was unpinned (deleting it kept all
tests green). Now exercised through the core-rebuild path, the one
A/B-eligible check that runs before the commit gate.

Four new behavioral scenarios (chatty baseline, no-commit round,
A/B-exempt checks, round-added workspace) plus workflow pins for the
forwarding, the clause, and the cap. Mutation-tested, 4 of 4 caught:
schema back to A/B (3 tests), guard dropped, side log reverted,
no-commit guard dropped.

* Address review round 2: A/B only what it can prove, prove what it claims

Ten findings across two rounds, each verified before fixing. The three
deepest share one lesson: the A/B is only sound for a check whose
inputs travel entirely with the git ref, and whose failure it can
IDENTIFY, not merely observe.

R2-1 — rc=1 at both legs does not make them the same failure: the
branch can fail for reason A while the round fails for reason B, and a
baseline infrastructure hiccup is a nonzero exit too. Pre-existing now
requires a MATCHING failure identity — tsc diagnostics normalized to
file + error code (positions shift with the round's edits), compared
via comm(1) on a per-check transcript. No diagnostics on either side
means identity cannot be established and the round stays charged.

R2-2 / R2-7 — gitignored dist survives the detach carrying the ROUND's
build, so any dist-consuming check A/Bs reverted sources against
round-built artifacts: package tests (channel-base resolved through
dist exports) and typecheck (sdk-typescript resolves core's d.ts —
probe-verified three-arm flip). Both are now A/B-exempt, as is lint,
leaving `npm run build` — the incident class, and the one check that
rebuilds its own inputs from the checked-out sources — as the sole A/B
candidate. The workspace-existence guard dissolves with it.

R2-3 — the fixture inherited the caller's global git config; a failing
global pre-commit hook broke all seven cases. The harness now isolates
GIT_CONFIG_GLOBAL/SYSTEM for every git child, and the suite is proven
green under a deliberately hostile hooksPath.

R2-4 — Finalize verification now selects preexisting from the same
attempt whose outcome it selects (repair verification included).

R2-5 / R2-8 — the "merge main" advice is now conditional at both
layers: the script paragraph states the measured fact and hedges the
remedy; the report headline uses the compare the step already ran —
behind/diverged gets the base-update clause, an up-to-date branch is
told its own pre-round code needs attention.

R2-6 — the rejection document now sizes its evidence tail against its
preamble (floor 500 bytes, total under the 3900-byte render cap), so
the closing fence can no longer be truncated off by a long branch name.

R2-9 — dissolved by R2-2: package tests no longer A/B, the guard and
its uncovered positive branch are gone.

R2-10 — the baseline-evidence merge is now pinned: the pre-existing
scenario asserts the baseline leg's own failure line (keyed by its SHA)
reaches gate-rejection.md.

Eight behavioral scenarios; mutation-tested 5 of 5: identity dropped,
typecheck re-enrolled, package tests re-enrolled, evidence merge
dropped, fixed tail restored.

* Address review round 4: sharpen identity, stage the git failures, sync prose

Nine findings, all refinements — the design held, the edges did not.

Identity now keeps the diagnostic MESSAGE (file + code collide: two
unrelated TS2339s in one file compared equal, skipping a repair that
could have shipped — probe-reproduced by the review), and the fixture
emits a SHIFTED position on the baseline leg so the position strip is
load-bearing instead of decorative (deleting the sed survived every
test before; it fails one now). vite/esbuild failures still yield an
empty signature by design — documented as the fail-closed limit rather
than half-widened.

The fail_signature assignments take `|| true`: grep exits 1 on the
normal no-match case and survives errexit today only because the caller
sits in an if-condition — a future unconditional call site would crash
the gate verdict-less.

The restore-failure branch is now stageable and staged: the baseline
leg recreates (untracked) a file the branch tracks, the checkout back
refuses, and the test pins retryable-not-preexisting with the
'could not restore' label. Relaxing the branch to `|| true` fails it.

Prose synced to the mechanisms that replaced it: the render-cap
invariant restates against the dynamic tail budget (the old 3000-based
arithmetic would misguide the next retune), the no-round-commit guard
comment names the core rebuild (schema/contracts left the A/B last
round), the describe wording counts both A/B-eligible builds, and the
pre-existing clauses no longer claim "the repair pass was skipped" —
with REPAIR_PREEXISTING forwarded, repair may have RUN; they now state
the invariant that is true either way: repair may only amend the
round's own fix, so it cannot reach this failure.

Mutation-tested, 3 of 3 caught: position strip dropped, message dropped
from the identity, restore rejection relaxed.

* fix(ci): watchdog silent sandbox hangs and reap the containers they leak

Four autofix rounds have died the same way (#8663 twice, #8761 r3,
#8763 r4): the agent's last output is the sandbox wrapper's
"ContainerName (regular): …" line at docker container entry, then
nothing — not one event — until the 2-hour absolute budget kills the
round. Four different runners, two image versions: systemic, not a bad
machine. Where exactly the container wedges is still unknown (that
needs docker state on the runner); what is certain from the logs is the
shape — a wedged sandbox produces NOTHING, and a legitimate run is
never silent for long (the fleet's longest tolerated quiet is the
review pipeline's 10-minute stream-idle window for thinking phases).

Two mitigations, each aimed at a measured half of the damage:

- run-agent.mjs gains an idle watchdog (QWEN_IDLE_TIMEOUT_MS, default
  20 minutes = 2x that longest legitimate silence): zero output for the
  window kills the agent with a distinct "idle-timeout … the sandbox
  likely hung at startup" detail, so the failure comment names the
  right knob and a hung round costs 20 minutes instead of 120. Polled,
  not reset-per-chunk — a busy stream should not spend its time
  re-arming timers.

- Both sandboxed jobs reap stale qwen-code-* containers at job start:
  a budget kill reaps the HOST-side docker client, not the container,
  so every killed sandbox keeps running on the persistent runner —
  observed directly when a later leg's container-name counter found
  qwen-code-0.21.8-0 already occupied and picked -1. One job per runner
  at a time makes any container alive at job start stale by definition.

Tested by executing the real run-agent.mjs end to end with stub agents:
the hang shape (one line, then silence) dies at the idle window naming
the idle limit, and a slow-but-talking agent that outputs every 400ms
across a 1500ms window survives to a clean exit — the test that
distinguishes a watchdog from a disguised absolute timer. Mutation-
tested, 3 of 3 caught: watchdog disabled, last-output tracking dropped
(the disguised-timer regression), cleanup dropped from a job.

* Address review round 5: the gate's verdict defects and the reaper's live kill

Budget-warning round — the five Criticals from both reviewers, no
suggestions (each deferred with a recorded reply).

fail_signature: `[^\n]*` in an ERE bracket expression does not mean
"rest of line" — in POSIX bracket expressions `\` is literal, so it
matched "neither backslash nor the letter n" and truncated every tsc
message at its first n. Nearly every real message has an early n
("Cannot find name", "is not assignable"), so distinct same-file
failures collapsed into identical signatures and a round-caused failure
could be labeled pre-existing, skipping the repair. grep is
line-oriented: `.*` is exactly the rest of the line. New fixture: two
messages differing only after their first n.

Pre-existing verdict: the intersection test mislabeled in both
directions. A round that ADDS a diagnostic sharing one normalized line
with the baseline was called pre-existing (repair skipped for a
round-caused, repairable failure); and `comm -12 | grep -q` under
`set -eo pipefail` SIGPIPEs comm (exit 141) once the shared output
outruns the pipe buffer, charging true pre-existing failures to the
round — the exact 18-minute repair waste the gate exists to kill.
Pre-existing now means the round's failing set is a SUBSET of the
baseline's, and the difference is captured before testing. New fixture:
a round adding a second diagnostic to a failing baseline.

Restore failure after the baseline leg: was retryable=true with HEAD
still detached at the baseline commit — the repair agent works in that
very checkout and does no git recovery, so its commit would land on the
baseline and be orphaned. Now rejected non-retryable (reject_fix grows
a third arg); the next round starts clean from the trusted checkout.
The restoreClash test pins the new semantics.

Stale-container reap: the premise "a runner runs one job at a time, so
any live qwen-code-* container is stale" holds per runner registration,
but the filter queries the docker daemon, which is per host — and this
pool runs several registrations on one OS. With per-issue/PR
serialization only, a concurrent job's sandbox is a substring match
away from `docker rm -f`. The reap now takes only provably-dead
containers (--filter status=exited/dead, both jobs) and the comment
says why a running one is left alone.

Preamble printf: the `\`` escapes sat inside a single-quoted format
where backslash is literal, so every pre-existing rejection rendered
raw backticks instead of code spans (shellcheck SC2016). Backticks
need no escaping there. Also syncs the side-log comment to the dynamic
tail_budget it actually renders.

Verified: scripts suite 140/140 (was 138; the two new fixtures and the
rewritten restoreClash test all fail against the pre-fix script),
npm run build / typecheck / lint pass, bash -n clean.

* Address review round 6: reap the kill's own orphan, tolerate the reaper

* Address review: hang-bound the reaper, unblock the kill path, pin the unpinned arms

- Wrap every docker call in the stale-container reap with timeout 30: an
  alive-but-wedged daemon blocks docker ps indefinitely, and the existing
  || guards only catch nonzero exits, not hangs (R3-1).
- Make the kill-path container removal async in run-agent.mjs: the
  spawnSync blocked the event loop between SIGTERM and the 10s SIGKILL
  backstop for up to its 30s timeout — in exactly the wedged-daemon
  scenario the watchdog exists for. The main flow awaits the removal so
  the leak warning stays deterministic (R3-6).
- Split the pre-existing gate clause for an empty CMP_R: a transient
  compare-API failure is "never measured", not "measured not-behind", and
  must not assert the branch's own code is at fault (R3-7).
- Swap the timeout breaker's closing remedy to the sandbox investigation
  when every counted timeout was idle, mirroring the round-level split
  (R3-11).
- Tests: pin the budget kill path separately from the idle kill path
  (R3-3), parameterize the idle-window parse guard over -1/0/NaN (R3-5),
  add a stderr-only liveness case (R3-12), pin the strict-subset A/B arm
  via a baseline-superset fixture knob (R3-15), and pin the breaker's
  current-round idle increment (R3-18).

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 11:00:11 +00:00
Shaojin Wen
8b056d8f81
chore(deps): sync external-context version in package-lock.json (#8858)
* chore(deps): sync external-context version in package-lock.json

The lockfile workspace entry still listed 0.21.8 while
integrations/external-context/package.json is at 0.20.1, so every
npm install rewrote the lockfile. Regenerated with
npm install --package-lock-only.

* fix(release): include external-context manifest in version commit

scripts/version.js bumps @qwen-code/external-context with every core
release and the follow-up `npm install --ignore-scripts` writes that
bump into package-lock.json, but the release version commit's `git add`
allowlist does not cover integrations/external-context/package.json.
The manifest bump is discarded while the lockfile bump is committed,
recreating the drift this PR cleans up after every release (seen in
v0.21.8, commit 4d6246bd88). Stage the manifest so both change
together.
2026-08-10 10:58:08 +00:00
Shaojin Wen
77bd04bd61
fix(acp-bridge): bound live journal replay chunks (#8801)
* fix(acp-bridge): bound live journal replay chunks

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): isolate shell retention sidecars

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(integration): cover aggregated live journal replay

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(core): isolate registry sidecars

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp-bridge): keep unmodeled chunk keys out of live journal merges

The merged live-journal entry is rebuilt by spread-merging the first and
last source events, which was only safe because producers happen to emit
exactly {sessionUpdate, content, _meta?} on mergeable chunks. Gate the
merge on that key set so unmodeled data/update fields keep entries
discrete instead of leaking into the aggregate. Also clarify the
live-journal truncation marker: its retained/truncated counts describe
source events, while the limits count replay entries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp-bridge): align replay boundaries for discrete and meta-shaped chunks

Turn compaction folded discrete thought chunks (and non-todo-stop-guard
discrete messages) into one text slot with the last chunk's meta, while
the live journal keeps every discrete chunk separate — resyncing from
compactedReplay mis-attributed text across background tasks. Guard both
chunk paths with the same hasDiscreteMessageMeta predicate the live
journal already uses. Also align the merge gate with the shapes the
shared meta builder emits: tolerate update-level timestamp/
serverTimestamp and qwenTranscript.planToolCallId, and treat an
empty-string parentToolCallId as top-level the way the extractor does.
Document that byte-cap truncation drops whole entries, so the retained
tail can be much smaller than the cap, and tighten the integration
assertion that became vacuous once entries merge source chunks.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp-bridge): merge subagent chunks in live journal replay

SubAgentTracker stamps every streamed subagent fragment with
{ parentToolCallId, subagentType }, but the live-journal merge gate
only modeled parentToolCallId, so subagent chunks stayed discrete and
a high-fragment subagent stream could still trip history_truncated.
Model subagentType as a carried label (like the completed-turn path,
which merges by parentToolCallId alone) and cover the producer wire
shape in the merge tests.

* fix(acp-bridge): preserve TextContent metadata in live journal replay (#8801)

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 09:52:17 +00:00
Shaojin Wen
0d56e50b64
fix(core): deflake the shell-registry fixtures, and share the display-strip helper (#8795)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
* test(core): give shell-registry fixtures their own output paths

The retention-cap tests time out at 15s on shared CI runners while passing
everywhere else, and it is not load. Every settle writes a status sidecar
next to the entry's outputPath, and the fixture's default pointed all of
them at one fixed path: `/tmp/s1.output`, so `/tmp/s1.status` on every
machine that ever ran this suite. On a sticky-bit /tmp the file can already
belong to another user, and then the atomic write's rename answers EPERM
and retries with a blocking exponential backoff — 50, 100, 200ms — before
giving up. That is ~350ms per settle, measured at 362ms; the cap tests
settle 34 entries each, which is 12.3s of a 15s budget before the
assertions even run.

A/B on one machine with that exact shape (an unrenameable file planted at
the shared path): the old fixture reports `Test timed out in 15000ms`
three times over, the new one passes 57/57 in 778ms.

The fix is the fixture, not the retry policy: the backoff protects real
writes against transient EPERM and should stay. Entries now get output
paths under a per-test temp directory keyed by shellId, which the existing
afterEach already cleans, so no two entries — and no two jobs — share a
sidecar. The two tests that hand-wrote `/tmp` paths to exercise `&` and
control characters in a basename keep exactly that subject and move their
directory too.

Seen on feat/daemon-git-worktree-guard and feat/review-capture-tui, four
tests apiece, same describe block, with the file taking 120s.

* test(core): expect the notification path through production's own escape

The rebuilt <output-file> expectations computed their value by hand — a
single-occurrence String.replace, and none at all in the control-character
test — while production emits escapeXml(stripDisplayControlChars(path)),
which escapes all five XML metacharacters globally. So the expectations
were only correct while os.tmpdir() itself contained none of & < > " '.

That is the same "passes here, fails there" dependence this PR exists to
remove, in a new form and introduced by its own first commit. Probe: under
TMPDIR=/tmp/qcprobe/o&brien and .../o'brien, three tests fail with
production behaving correctly. Routing every expectation through escapeXml
makes all three environments agree — 57/57 under a plain TMPDIR and under
both poisoned ones.

Includes the pre-existing assertion in 'emits one task-notification when a
shell completes', which carried the same latent dependence and now goes
through the same pipeline.

* test(core): compose the notification path instead of re-deriving it

Third time on the same class, so this fixes the class rather than the case.
Production renders <output-file> as escapeXml(stripDisplayControlChars(p)).
My first pass hand-wrote the escape (single-occurrence, so only the first
`&`), the second added escapeXml but not the strip, and each was correct
only while os.tmpdir() happened to hold nothing the pipeline transforms — a
TMPDIR under a control character, a C1 byte, or a second \x03 failed the
assertion with production behaving correctly.

The expectations now call the real functions in the real order through one
helper, so they cannot drift from production at all. Verified 57/57 under a
clean TMPDIR, one containing \x01, and one containing `&`; dropping either
half of the composition turns three tests red under the matching hostile
TMPDIR.

(Noted while here, not changed: the registry keeps a private copy of
stripDisplayControlChars byte-identical to the exported one in
terminalSafe.ts. Out of scope for a deflake, but worth a look.)

* fix(core): use the shared stripDisplayControlChars in the shell registry

The registry kept a private stripDisplayControlChars that predates #4358:
that change added the bidi override/isolate ranges (U+202A-U+202E,
U+2066-U+2069) to the shared terminalSafe version and migrated
monitorRegistry onto it, but left this copy behind. The copies have
diverged ever since, even though terminalSafe's contract says both
background notification surfaces must apply the same defense.

The divergence bit this PR: the deflake's rebuilt <output-file>
expectations compose escapeXml(stripDisplayControlChars(...)) from the
shared utils — "exactly what production composes" was true for every
input except bidi codepoints, one more pass-here-fail-there dependence
in the class this PR exists to remove (and the earlier "byte-identical
copies" note was wrong: the shared one strips two more ranges).

Import the shared function and delete the stale copy. Notification
rendering (cwd, command label, result, output-file) now strips bidi
override/isolate characters — the documented Trojan-Source defense the
shared function exists for — and the test's composition can no longer
drift from production on any input. Net -20 lines.

* test(core): pin escapeXml's GLOBAL replace, not just its five characters

The composed oracle this PR introduced cannot discriminate a mutation in
escapeXml — it computes the expectation with the same function under test,
so both sides move together by construction. The literal case in
xml.test.ts is where that property has to live, and it carried exactly one
`&`: the other four metacharacters appeared twice and were pinned globally,
`&` was not.

Measured: mutating `.replace(/&/g, '&amp;')` to `.replace('&', '&amp;')`
ships green across the whole package — 19,546 tests passed. A real path
with two of them (a TMPDIR under `o&brien` with a basename like
`out&err.log`, the very case this PR's own history is about) would then
render a raw `&` into a model-facing XML envelope with nothing failing.

The case now carries two `&`, and the mutant turns it red.

* test(core): pin the bidi stripping the shared helper brought with it

The swap to the shared stripDisplayControlChars is NOT behaviour-preserving,
and I said it was. The shared helper carries two lines the registry's own
copy did not — U+202A-202E and U+2066-2069 — so it strips bidi overrides as
well as C0/C1, and that changes what five notification paths render. My
"byte-identical" claim came from comparing the first dozen lines and
stopping where they matched.

Measured: `/tmp/a<RLO>evil<PDI>/out.log` renders unchanged through the old
copy and as `/tmp/aevil/out.log` through the shared one. Those characters
reorder how a path DISPLAYS without changing its bytes, so leaving them in
a model-facing envelope is a spoofing surface; the stronger behaviour is
the one to keep.

Kept, then, but no longer undocumented: a registry-level test pins the bidi
stripping in the notification, and reverting to a C0/C1-only helper turns it
red. The PR title and body say what actually changes.

* fix(core): strip bidi overrides from the output tail too

The consolidation stopped one surface short, and my own test hid it: it
asserts modelText-wide absence of U+202E, which reads as whole-envelope
coverage, but its fixture shell has no output file — so <output-tail>
renders the canned unreadable form and was never exercised. The tail is the
LARGEST attacker-controllable field in the envelope, up to 8 KiB of a
background shell's own output, and it renders through a different helper
that stripped C0/C1 and passed bidi through verbatim. Probe-verified before
the fix: a shell whose output contains U+202E and U+2069 puts both into
<output-tail> unchanged.

Fixed in that helper rather than by swapping in the shared one, because the
tail must preserve newlines and carriage returns that the display helper
strips. Pinned by a test whose shell actually writes an output file — the
gap the previous pin had — asserting both that the overrides are gone and
that the line structure survives. Reverting the two ranges turns it red.

This is the Trojan-Source class (CVE-2021-42574): the characters reorder
how surrounding text reads without changing a byte, which in a model-facing
envelope means the model can be shown something other than what ran.

* refactor(core): give the bidi range set one home and pin all nine codepoints

* test(core): pin bidi stripping at the failed shell's <result> site

* fix(core): strip bidi overrides from the monitor's streaming <result> (#8795)
2026-08-10 07:57:39 +00:00
jinye
5855553428
fix(webui): Fence stale session work by attachment identity (#8833)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 07:55:18 +00:00
신동석
feea80644e
docs(readme): add Korean to the documentation language bar (#8836)
The Korean docs went live with QwenLM/qwen-code-docs#212, so
/ko/users/overview now resolves. Until now the language bar could not
list Korean because that path 404ed.

Uses the native name 한국어, matching the other entries, and appends it
last so the existing order is untouched.
2026-08-10 07:34:11 +00:00
Shaojin Wen
9aec40f2d2
perf(review): cap the reverse audit and shed Agent 8 on a huge diff (#8773)
* perf(review): cap the reverse audit at one round below the sweep floor

The sweep's rationale, extended to the high tier's second pass: below
~25 effective lines a diff fits in one view, and a second reader of the
same few hunks is the same reader. The reverse audit is that second
reader run as a LOOP — to two consecutive dry audits per chunk — and on
a micro diff the loop re-reads the same lines round after round.
Measured on a 23-line one-file PR: three rounds, eleven minutes, and
the single finding round 2 produced was verifier-rejected.

The plan's budget gains reverseAuditRounds (5, or 1 below the sweep
floor; never 0 — one round IS the second look, the budget must not
scale a dimension away). Under a cap of 1 a single substantive dry
audit — or a retroactively-dry round — is the certificate, final, with
no cold check: the retirement scheduler retires on one dry audit and
reads retirement from round 2, so an all-dry round 1 exits 5 CONVERGED
at the round-2 build. A hot chunk at the cap gets a deterministic
ROUND CAP refusal from the admission gate (exit 4, the deadline gate's
termination contract, naming the unreviewedDimensions entry only when
scope is outstanding) — the cap is enforced by the builder, not by the
orchestrator counting rounds. An older or garbled plan reads as the
full cap: more auditing, never less.

* perf(review): cap the reverse audit and shed Agent 8 on a huge diff

Refocuses this PR from the micro-diff cap (which the review showed was
mis-built: micro diffs run 3A, but the cap-1 machinery lived on the 3B
path they never take, and its threshold never fired on the motivating
case) onto the huge end, which is where the six-hour timeouts actually
are.

A timeout survey found 26 review-pr jobs dying in one recent window —
~122 hours of compute, zero posted, several the same PR retried. The
wall clock is model inference (82-88% inside subagents, ~81% of that
model turns), so on a 4,000-5,300-line PR the driver is sheer volume:
dozens of agents reading the diff, then a reverse-audit loop whose every
round re-reads it against a growing findings list (~90 min a round; five
rounds alone exceed the ceiling).

The elastic budget answers this in the band where the review otherwise
posts nothing: reverseAuditRounds drops 5 -> 3 for a huge diff
(effective >= 3000; three is the smallest loop two-consecutive-dry still
converges), and specialistCap sheds Agent 8 to 0 there (a whole-diff
pass on top of the base fan-out is the marginal cost that tips a too-big
review over the wall). Neither drops a required dimension. The ROUND CAP
refusal now writes a marker compose-review caps the verdict on, so a
non-converged stop discloses like a budget stop rather than resting on
the orchestrator's relay.

Removing the cap-1 tier resolves the review's two Criticals and
collapses the retirement scheduler back to its clean two-dry logic; the
duplicated plan-cap read is gone (agent-prompt reads the parsed report,
retirement no longer needs the cap at all). Tests pin the huge/normal
boundary, the effective-vs-source split, round-past-cap enforcement at
both 3 and 5, and the marker round-trip.

* refactor(review): tidy round-cap prose left from the earlier iteration

- Drop the always-plural ternary in the retirement note: dryRounds is a
  two-tuple again, so the singular branch is dead prose.
- SKILL.md's merge bullet: the loop's bound is the plan's round cap (5, or
  3 for a huge diff), not a flat 5-round cap.
- Re-wrap the overlong deadline.ts header line.

* fix(review): address reverse-audit round-cap review feedback

- Floor `reverseAuditRoundCap` at the huge-diff cap of 3: an out-of-band
  1 or 2 reads as the full cap, never fewer rounds.
- Delete the dead `REVERSE_AUDIT_MAX_ROUNDS` re-export (no consumers) and
  point the retirement cold-check comment at the plan cap.
- Correct the cap-3 rationale everywhere it was mechanically wrong: three
  is one audit round above the convergence floor of two (the all-dry
  rounds-1-and-2 shape converges under any cap of two or more, since the
  convergence check runs before the cap gate), not the smallest converging
  loop — in budget.ts, budget.test.ts, DESIGN.md and SKILL.md.
- Define `effective` in the SKILL.md budget bullet and make the round-5
  narration cap-agnostic; scope the retroactive-dry example to the cap-5
  shape.
- Add tests for the cap-3 retirement certificate, the per-chunk and
  chunkless round-cap gates, the undefined-round marker, and the
  specialistCap effective-vs-src dependence.

---------

Co-authored-by: verify <verify@local>
2026-08-10 07:28:36 +00:00
易良
15f54145be
fix(desktop): restore the macOS window after closing it (#8802)
* fix(desktop): restore macOS window on reopen

* fix(desktop): avoid stealing focus on reopen

* fix(desktop): exit fullscreen before hiding window

* fix(desktop): cancel pending hide on reopen

* fix(desktop): cancel pending hide on refocus

* test(desktop): cover should_restore_main_window truth table

* fix(desktop): close fullscreen-hide races and tidy atomics

- Run the pending hide on the main thread via run_on_main_thread so that
  a Dock/Finder reopen event serializes with the queued hide and cannot
  lose the race around the two-second fullscreen-exit boundary.

- Clear the pending flag when set_fullscreen(false) fails so that a
  failed fullscreen exit does not leave the window hidden.

- Use Release/Acquire/AcqRel ordering for the FULLSCREEN_HIDE_PENDING
  flag (cross-thread / cross-event-loop synchronization).

- Gate FULLSCREEN_HIDE_PENDING and its focus_main_window store behind
  #[cfg(target_os = "macos")] so non-mac builds carry no dead state.

* fix(desktop): isolate delayed fullscreen hides
2026-08-10 07:19:33 +00:00
Nothing Chan
2e3d297eee
fix(cli): respect trusted env boundaries (#8643) (#8706)
Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
2026-08-10 06:37:11 +00:00
Heyang Wang
e46586782c
feat: support drag and drop img in web-shell (#8696)
* feat(web-shell): support image drag and drop

Allow Web Shell composers to ingest image files reliably while
preserving the existing multimodal prompt protocol.

- Share ordered image ingestion across desktop and mobile editors
- Support image-only prompts and BMP preview and provider-safe handling
- Preserve queued payloads across retries and uncertain outcomes
- Add lifecycle guards, user feedback, unit coverage, and browser tests

* fix(web-shell): harden image prompt admission recovery

Preserve complete prompt payloads and prevent duplicate or uncertain
delivery states when admission responses race with queue lifecycle
events.

- Correlate admission, queue, and terminal events by prompt ID
- Restore images and input annotations across retry and edit flows
- Bound image reader concurrency and encoded attachment memory
- Reconcile confirmed removals and explain ambiguous queue entries

* docs(web-shell): align image drag design with review fixes

Document the reviewed admission, recovery, and resource invariants.
Keep the design aligned with the hardened Web Shell implementation.

- Record bounded image ingestion and encoded-data budgeting
- Clarify prompt lifecycle correlation and confirmed removal behavior
- Describe annotation restoration and internal action boundaries
- Update focused validation evidence and acceptance criteria

* fix(web-shell): avoid duplicate restored attachments

Skip payload attachments when restoring text is a no-op because the
same prompt text already exists in the composer.

- Restore images and annotations only when their text is inserted
- Preserve image-only restoration regardless of the current draft
- Add regression coverage for duplicate text with attachments

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
2026-08-10 05:48:17 +00:00
易良
57e6c31030
fix(web-shell): gate the floating todo entry behind the session workflow setting (#8828) 2026-08-10 05:39:07 +00:00
Shaojin Wen
e60d182241
perf(ci): tighten the automatic review kill switch for micro diffs (#8774)
* perf(ci): tighten the automatic review kill switch for micro diffs

Below the review skill's sweep floor (25 changed lines) an automatic
review keeps --effort high and its inline comments — a medium downgrade
would drop exactly the inline findings a source fix deserves, and with
the file-scoped suites and the one-round reverse audit the pipeline
itself is what shrinks on a micro diff. What a micro run must not keep
is a multi-hour kill switch: the small-PR budget halves with the same
90-minute floor the docs-only downgrade uses, so a hung run dies at the
scale of its work.

Reuses the PR_SIZE_LINES the size-aware budget already fetched — no new
API call; an unknown size never tightens, an explicit --timeout wins
(the size block is skipped), and a docs-only run is already halved,
never twice. Gate tests drive the extracted step source with seeded
sizes: the 24/25 boundary, the floor, the docs-only interaction, and
the unknown-size fallback.

* fix(ci): share one halve-with-floor implementation and pin the guard states

Review rework. The docs-only branch and the micro tightening now call
one halve_budget_floor() — a / 2 → / 3 mutant survived every test
because both micro inputs land on the floor under any divisor ≥ 2, and
two verbatim copies let a one-sided edit diverge the branches while the
comments claimed they matched; the floor cases now execute the shared
function and a structural pin asserts one definition, two calls, one
occurrence of the arithmetic. The threshold comment states the unit
honestly (total churn rides the skill's source-weighted sweep floor in
the direction that cannot over-tighten). Two surviving guard mutants
get pins: a manually requested review with a populated size is never
tightened (the caller owns its timeout), and a failed docs
classification still tightens a micro automatic run (the guard keys on
!= "true", not = "false").

* fix(ci): state the micro-tightening's true justification, not a false invariant

The comment claimed "total churn < 25 implies the skill's source-weighted
measure < 25", which is backwards: srcDiffLines counts raw unified-diff
lines (file/hunk headers, context), so it is LARGER than churn — a
scattered micro diff (churn 10 across 5 files) computes srcDiffLines ~65
and keeps sweep on, un-shrunk, while the gate still halves its budget.

The threshold is now stated as what it is: an independent "small PR"
churn bound, deliberately not SWEEP_FLOOR (the two measures differ, so a
micro diff may still run the sweep and the full reverse audit). The
tightening is justified by "churn < 25 bounds the reviewed territory and
90 minutes is ample for it even on the full pipeline" — measured, a
23-line PR runs high end to end in ~30 min — not by the pipeline
shrinking. With the SWEEP_FLOOR coupling claim removed, there is no
cross-file coupling left to drift, so no equality pin is owed.

* fix(ci): reword the micro test comment to the independent churn bound

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): reword the micro gate comment to the 180-minute budget it actually halves

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-10 04:01:36 +00:00
Shaojin Wen
27216eb4ca
feat(web-shell): show context usage as a mini progress pill in the status bar (#8794)
* feat(web-shell): show context usage as a mini progress pill in the status bar

Replace the plain "X% context used" text in the status bar with a compact
pill: a 52px progress bar plus the bare percentage. The fill follows the
same thresholds as the /context panel (>60% warning, >80% error), the fill
width caps at 100% while the number keeps reporting overflow, and the full
wording moves to aria-label so the accessible name is unchanged. Clicking
still opens the /context breakdown.

* fix(sdk): stop rendering usage_update frames as debug transcript text

The ui normalizer had no case for the usage_update session update, so the
frame fell through to the debug default and every model round appended a
raw-JSON bullet to the assistant turn in the web UI. Context occupancy is
surfaced by the status bar pill; the transcript drops the frame like
current_mode_update.

* feat(web-shell): move the context indicator into the composer toolbar as a ring

Review rework: replace the status-bar pill with a compact circular
progress ring in the composer toolbar's right cluster, immediately left
of the voice actions. The ring keeps the /context thresholds (>60%
warning, >80% error) and the visual 100% cap, hovers a Tooltip with the
full used/total detail (e.g. 53.6k / 1.0M tokens (5.4%)), keeps the full
wording on aria-label, and still opens /context on click. It ships as a
new contextUsage entry in composerToolbarActions so embedders can hide
it, hides while usage or the window is unknown, and follows the
toolbar's mobile-voice hiding. The StatusBar changes are reverted so the
indicator lives in exactly one place.

* fix(web-shell): let Radix position the shared tooltip arrow

The shared TooltipContent drew its arrow with a ::before pinned at the
content's horizontal center. Near a viewport edge Radix collision
avoidance shifts the content, so the arrow stopped pointing at the
trigger (about 35-40px off for the composer's context ring, which sits
at the far right). Replace the pseudo-element with TooltipPrimitive.Arrow,
which computes the offset from the trigger and the collision-shifted
content, keeping the tip on target for every tooltip and side.

* refactor(web-shell): share context-usage thresholds and token formatting (review)

Review round 3 suggestions:
- The 60/80 severity thresholds now live in one shared helper used by
  both the composer ring and the /context panel, so the two surfaces
  cannot drift.
- The ring tooltip's token formatter moves into the shared token-count
  utils and the /context panel uses it too, giving both surfaces the
  same k/M rendering (the panel previously showed a 1M window as
  1000.0k).
- The ring arc's transition is disabled under prefers-reduced-motion,
  matching the file's other decorative motion.
- New tests: exactly-80% stays warning (pins the strict threshold), the
  App wiring from connection usage to the ring props, click-through
  reaching the context-usage request, and the 0 fallbacks before any
  usage arrives.

* fix(sdk): resolve usage_update overlap with main

The same normalizer fix landed on main via #8790 while this PR was in
review; the merge auto-combined both edits into a duplicate case and a
duplicate test. Keep main's version — this branch now carries no
sdk-typescript delta.

* refactor(web-shell): consolidate the remaining token-count formatter copies (review)

Review round 4: the task-status panel's local formatter was
byte-identical to the shared one, and the collapsed-turn footer's copy
lacked the M branch — a collapsed turn with a >=1M-token input rendered
1048.6k while the ring tooltip and /context panel said 1.0M for the same
session. Both now import the shared formatter. Also pin the tooltip
arrow's positioning classes in the test, so a shadcn regeneration that
drops them fails instead of passing on a bare existence check.

* fix(web-shell): restore tooltip spacing and finish formatter consolidation (review)

Review round 5:
- With a Radix Arrow child, the offset middleware computes
  sideOffset + arrowHeight, so keeping the pseudo-element-era default of
  8 pushed every tooltip ~10px farther from its trigger. Default now 0;
  measured in a real browser the content edge sits 10px from the trigger
  (8px before the arrow change) with the tip 6px away.
- Drop the formatContextTokens-as-formatTokenCount aliases: the alias
  reused the exact name of the module's other, differently-behaving
  export, inviting wrong-import drift. Call sites use the real name.
- Colocate the pure-logic tests: utils/contextUsage.test.ts pins the
  strict-> boundaries, utils/formatTokenCount.test.ts owns the formatter
  cases (moved from ChatEditor.test.tsx), and ContextUsageMessage gains
  progress-bar color cases at 60/61/80/81 so the panel half of the
  shared-threshold contract is pinned too.
2026-08-10 03:48:06 +00:00
易良
856b793c15
fix(desktop): surface automatic update failures (#8807)
* fix(desktop): report updater installation failures

* fix(desktop): avoid unsafe updater retries
2026-08-10 03:15:40 +00:00
Shaojin Wen
af372e5a21
perf(review): guarantee compose survives a reverse-audit budget stop (#8791)
* perf(review): guarantee compose survives a reverse-audit budget stop

PR #8687 — a 4,269-line cross-worktree git guard — timed out after six
hours and posted nothing, holding ~20 E2E-confirmed Critical bypasses.
The deadline gate worked: it refused round 3 correctly with ~110
minutes and the whole reserve in hand. The tail after the stop was the
killer — a single hand-rolled verification agent re-running a 15-family
shell/git bypass battery with real filesystem E2E consumed all of it,
and the wall hit mid-verification before compose-review ever ran.

The reserve was one number covering "verification + compose + submit",
which is right for a normal per-finding re-trace but wrong for a
security PR where verification cost is unbounded (real E2E per finding)
while compose and submit stay bounded. So a distinct, smaller compose
FLOOR is carved out and the VERIFIER — not the reverse-audit builder —
is gated on it: below the floor `agent-prompt --role verify` refuses to
build (VERIFY BUDGET, exit 4), the findings keep their `— [unverified]`
tag for compose-review to cap, and compose runs. The floor is strictly
below the reserve, so a healthy run reaches the reverse-audit gate first
and never sees it; it is the cover for the one span the reserve cannot
bound.

The prose closes the bypass the gate cannot see: the post-stop tail
verifies only through the gated builder, never a hand-rolled agent, and
invents no fresh re-verification pass for findings already confirmed —
compose and submit are non-negotiable. DESIGN.md records the incident;
the RA budget message and SKILL Step 5 tail are rewritten to match.

* fix(review): close the round-1 gaps in the compose-floor gate

- R2-2 (Critical): the documented `0` escape hatch did not disable the
  verify gate past the deadline — `remainingSeconds` goes negative there
  and `negative >= 0` is false, firing the supposedly-disabled gate.
  verifyBudgetExhausted now returns null the moment the effective floor is
  0, before the comparison. Pinned with a past-deadline case.
- R2-1 (Critical): the gate bounds prompt CONSTRUCTION, not the wall time
  of an already-admitted verifier that then runs a long E2E past the
  floor — and agent-prompt builds prompts, it cannot cancel a running
  agent. The SKILL tail now tells the orchestrator to bound the WAIT: when
  the deadline is within the compose floor and a verifier batch has not
  returned, stop waiting on it, keep its findings unverified, and compose.
  The remaining execution-time cancellation is a harness capability, noted
  as such (same layer boundary as the hand-rolled-agent caveat).
- R2-3: the agent-prompt exit-code help now documents both the BUDGET and
  VERIFY BUDGET exit-4 refusals.
- R2-4: the reverseAuditBudgetMessage test now pins the new tail rules
  (gated verifier only, no hand-rolled agent, no re-verification).
- R2-5: docs/users/features/code-review.md documents the compose floor —
  default, env var, reserve nesting, exit-4 behaviour, zero hatch.

* fix(review): round-2 fixes for the compose-floor gate

- R3-1 (Critical): the verify gate admitted at exactly the floor, where
  the first work crosses below it — the floor is compose-only with no
  margin, so it now refuses at equality (`> floor`, unlike the RA reserve
  which admits at exact cover). Exact-boundary test flipped.
- R3-2 (Critical): the refusal message and SKILL claimed unverified
  findings "post as needing human review", but the confirmed-only rule
  keeps tagged details terminal-only. Reworded to the true contract:
  compose-review caps the verdict and discloses the verification gap; the
  tagged details stay terminal-only; what posts is the earlier rounds'
  confirmed findings plus that gap.
- R3-5: extracted readDeadlineSeconds / readNonNegativeSeconds, shared by
  both gates so the fail-open contract lives in one place.
- R3-3: pinned the verify gate's fail-open branches (malformed/non-positive
  deadline, past-deadline negative remaining, negative-floor fallback).
- R3-4: pinned the floor-minutes rendering (a field swap to remainingSeconds
  would misstate the protected floor).
- R3-7: pinned that a refused verifier writes no budget-stop marker and no
  admission stamp.
- R3-8: pinned validation-before-gate (a malformed verify call under the
  floor throws, not exit 4).

R3-6 needs no change: the SKILL.test pointer<->heading gate already covers
the DESIGN section (a dangling pointer fails it).

* fix(review): round-3 cheap fixes for the compose-floor gate

Low-risk corrections; the two edge-case Criticals (R4-1 broken-plan
masking, shared with the RA gate; R4-2 compose-review relaunch FIX) are
left as follow-ups — noted on the threads.

- R4-4: the readDeadlineSeconds extraction stranded reverseAuditBudgetExhausted's
  contract JSDoc above the helper; moved it back onto the function.
- R4-5: the round-2 "terminal-only, never posted" wording contradicted
  compose-review's own verdict line ("posted, disclosed as unverified") —
  a pre-existing contract ambiguity this PR should not relitigate. Reworded
  the message and SKILL to the invariant both readings share: an unverified
  finding is never treated as a confirmed blocker; the verdict is capped.
- R4-7: "below the N-minute floor" contradicted the exact-equality refusal
  (the gate admits on `> floor`); now "at or below", in the message and the
  user docs.
- R4-3: pinned that a blank/whitespace floor override falls back to the
  default (only explicit 0 disables).
- R4-6: pinned the negative-remaining clamp in verifyBudgetMessage.

---------

Co-authored-by: verify <verify@local>
2026-08-10 02:50:45 +00:00
易良
cc46babf79
feat(desktop): create default workspace on first launch (#8814)
* feat(desktop): create default workspace on first launch

* test(desktop): cover default workspace failure

* feat(desktop): honor QWEN_DEFAULT_WORKSPACE_DIR for the default workspace

* fix(desktop): defer default workspace creation; kill in-flight runtime on stop

Move default-workspace directory creation out of setup into the runtime
start blocking task, so the first touch of ~/Documents (which can raise
the macOS TCC prompt) no longer blocks the main thread. Path resolution
(initial_workspace/default_workspace) is now pure and reports whether the
derived default directory must be created; creation failures surface
through the existing runtime-failed path.

Register the spawned runtime child in a shared pending handle before the
startup wait, so app exit, restart, and generation switches can kill an
in-flight daemon instead of orphaning it in its own process group. The
startup wait loops poll the shared handle and treat a taken child as a
stop during startup.

* fix(desktop): close runtime startup cancellation race

* fix(desktop): preserve default workspace retries
2026-08-10 02:44:08 +00:00
易良
916a8d97aa
fix(desktop): open Local Control on the active session (#8806)
* fix(desktop): share the active session safely

* fix(desktop): reject malformed Local Control headers

* test(desktop): cover Local Control boundaries

* test(desktop): cover bare-CR header and empty workspace guard
2026-08-10 02:33:36 +00:00
Shaojin Wen
33e602cc41
fix(test): stop background-shell tests sharing a fixed /tmp sidecar path (#8813)
* fix(test): stop background-shell tests sharing a fixed /tmp sidecar path

`makeEntry` defaulted to `outputPath: '/tmp/s1.output'`, so every entry in
this file — across tests, across workers, across CI jobs on the same host —
mirrored its status sidecar to the single path `/tmp/s1.status`.

`/tmp` carries the sticky bit. Once that file belongs to another uid, the
atomic rename in `atomicWriteFileSync` fails EPERM, and
`renameWithRetrySync` burns its full 50+100+200ms backoff before the
registry swallows the error. Every register/complete then costs ~350ms and
the sidecar never lands.

That is what the loop tests were paying: the retention-cap cases do 68
register/complete calls, and CI measured 23.8s each. The durations across
the whole file were exact multiples of 351ms — 352 / 703 / 1405 / 2113 /
3520 — with no variance, which is the backoff sum, not disk latency.

Give each entry its own temp directory instead. The shared path is gone,
the rename succeeds, and the file drops from 128.9s to 3.2s locally with
`/tmp/s1.status` made immutable to reproduce the CI condition.

#8797 raised these four cases to a 120s timeout to survive the cost. With
the cost removed the band-aid goes too, so a future regression fails loudly
instead of silently taking two minutes.

* fix(test): unify sidecar test helpers and pin per-entry outputPath uniqueness

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test: assert the whole output-file element, not just its tail

Unifying the sidecar helpers moved these two entries onto random temp
directories, and the assertions were relaxed to a suffix match to cope.
Rebuild the expected element from the path under test instead: the temp
prefix is random, but the escaping and control-byte stripping these two
cases exist to pin are exact.

* test: escape expected XML paths the way the registry does

The anchored `<output-file>` assertions built their expected value by hand —
one replaced `&` only, the other nothing at all. `tmpdir()` may legally
contain XML metacharacters (`&` on Windows, `<` on POSIX), so those cases
became environment-dependent the moment they moved off the fixed `/tmp`
path.

Run the expected path through the same `escapeXml(stripDisplayControlChars())`
the registry uses. Verified with `TMPDIR=/tmp/qwen-xml-probe/a&b<c`: the
helper passes all three focused cases, while the hand-rolled version fails
two.

* test: escape last dynamic output-file expectation, stop sidecar leak

The escape conversion in 5549db2348 covered two of the three dynamic
<output-file> expectations; `emits one task-notification when a shell
completes` still interpolated the temp path raw, so a TMPDIR containing
XML metacharacters (legal on POSIX and Windows) failed it. Route it
through the same expectedOutputFileElement helper.

Also remove the `<dir>.status` sidecar the directory-outputPath test
leaks next to its temp dir on every run; afterEach only tracks the dir.

* test(core): clean the dir-outputPath sidecar in afterEach, not after assertions (#8813)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): reuse shared display sanitizer in background shell registry (#8813)

The registry kept a private stripDisplayControlChars that only removed
C0/C1 controls, while the shared terminalSafe helper also strips Unicode
bidi overrides and isolates. With a TMPDIR containing one of those
characters the test expectations (built with the shared helper) lost a
character the notification XML retained. Reuse the shared helper so both
background notification surfaces apply the same Trojan-Source defense and
the expectations cannot drift from the implementation again; extend the
control-character test to pin the bidi stripping.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 01:53:58 +00:00
Shaojin Wen
b314d01f2d
fix(web-shell): stop rendering unrecognized daemon events in transcripts (#8812)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* fix(web-shell): stop rendering unrecognized daemon events in transcripts

The daemon UI normalizer projects any frame it has no case for into a
`debug` event carrying a raw JSON dump. webui's ChatViewer drops those
blocks, but Web Shell renders `status` and `debug` together as system
info, so every event kind the daemon ships ahead of the UI surfaces as
unreadable JSON in the middle of the conversation. This has been patched
per-symptom three times now: two string-prefix suppressions inside
`isIgnoredWebShellStatus`, plus #8790 for `usage_update`.

Give the normalizer's debug events a structured `debugReason` and let
Web Shell branch on it instead of pattern-matching text:

- `unrecognized_event` / `unrecognized_session_update` — the daemon runs
  ahead of this client; developer diagnostics, not conversation content.
  Web Shell no longer renders them.
- `malformed_payload` — a frame the client does know arrived unusable.
  That is a real defect signal, so it stays visible.

Debug events dispatched by clients themselves, such as Web Shell's own
model-switch summary, carry no `debugReason` and keep rendering.

The two `(unrecognized daemon event)` prefix checks are now covered by
`debugReason` and are removed; the `Model switched: ` check stays, since
`model.changed` projects to a `status` block rather than a debug one.

* fix(sdk): classify a discriminator-less session_update as malformed

Review of #8812 caught a hole in the new classification: `session_update`
payloads such as `{}` or `{ sessionUpdate: 42 }` reach the default branch
with `kind === undefined`, and stamping them `unrecognized_session_update`
made Web Shell hide the only diagnostic a malformed frame produces.
Reserve the unrecognized reason for a real unknown string kind.

Also update the top-level default-case comment, which still pointed
adapters at the debug text prefix, and add a reducer-level test proving
`debugReason` survives the UI-event → transcript-block boundary: the
normalizer tests inspect events and the Web Shell tests build blocks by
hand, so dropping the spread in transcript.ts would leave both green.

* fix(web-shell): keep filtering legacy debug blocks, tighten the reason split

Four review findings from #8812:

- `WebShellTranscript` is a public entry point taking already-projected
  blocks, so blocks from an SDK predating `debugReason` still arrive with
  no reason and started rendering again when the prefix checks were
  removed. Fall back to the stable ` (unrecognized daemon event): ` marker
  when no reason is present — which covers every unrecognized event type,
  not just the two previously suppressed by name. The old-shape fixture is
  restored (adding `debugReason` to it had hidden this path) and a
  dedicated legacy test now pins it.
- A whitespace-only discriminator is truthy, so `sessionUpdate: ' '` was
  classified unrecognized and hidden. Gate on `trim()`, matching the
  convention `getFirstString` already uses.
- Add the mirror invariant for the reducer: a client-dispatched debug
  event must produce a block with no `debugReason`. Defaulting the field
  in `appendStatusBlock` otherwise passes every other test while tagging
  the model-switch summary unrecognized.
- Guard the outermost public re-export. A type-only guard would not hold —
  vitest erases `export type` through esbuild and this package's tsconfig
  excludes `test/` — so ship the union as `DAEMON_UI_DEBUG_REASONS`,
  matching `DAEMON_ERROR_KINDS`, and assert it at runtime.

* fix(web-shell): suppress legacy usage_update/a2ui blocks with no debugReason

Follow-up verification on #8812 pointed out the marker fallback does not
close the original report. #8790 stopped the SDK inserting new
`usage_update` blocks, but `WebShellTranscript` renders whatever blocks its
caller passes, so a transcript persisted or projected before that still
holds them and the spam returns after upgrade.

The legacy `session_update` projection is `<kind>: <json>` with no marker to
key on, so match those by kind name instead. The list is closed on purpose —
`usage_update` and `a2ui`, the two known to have leaked — and requires the
`: {` shape, because a generic `<word>: {` rule would swallow legitimate
diagnostics. Blocks the normalizer classified still win on `debugReason`,
so `malformed_payload` and client-dispatched debug blocks stay visible.

Mutation-checked in both directions: dropping the fallback fails the legacy
test, and loosening the prefix to bare `usage_update:` fails the test that
pins prose and classified blocks staying visible.

* fix(web-shell): match the legacy projection shape, not a quoted marker

The legacy fallback was too broad in two ways, both reachable. It ran for
`status` blocks as well as `debug` ones, because this helper is called from
the shared `case 'status': case 'debug':` arm, and it matched the marker as
a substring anywhere in the text.

Probed at df0b757c3c: all four of these were dropped — a status line
quoting the marker, a legacy malformed payload relaying an upstream peer's
message that contains it, a client-dispatched summary quoting it, and a
status block whose text starts with `usage_update: {`. Exactly the
diagnostics this PR promises to keep.

Scope the text match to `debug` blocks, and anchor it to the whole legacy
projection (`<event-type> (unrecognized daemon event): <json>`) instead of
the bare marker. Classified blocks still win on `debugReason` before any of
this runs.

Mutation-checked: dropping the `kind === 'debug'` guard and restoring the
substring match each fail the new negative test.

* fix(web-shell): match legacy projections with non-object payloads

The anchored legacy pattern required the payload to start with `[`, `{` or
`"`, which only holds for objects and arrays. `DaemonEvent.data` is
`unknown`, and `stringifyJson` returns strings verbatim, serializes
primitives as `42` / `true` / `null`, and yields `''` for `undefined` — so
every non-object payload bypassed the compatibility fallback and rendered.

Drop the leading-character constraint. The event-type prefix plus the fixed
phrase, anchored at the start, is specific enough on its own, and the
negative cases for quoted markers and status blocks still pass.

Regression test covers object, array, string, number, boolean, null and
empty payloads. Mutation-checked: restoring the character class fails it.

* fix(web-shell): hide debug blocks by the unrecognized_ reason category

The debugReason filter enumerated the two current `unrecognized_*` values,
but the SDK contract this PR adds names reasons by category: `unrecognized_*`
is forward-compat noise to hide, `malformed_*` a defect signal to keep
visible. A reason a newer SDK adds would compile silently against the
two-literal comparison and render raw JSON again with both suites green.

Match the category prefix instead. Also drop the now-dead marker branch in
MessageList's mid-turn hide check: every block carrying that prefix is
filtered upstream in the adapter (reason-stamped via `debugReason`, legacy
via the anchored pattern), and the dedicated normalizer case emits a status
event keyed by `source`. Document `DaemonUiDebugReason` beside the sibling
closed enums in the daemon-ui docs, whose forward-compat bullet still
described the unstamped projection.

Regression test pins both directions of the category contract with reasons
outside the current enum.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-10 00:51:57 +00:00
qqqys
55e20db328
feat(workflows): add an orchestration policy layer to the Workflow tool description (#8694)
* feat(workflows): add an orchestration policy layer to the Workflow tool description

The description was an accurate technical specification that said nothing
about when to orchestrate, which shape to use, or how to trust what comes
back. With only the API in view, the naive shape wins every time: fan
everything out through one barrier and take the first answer at face value.

Adds the decision layer on top of the existing runtime facts — purpose
framing, pipeline-by-default with an explicit test for when a barrier is
genuinely required, scout-then-orchestrate, reusable shapes, adversarial
and perspective-diverse verification, the deduplicate-against-seen rule
that keeps discovery loops terminating, and honest reporting of bounded
coverage.

Prompt text only: no runtime, sandbox, or schema change.

Closes #8690

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(workflows): close the description's limit gaps and pin every policy section

Addresses the round-1 review on #8694 (R1-1 through R1-6).

The description now names the three limits a model has to plan around
rather than discover from a mid-run failure: `workflow()` nests one level
only, the 30-minute wall-clock cap per run
(`QWEN_CODE_MAX_WORKFLOW_SECONDS`), and the per-run output-token cap —
surfaced as a `budget.total` probe rather than a knob name, keeping the
P5 R2 rule that the env var stays out of model-reachable text.

The pinning test grew anchors for the three policy sections a mutation
probe could delete while it stayed green, and for
`QWEN_CODE_MAX_WORKFLOW_CONCURRENCY`, which the description advertised
with nothing asserting its spelling.

The JSDoc claim that the authoring contract "is not duplicated here" was
false — the limits and env knobs appear in both halves — so it now says
they are a summary to keep in sync.

* docs(workflows): anchor the planning caps and pair each env knob with its limit

Round-2 review (R2-1/R2-2/R2-3) plus the two non-blocking suggestions from
the maintainer verification round.

- R2-1: anchor the two env knobs through the orchestrator's exported
  MAX_WORKFLOW_AGENTS_ENV / MAX_WORKFLOW_CONCURRENCY_ENV instead of
  hardcoded literals, so a rename on the runtime side fails the guard too.
  QWEN_CODE_MAX_WORKFLOW_SECONDS has no exported constant
  (workflow-sandbox.ts reads it inline), so it stays a literal.
- R2-2: pin the numbers the model plans around — "up to 1000 agents total"
  and the "30-minute wall-clock cap" — not just the knob names.
- R2-3: the sync note claimed the whole limits block restates the `script`
  contract; the wall-clock cap, token budget and one-level workflow()
  nesting limit live only in the tool description. Say which is which.
- W-1: budget exhaustion refuses each further agent() call; a bare
  sequential await sees the rejection, while parallel()/pipeline() turn the
  refused slot into null and keep running (errors-as-data, settleToNullArray).
- W-2: pair each env knob with the quantity it overrides — the shared
  parenthetical read as if both knobs covered the total.

Mutation-probed: 1000->500, 30-minute->15-minute, and renaming
MAX_WORKFLOW_AGENTS_ENV's value each turn the suite red on the intended
assertion; restored tree is 40/40 green.

* fix(workflows): derive description caps from the runtime constants

Addresses the four Suggestions from review round 3 on #8694.

R3-2 / R3-3: the agent cap and the two env-knob names were prose
literals in both model-visible halves — the tool description and the
`script` parameter description — a third copy sitting in the pinning
test. Interpolate `DEFAULT_MAX_AGENTS_PER_RUN`, `MAX_WORKFLOW_AGENTS_ENV`
and `MAX_WORKFLOW_CONCURRENCY_ENV` from `workflow-orchestrator.ts` into
both halves instead, so raising a cap moves every copy at once and the
model can no longer read two contradictory caps from one tool call.
`DEFAULT_MAX_WALL_CLOCK_MS` stays a literal: it is private to
`workflow-sandbox.ts`. The doc comment now says which values still need
hand-syncing rather than claiming all of them do.

R3-1: the numeric anchor pinned the description's own literal, so it
could not catch the drift its comment claimed to catch. Anchor it
through the exported constant, and add a test pinning the `script`
parameter description's copy of the same caps — nothing asserted it
before, so a maintainer could raise a cap, get the tool-description
test green again, and leave `script` advertising the old number.

R3-4: issue #8690 asked the text to use this project's vocabulary. The
description advertised `workflow('<name>')` without ever saying where
saved workflows live, leaving the model to guess a name blindly or
construct an absolute `scriptPath` it has no way to know. Name both
scopes and their precedence.

Verification: npm run build, npm run typecheck, eslint on both changed
files, and vitest on workflow.test.ts + workflow-orchestrator.test.ts
(174 passed). Probed the new anchors both ways with the cap temporarily
raised to 2000: the suite stays green (the description tracks the
constant), and pasting the literal `1000` back into the description
turns both assertions red.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:50:21 +00:00
jinye
0a3d7bb5c1
feat(acp): Protect against repeated tool execution failures (#8469)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
* feat(acp): protect repeated tool execution failures

Add a conservative prompt-local guard for repeated typed ACP tool execution failures, with shadow/warn/enforce rollout modes, privacy-safe telemetry, and coverage for the final execution outcome contract.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp): harden repeated tool failure guard rollout

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp): address repeated failure guard review

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(acp): improve repeated failure guard recall

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(acp): clarify review and rollout gates

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-08-09 15:47:38 +00:00
jinye
a810f7e16c
fix(serve): Make session restore timeouts safe and observable (#8691)
* fix(serve): make session restore timeouts safe

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): restore missing core mock exports in the ACP worktree suite

The restore-tracing change added `extractDaemonTraceContext` and
`withDaemonSpan` to `acpAgent.ts`, but `acpAgent.worktree.test.ts`
replaces `@qwen-code/qwen-code-core` with a full mock factory that never
listed them. `loadSession` then failed on an undefined export, taking all
three cases down and producing teardown rejections from the half-built
agent. The sibling suite was updated; this one was missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): bound and disambiguate the abandoned restore lifecycle

Four follow-ups from review of the restore timeout work.

A startup budget may now raise the restore budget but never lower it.
Taking an explicitly configured `initializeTimeoutMs` as the restore
fallback meant a deployment that tightened its child-initialize check
still inherited a sub-default restore deadline — exactly the failure this
change exists to remove. An explicit `sessionRestoreTimeoutMs` still wins
outright, including below the default, for deployments that want restore
to fail fast. Validation now names the field actually at fault.

A restore fenced behind a timed-out predecessor is no longer reported as
an ordinary in-flight restore. It carries `reason:
awaiting_abandoned_cleanup` and a retry hint of one restore budget
(capped at 120s) instead of the ordinary 5 seconds, because the fence
cannot clear until the non-cancellable ACP request settles and a 5-second
cadence just spins the caller against a 409 it cannot resolve.

Whether a channel is condemned is now derived rather than sticky. A
timeout recorded `emptyReapPending` permanently, so any channel that had
ever seen one was guaranteed to be reaped once its remaining work
drained, forcing a cold respawn even when the late restore had landed and
closed cleanly. The reap condition is now computed from an outstanding
`unsettledAbandonedRestores` set, quarantine, or an ordinary pending
empty reap; real settlement clears the entry and hands the channel back
to the configured idle policy.

Abandonment no longer retains ownership without bound. One further
restore budget after the deadline, a still-unsettled restore marks the
channel `restoreSettlementOverdue`: existing sessions and workspace
control keep working, but fresh session work is refused so the channel
can drain, since closing the transport is the only lever that releases a
permanently hung request. Releasing capacity while hidden work runs would
allow unbounded oversubscription, and force-killing a channel with live
siblings would reintroduce the failure this work removes, so neither is
done. Fresh-admission blocking is now scanned across alive channels
rather than tracked in a single reference, so a second condemned channel
cannot silently displace the first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): keep the abandoned restore lifecycle off ids it no longer owns

Two correctness gaps in the abandoned-restore machinery introduced by this
PR, both reported by automated review and both confirmed by mutation
testing (each new test fails when its fix is reverted).

A caller-supplied `sessionId` is used verbatim by the agent, but
`spawnOrAttach` never consulted `inFlightRestores`. A fresh spawn could
therefore take an id that a restore still owns, in either lifecycle phase.
The consequences were silent: `abandonedRestoreIds` suppresses session
updates, guardrail events, and child notifications, so the new session
would have registered successfully and then emitted nothing; and a late
`settleAbandonedRestore` would have closed and tombstoned it out from
under its owner. Such a spawn is now rejected with the same
`RestoreInProgressError` and reason the restore path uses, so the caller
gets the correct retry hint for whichever phase is holding the id.

The cleanup path is guarded independently, because the request-level check
only covers the id the caller asked for and a session registers under the
id the child returns. An abandoned restore never reaches
`createSessionEntry` — the deadline rejects before registration — so any
live entry under that id belongs to someone else. Cleanup now detects that
and returns without closing or tombstoning, releasing its own bookkeeping
instead.

The notification fence has no TTL and was only cleared by
`markRestoreInFlight`, which covers a subsequent restore and nothing else.
`createSessionEntry` now clears it for every registration route, so a
legitimate owner of the id is never handed a session that silently drops
everything the child sends it.

Also tightens two tests that could not observe the values they pin. The
SDK default restore timeout admitted any value in (30s, 70s]; it is now
split at the exact boundary, so collapsing the default onto the 60s server
budget — which would make the client abort race the daemon's own deadline
and cost the caller its structured 504 — fails. And the advertised-budget
propagation from capabilities through to the SDK call had no live-path
assertion; dropping the capabilities argument at the real call site left
every existing test green. The `as never` casts are replaced with typed
`DaemonCapabilities` values so a field rename fails typecheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): let a condemned channel drain without its wedged child

Merging main's active-work close protocol (#8588) into this PR's abandoned
restore bound produced a deadlock that neither side has on its own, and the
conflict resolution was committed without running tests.

`maybeCloseIdleSession` now routes through `confirmChildUnheld`, which asks
the child whether it still holds work before closing a session nobody is
attached to. That is right in general and wrong for a channel this PR has
already condemned. `restoreSettlementOverdue` and quarantine exist precisely
because the child stopped being answerable, and their whole premise is that
visible work drains so the channel can be reaped — closing the transport is
the only thing that can release a restore we cannot cancel. Making that
drain depend on a round trip to the wedged child inverts it: a child stuck
in a non-cancellable restore is exactly the one that cannot reply inside
`ACTIVE_WORK_CLOSE_TIMEOUT_MS`, so the sessions never close, the channel
never drains, the reap never fires, and the bound never takes effect.

A channel condemned by the restore lifecycle now skips the round trip and
proceeds to local teardown. Nothing is attached to the session by then —
`maybeCloseIdleSession` gates on that — and the sibling-safety invariant is
untouched: this closes sessions whose clients have already left, it does not
force-kill a channel that still has live ones.

The regression test drives an overdue channel whose child never answers the
close-if-unheld probe and asserts the detach still reaps it. Reverting the
guard reproduces the deadlock as a test timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(serve): pin the restore-timeout contract the review found unasserted

Automated review identified eleven places where the restore-timeout work's
behavior was correct but unpinned — each with a mutation that ships green.
Every fix below was verified the same way: apply the mutation, watch the new
assertion fail, revert, watch it pass.

The timeout path's telemetry had no coverage at all, which is the sharpest
gap given that observability is what this work exists to deliver. A shared
recorder now asserts the public timeout result and its kill_empty-vs-
fence_shared signal, the late arrival, and the cleanup outcome for both the
closed and quarantined cases.

The deadline timer's cancellation on a successful restore was likewise
unpinned: deleting both `clearTimeout` calls kept the whole suite green,
while in production the stale timer fires one budget after a successful
restore and abandons a live session — fencing its frames, closing its event
bus, and emitting a spurious timeout. A success-path test now advances past
the deadline and asserts no second public result.

Three more bridge assertions proved less than they claimed: the concurrent-
restore case never checked that the abandoned restore settles, the
workspace-control case never checked that the deferred reap eventually
fires, and the resolver never pinned the accepting side of the MAX boundary
(a `>` to `>=` mutation rejects the largest legal delay at boot). The
workspace-control case also needed a positive channel idle budget, since
with the default zero the idle-timer kill substitutes for the reap junction
under test; its assertions are rewritten around the derived reap semantics
rather than the sticky flag they predate.

Outside the bridge: the scheduled-task timeout wiring had no test, so
deleting the arguments silently fell back to the helpers' own defaults; the
cold restore path never asserted that `live_restore_ms` is absent; the SDK's
per-request validation and its over-ceiling clamp were untested; the WebUI
watchdog test jumped straight to its own value, staying green for any
watchdog at or below it, including the 30s attach value that would recreate
the original symptom in the browser; and the two new known error types were
unexercised, so dropping either would relabel every restore-timeout and
quarantine error as unknown.

Two review items are deliberately not taken here and are recorded in the
design doc's non-goals instead: transcript materialization is still not
separately attributable from `config_setup`, which needs instrumentation
inside the core session loader that P1/P2 restructures anyway, and sibling
event-loop latency during a large restore remains unmeasured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): bound the condemned-channel close and complete the fence contract

Second automated review round, on the code the first round produced. One
Critical and twelve suggestions; all verified by mutation before and after.

**The Critical is a regression I introduced.** Letting a condemned channel
skip the bounded hold probe routed it into `closeSessionImpl`, whose agent
close is unbounded when it throws on failure — so the fix traded a bounded
wait on a wedged child for an unbounded one. A settlement-overdue channel
with an unresponsive child would hang `detachClient` forever, strand the
session in `closing`, never drain, never reap, and 503 every new session
until restart: strictly worse than before. `CloseSessionOpts` now carries an
`agentCloseTimeoutMs` that the condemned path sets, so a hang lands in the
existing unknown-outcome recovery, which kills the channel — the teardown
the drain was waiting for. The earlier test missed this because its fake
child still answered the plain close; it now answers nothing at all, and
asserts the detach itself returns.

**The fence was invisible on the transports clients actually use.**
`toRpcError` had no `RestoreInProgressError` case, so over acp-http and
acp-ws — which SDK negotiation prefers over REST — the fence degraded to an
opaque internal 500 with no code, reason, or hint, and the backoff contract
this work documents was impossible to honor.

**Two retry hints still advertised five seconds for states that outlive a
budget.** The restore 504 creates the fence, and quarantine lasts until the
channel drains; a fresh-id caller never reaches the 409 that carries the
real hint, so its header was the only signal it got. Both now derive from
the budget through one shared clamp helper, which also replaces the formula
that was inlined in the bridge and gives the documented 5-120s bounds a
test.

**A spawn collision reported an operation the caller never issued**, naming
the restore owner's action as both the active and the requested one and
telling the caller to retry an endpoint it never called.

The rest: five places still described the initialize-timeout fallback as a
plain chain rather than raise-only, contradicting sibling docs shipped in
this same PR; the design doc omitted the retry-hint clamp; the protocol
reference omitted the new spawn emission site; the error taxonomy omitted
`restore_settlement_overdue`, which matters because its audience is
monitoring. Test-only gaps: the dynamic 409 had no HTTP-layer coverage, the
120-second cap was unpinned, and the SDK's precedence of an explicit global
timeout over the advertised budget was pinned only branch-by-branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(serve): preserve restore session ownership handoff

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-09 15:47:09 +00:00
Shaojin Wen
3c3084e78a
fix(ci): route workflow label mutations through REST (#8761)
* fix(ci): route workflow label mutations through REST

`gh pr edit` cannot mutate anything on this repository: its GraphQL
lookup requests repository.pullRequest.projectCards, and with Projects
(classic) attached GitHub returns the deprecation as an error, so the
command exits 1 before applying the change. Reproduced from a live
clone against PR #8755 — the error names the field outright.

Three workflows carried label mutations through it:

- pr-self-report-label.yml: every add/remove arm failed — 43 straight
  run failures from 2026-08-04 on; the green runs were all the
  nothing-to-do arm. Self-reported PRs (like #8755, whose author also
  opened #8750) never got the label.
- qwen-autofix.yml: the `@qwen-code /takeover` and `/takeover stop`
  COMMAND paths never toggled the label — only the UI label events
  worked, so the command was dead weight wearing an ack.
- repo-hygiene.yml: the add was `|| echo`-guarded, so it never failed
  the run — it just never labeled anything, while the fallback message
  blamed a label that exists.

All five sites now use the REST issues/labels endpoints, which never
touch that query. Two traps handled on the way:

- Every label involved contains a slash, and in the DELETE the label is
  a PATH SEGMENT — unencoded it 404s. Encoded via jq @uri, and the
  tests assert the literal %2F because a real jq runs in the replay.
- The REST add auto-creates a missing label, which repo-hygiene
  explicitly promises never to do — that site gets an existence probe
  first, and its misdiagnosing fallback message is corrected.

Verified live on #8755 before editing anything: the exact gh pr edit
call fails with the projectCards error; REST POST applies the label
(backfilling the one it was owed), DELETE with %2F removes it.

Tests: the stub-driven replays for both the self-report step and the
takeover toggle now pin the full REST method + path (encoding
included), and a repo-wide guard bans `gh pr edit --add-label/
--remove-label` in every workflow so the class cannot return.
Mutation-tested, 6 of 6 caught: each of the five sites reverted to
gh pr edit, and the DELETE stripped of its encoding.

* fix(ci): harden REST label mutation steps per review (#8761)

* fix(ci): pin REST label failure policies per review (#8761)

Review round for the REST migration:

- The DELETE arms tolerated EVERY failure (`|| true`), masking
  403/5xx/network errors behind a green run and a false "removed"
  log. They now tolerate only the documented 404 race — any other
  failure emits a :⚠️: while keeping the step green
  (pr-self-report-label) and the release ack alive (qwen-autofix).
- Neither replay harness could make a `gh api` call fail, so both
  failure policies were unpinned. They gain failure knobs (knob
  value on stderr like a real gh HTTP error) and now pin: 404 race
  silent, other DELETE failures warned, POST loud. The toggle
  replay also moves to -eo pipefail like the runner's bash default,
  reproducing the step's real failure semantics.
- The jq stub enforced only the --arg shape; it now also enforces
  the `$l|@uri` program, so a filter mutation fails the suite
  instead of riding the stub's unconditional percent-encoding.
- The gh-pr-edit guard misfired on comments and miscounted lines
  after joining continuations: comments are stripped before
  matching, and offenders are reported at the physical line where
  the (possibly wrapped) command starts.

Mutation-tested with 8 probes, all caught: blanket || true on
either DELETE, || true on either POST, dropped |@uri, a comment
quoting the ban (stays green), an executable and a wrapped
violation (both red, correct line).

* Address review round 3: close the guard evasions, convert the release path

Four round-3 findings, each reproduced before fixing, plus the release
path the round-1 scope note deferred.

- The ban guard now scans what bash executes, not the YAML surface: the
  decoded run: values of every parsed workflow, whole-line comments
  stripped, continuations joined the way bash joins them (backslash-
  newline removed, nothing inserted), matched whitespace-tolerantly. All
  three reproduced evasions — a # inside a quoted string eating the
  trailing backslash, wraps inside the command prefix or a flag token,
  and folded scalars — are fixture-pinned. Offenders report as
  file » job » step; line numbers stopped meaning anything after joins.
- classify-release-notes.mjs mutates labels through REST now, and the
  guard grew an argv-form scan over .github/scripts/*.mjs that flags the
  old file (negative-controlled) — the release path was the last
  gh pr edit label site, failing silently behind continue-on-error.
- JQ_STUB enforces the full invocation: -rn (with -r alone real jq
  evaluates zero inputs and prints nothing), the binding name l (real jq
  exits 3 on $l undefined), and the program. Either reproduced mutation
  previously expanded the substitution empty, sent the DELETE to
  …/labels/ with no name segment, and the 404 tolerance swallowed it.
- The takeover engage POST gets the idempotent create its siblings
  carry, pinned to the label's real color (1D76DB): the REST add would
  re-create a deleted label silently with a random color.
- runToggle captures writes on throw, and the engage-failure assertion
  now pins the ORDER its comment claims: a failing apply must leave no
  "takeover-ack engaged" in the captured writes — the bare toThrow
  passed even with the ack moved above the POST (reproduced).
- The two REMOVE_ERR DELETE idioms are drift-pinned byte-identical
  modulo the label variable, the honest substitute for sharing shell
  across workflow files.

Mutation-tested, 6 of 6 caught: the evadable regex restored, -rn and
the binding name mutated in the workflow, the create dropped, the ack
posted before the POST, and the old .mjs flagged by the new scan.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 15:05:15 +00:00
易良
b7eb4cf29e
fix(desktop): compact Local Control dialog (#8800) 2026-08-09 12:51:19 +00:00
Shaojin Wen
e39781d858
feat(web-shell): add fullscreen view for the right artifact panel (#8614)
* feat(web-shell): add fullscreen view for the right artifact panel

The right panel (artifacts / subagents / review) is narrow, so long
previews and transcripts are hard to read. Add a fullscreen toggle to
the panel header that expands the panel to cover the viewport; pressing
Escape or the toggle again shrinks it back to its dock or drawer.

* test(web-shell): capture fullscreen artifact panel in visual previews

The fullscreen toggle and surface are only reachable with the artifact
panel open, so no visual scenario rendered them and the before/after
preview could not see this feature. Extend the code review artifact
scenario to expand the panel fullscreen, capture it, and assert the
Escape restore path.

* fix(web-shell): keep artifact panel mounted across fullscreen toggle (#8614)

* fix(web-shell): harden artifact panel fullscreen per review feedback (#8614)

* fix(web-shell): guard artifact drawer Escape for IME composition (#8614)

* fix(web-shell): scope toast z-index, reset dock animation flag, guard IME keyCode (#8614)

Round-4 review fixes for the artifact panel fullscreen:

- Elevate ToastHost above dialog-backdrop-tier surfaces only while the
  fullscreen surface is up (new `elevated` prop); otherwise it stays at its
  original z-index 30 so DialogShell modals keep painting above toasts. The
  old comment rested on DialogShell.module.css's dead `.backdrop` class —
  the live modal path resolves the same host variable with fallback 50.
- Reset suppressArtifactDockOpenAnimation when the dock unmounts (panel
  close or floating drawer taking over) and only set it while docked, so a
  floating<->docked flip after a fullscreen round-trip no longer mounts the
  dock without its slide-in animation.
- Extend the window Escape handler's IME guard to keyCode 229, matching the
  paired guard used everywhere else in the package: a WebKit-shaped IME
  Escape (isComposing false, keyCode 229) previously shrank a fullscreen
  panel and swallowed the native IME cancel.

Adds the suggested coverage: keyCode-229 drawer variant, docked-fullscreen
IME Escape, ask-user shrink keyboardActive history, streaming+fullscreen
Escape ordering, and the floating-interlude dock animation regression.

* fix(web-shell): portal docked fullscreen surface, gate chat shortcuts, clamp z-index (#8614)

Round-5 review fixes for the artifact panel fullscreen:

- Clamp the fullscreen surface z-index floor to max(1, backdrop - 10): a
  host setting --web-shell-dialog-backdrop-z-index below 10 gave the
  surface a negative z-index, painting the opaque panel behind the app
  background and blanking the shell.
- Add artifactPanelFullscreen to interactionBlocked: chat-only global
  shortcuts (Ctrl+L/O/Y, Shift+Tab, the btw hotkey) kept mutating the
  hidden chat behind the surface, and the btw capture-phase Escape handler
  dismissed hidden content and swallowed the Escape that shrinks the panel.
- Pad the floating drawer fullscreen content with env(safe-area-inset-*):
  the portaled drawer sits outside the padded app root, so on
  viewport-fit=cover devices the toolbar and Exit control could sit under
  the notch/status bar and bottom content behind the home indicator.
- Move the docked fullscreen surface into the top-level portal root and
  give it document-level modal semantics: a transformed, paint-contained,
  or lower-stacking host ancestor could bound the fixed panel or paint
  over it, and Tab could escape into covered host controls. The panel
  wrapper portals into a display:contents slot that the fullscreen effect
  parks in the portal root, so the SAME node survives the move (panel
  state preserved, React event delegation intact); FocusScope provides the
  Tab containment and the effect hides every outside tree from AT and
  captures stray focus — matching what the floating variant gets from
  vaul's Radix dialog. Declares @radix-ui/react-focus-scope (already in
  the tree via radix-ui) as a direct dependency.
- Portal the elevated ToastHost into the portal root: in shadow-DOM portal
  mode the fullscreen drawer surface is sealed inside the portal host's
  stacking context, so an in-tree toast painted beneath it for its whole
  auto-dismiss lifetime.
- Keep the dock animation suppression flag across dock<->floating
  hand-overs performed mid-fullscreen in both directions, so shrinking
  back to the dock never replays the slide-in on the already-open panel.

Tests: pin both hand-over suppression directions (mutation-verified), the
portal placement, the interaction gate, and toast elevation in the main
fullscreen test; add a colocated ToastHost.test.tsx for the elevated
class; fix the dead 0-measurement step in the docked-width round-trip
test (the 0 is now seeded before entering fullscreen, where the clamp
effect can actually observe it). Adds a drawer-fullscreen visual scenario
at a narrow viewport in both themes, with Escape restoring the drawer.

* fix(web-shell): repair docked fullscreen panel modal semantics (#8614)

* fix(web-shell): reset fullscreen state in the panel close commit (#8614)

Closing the artifact panel while fullscreen reset the fullscreen flag
only in the passive management effect, so one render committed with the
panel unmounted while the covered shells stayed display:none — one
painted frame of an empty shell before the chat reappeared. Batch the
fullscreen and dock-animation-flag resets into closeArtifactPanel and
the last-tab close so the recovery happens in the same commit.

Adds a regression test asserting the shells are revealed in the close's
committed frame (fails without the batched reset), and pairing coverage
for the drawer Escape pass-through: a plain Escape still closes the
floating drawer when the panel was never fullscreen.

* fix(web-shell): pull escaped focus back into the docked fullscreen surface (#8614)

* build(external-context): restore the node-only types override from main

Cherry-picks the tsconfig guard from 3037744602 (#8693), which postdates
this branch's merge base. With @types/jsdom installed, the inherited
vitest/globals types drag the DOM lib into this program and break the
for-await over response.body in http-client.ts, failing npm run build
and npm run typecheck.

* fix(web-shell): keep docked fullscreen usable in shadow DOM (#8614)

Resolve two Critical review findings on the docked fullscreen surface:

- focusin is composed, so in shadow-DOM portal mode the document-level
  pull handler and the Tab-wrap handler saw the shadow host retargeted
  as the target and snapped every focus change back onto the surface.
  Resolve the real node via composedPath() and read the active element
  from the surface's own root.
- The aria-hidden sweep recorded values still owned by a live
  hideOthers lock when the floating drawer unmounted in the same commit
  the docked surface mounted; the deferred restore then re-applied the
  lock's value and permanently hid the shell from assistive tech.
  Record null for lock-owned siblings and let the lock's unlock restore
  the original value.

Adds regression tests for both paths; both fail without their fix.

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 12:41:48 +00:00
carffuca
f3ba99f545
fix(sdk): hide ACP usage updates from transcripts (#8790)
* test(sdk): reproduce visible usage updates

* fix(sdk): hide ACP usage updates from transcripts
2026-08-09 09:57:32 +00:00
Shaojin Wen
3e731cda8b
fix(test): deflake three CI-load-sensitive tests (#8797)
* fix(core): deflake auto-memory extract tests under CI load

waitForMockCall polled ten zero-delay event-loop turns and gave up. The
mock it waits for fires after real async work (index rebuilds, cursor
I/O), so on a loaded CI runner the poll spun through its ten turns
without waiting any wall-clock time and the two rebuild-isolation tests
failed with 'Expected mock to be called' (seen on PR #8773's Test
(ubuntu-latest) job). Wait against a 2s deadline instead; the fast path
still returns on the first check.

* fix(cli): give the manifest-context fixture teardown a real timeout

The afterAll deletes several 16k-entry fixture trees — tens of thousands
of unlinks — and blew past vitest's default 10s hook timeout on a loaded
CI runner (PR #8773's second Test (ubuntu-latest) run), failing a suite
whose 59 tests had all passed. Same CI-load flake class as the extract
test fix in this branch.

* fix(core): let the shell-registry retention tests pay for their sidecar I/O

Each register/complete in the retention-cap loop tests also writes the
status sidecar via atomicWriteFileSync, and loaded CI runners have been
measured at ~700ms per sidecar write — the ~70 writes of the longest
loop take ~50s, past vitest's 15s default, failing four tests whose
assertions are pure eviction semantics (seen twice on PR #8773, on two
different runners). Give them an explicit 120s timeout; no assertion
changes.
2026-08-09 08:58:50 +00:00
易良
bf84caf173
feat: add Local Control pairing to CLI and Desktop (#8727)
Some checks failed
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
npm cache producer / Save npm cache (push) Has been cancelled
* feat(cli): add Local Control pairing

* fix(cli): address Local Control review feedback

* fix(cli): allow Local Control loopback origin

* feat(desktop): add Local Control pairing

* fix(local-control): bound unauthenticated connections

* test(desktop): allow Windows proxy cleanup

* test(desktop): avoid socket cleanup timing

* fix(desktop): surface Local Control status

* fix(desktop): simplify Local Control window

* fix(desktop): harden Local Control pairing

* fix(desktop): bind Mac wake lock to app
2026-08-09 08:31:54 +00:00
易良
d8c15b3456
fix(tests): await rig setup in Qoder plugin install integration test (#8793) 2026-08-09 08:31:39 +00:00
易良
ab61d81097
fix(desktop): enable microphone access on macOS (#8715)
* fix(desktop): enable microphone access on macOS

* fix(desktop): narrow helper entitlements

* test(desktop): pin macOS permission packaging

* test(desktop): strengthen entitlement release guards
2026-08-09 08:30:39 +00:00
Shaojin Wen
10621b3a93
fix(external-context): read the response body with a reader, not for-await (#8764)
* fix(external-context): read the response body with a reader, not for-await

Async-iterating a ReadableStream needs [Symbol.asyncIterator] on the
TYPE, and whether it is there depends on which lib set the program
resolves — @types/node's stream has it, the DOM lib's needs
lib.dom.asynciterable. That resolution flipped underneath this file on
2026-08-08: #8693 installed @types/jsdom at the root, vitest's types
pull the jsdom types in wherever they exist, and jsdom's carry
/// <reference lib="dom" />. #8693 shipped the tsconfig `types` guard in
the same commit, so main stayed green — but the guard travels with the
BRANCH while node_modules travel with the TRUSTED BASE in the autofix
verification build, so every managed branch behind #8693 failed that
build with TS2504 on this line. Two legs measured on run 31276008548:
63 minutes of accepted agent work discarded per round, 18 more minutes
burned by a repair step that cannot fix a failure outside the PR's diff
(#8614 reached attempt 13 that way; #8616 died identically).

Reproduced locally in both directions before changing anything:
@types/jsdom installed + guard removed = the gate's exact error,
character for character; with the reader loop the same poisoned setup
builds clean. The guard stays — belt and suspenders — but the build no
longer depends on it, or on which lib set any future environment
resolves.

Behavior is unchanged and now pinned by tests the file never had:
multi-chunk assembly, the exact MAX_RESPONSE_BYTES boundary (bound is
strictly-greater), invalid-UTF-8 rejection, and the easy one to drop in
this rewrite — cancelling the stream on early exit, which `for await`
did implicitly via iterator return(). Mutation-tested: removing the
cancel fails exactly that test against an endless producer.

The package's other for-awaits iterate process.stdin (a Node stream,
async-iterable in every lib set) and are untouched.

* fix(external-context): await stream cancellation before rejecting the request

On early exit from the reader loop (the oversize throw) cancellation was
started fire-and-forget, so postJson() rejected while the stream's
teardown was still settling — `for await` had awaited its implicit
iterator return() before propagating. An immediate retry could overlap
the previous response transport's unfinished cancellation. Await
reader.cancel() before releaseLock(), and pin the sequencing with a
deferred-cancel regression test that fails against the fire-and-forget
form.

Also cover read() rejecting after a partial chunk was received: the
error maps to the request-did-not-complete transport error rather than
EOF-then-parse of the partial JSON, and the reader lock is still
released.

* fix(external-context): drop the types guard the reader rewrite made obsolete

The `"types": ["node"]` override existed solely to keep @types/jsdom's
lib.dom out of this program while http-client.ts read the response body
with `for await` — the DOM lib's ReadableStream is not async-iterable,
and the flip broke the build with TS2504 (#8693). The reader loop that
replaced the `for await` types identically in every lib set, so the
guard is no longer load-bearing: with it removed, lib.dom re-enters the
program and the package still builds cleanly. Drop it with its stale
comment instead of leaving maintainers two contradicting stories about
whether it is needed.

Also export MAX_RESPONSE_BYTES and import it in the boundary tests
instead of re-declaring it locally, so the tests pin the real constant
rather than a copy that can silently drift.

* test(external-context): make the invalid-UTF-8 test pin fatal decoding

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 07:42:04 +00:00
Shaojin Wen
5e83efb555
fix(serve): stop usage_update frames from flooding the demo event log (#8762)
* fix(serve): stop usage_update frames from flooding the demo event log

The demo page's SSE fallthrough logs every unrecognized session update
as raw JSON. usage_update is emitted once per main-session model round,
so a long agentic turn (/review runs hundreds of rounds) turned the
Events tab into a scroll of identical usage_update lines.

Render the frame as what it is instead: an in-place context meter in
the session panel (used / size, percentage), reset on session create or
attach, with an Events entry only when the integer percentage moves —
the transitions stay on record without the flood.

* test(cli): cover demo usage_update dedup logging (#8762)

* test(cli): pin usage_update size type guard and meter reset (#8762)

* fix(cli): align demo context meter with CTX log format (#8762)

The meter label used toLocaleString() while the CTX log entry used plain
concatenation, so the same usage_update frame rendered differently once
token counts passed 1000. Build one label and reuse it in both places,
and pin the rounding, downward-transition, >1000 formatting, and the
script-scoped dedup declaration in the jsdom tests.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 07:41:50 +00:00
jinye
60458f5e37
fix(serve): Coordinate caller-supplied session IDs (#8415)
* fix(serve): coordinate caller-supplied session IDs

Complete daemon-wide admission across REST, ACP, workspace generations, SDKs, and MCP.

Closes #8411

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(serve): wire session bridges in hot-reload harness

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): address review round for caller-supplied session IDs (#8415)

Restore the observability and fail-loud guarantees flagged in review:
log every session-id admission routing failure, name the live foreign
owner workspace in restore conflicts, make the ACP dispatcher's
admission dependency required so load/resume cannot run on a mount
without one, and require mountAcpHttp hosts to inject the daemon-wide
admission instead of silently building a weak fallback. Harden the SDK
WS transport against environments without global fetch and against
non-capabilities 200 envelopes, and align the design doc with the
implemented restore-sharing and persistence-failure semantics.

* fix(sdk): harden session ID capability fallback

Preserve REST capability errors, fail closed on malformed envelopes, retain restore routing diagnostics, and align retry documentation.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): normalize restored session IDs

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(session): preserve mixed-case legacy session access

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 07:31:30 +00:00
Shaojin Wen
afcc937ec5
fix(ci): render the queued-acknowledgement comment (#8726)
* fix(ci): render the queued-acknowledgement comment

The ack comment posted on every PR that requests a review was built as

  <!-- qwen-review-ack -->_Qwen Code review request accepted. …[workflow
  run](URL)._

with the prose glued straight onto the marker. A line opening with `<!--`
starts an HTML block, and that block runs to the line containing the
closing delimiter INCLUSIVE — the rest of that line is still inside it and
never parsed as Markdown. So the comment shipped as raw source: no
emphasis, and the link to the workflow run was dead text.

That link is the only pointer a PR author gets to their review run —
`issue_comment` runs are not attached to a commit, so they never appear in
the PR's checks list. Losing it leaves no way to reach the run from the PR.

Measured through GitHub's own renderer (POST /markdown, mode=gfm) on the
exact bodies:

  marker + text          -> 0 <a>, 0 <em>
  marker + \n   + text   -> 1 <a>, 1 <em>
  marker + \n\n + text   -> 1 <a>, 1 <em>

Use the blank-line form, matching how autofix-status already builds its
body. The marker text is unchanged, so the `contains(...)` upsert lookup
still finds prior acks and updates them in place.

Pinned by a test that scans every marker in the workflow and rejects one
with prose glued to it, skipping comment lines. It fails against main,
naming the offending line.

* fix(ci): harden the marker guard per review round 2

Pin the workflow-run URL weaving into the ack printf, bound the marker
scan to the marker's physical line, anchor the newline exemption to the
literal the marker opens, widen it to double-quoted printf formats, and
flag unquoted command-substitution concatenation. Declare the remaining
coverage gaps in the test instead of papering over them.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ci): widen marker-guard regex and declare known gaps per review

* fix(ci): pin ack link shape and dedupe workflow scan per review

---------

Co-authored-by: verify <verify@local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-09 06:04:26 +00:00