Commit graph

8378 commits

Author SHA1 Message Date
신동석
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
Shaojin Wen
4a79517815
feat(cli): surface the posted review link from /review submit (#8770)
The Create Review response's html_url was parsed for the receipt id and
then dropped, so the terminal summary had no deterministic link to the
review just posted — in the Web Shell there is no scrollback to recover
it from. submit now relays html_url on the Posted stderr line and as
`url` in the stdout JSON (best-effort, like the receipt), and SKILL.md
requires the final summary to carry a `Posted: <url>` line before the
fixed `Review complete:` line.
2026-08-09 01:28:56 +00:00
易良
306bfa582a
fix(cli): require enabled attribution markers (#8712) 2026-08-09 01:15:27 +00:00
Nothing Chan
73e9eab626
fix(cli): prefer wl-copy on Wayland (#8481)
Some checks are pending
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 / ubuntu-latest / Java 17 (push) Waiting to run
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
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
* fix(cli): prefer wl-copy on Wayland

* fix(cli): address Wayland clipboard review feedback

* fix(cli): address Wayland clipboard review feedback (#8317)

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
2026-08-08 23:30:33 +00:00
qwen-code-dev-bot
ad7f905d38
fix(cli): improve WebSearch no-model notice with a copy-paste config example (#8665)
* fix(cli): improve WebSearch no-model notice with a copy-paste config example

The startup notice when webSearch is enabled but no model is set only
said "Set tools.webSearch.model to a model declared under
modelProviders" without showing what that looks like. Users had to
guess the modelProviders shape.

Expand the notice to include a minimal working settings.json example
(tools.webSearch + a DashScope modelProviders entry) and the env-var
alternative, so users can copy-paste to get web_search working.

* fix(core): complete WebSearch env recipe in no-model notice (#8665)

* fix(core): single-source WebSearch notice endpoint and pin recipe (#8665)

* test(core): pin notice example baseUrl in WebSearch gate test (#8665)

* fix(core): state WEB_SEARCH_BASE_URL value in WebSearch env recipe (#8665)

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

* fix(core): split WebSearch env recipe line in no-model notice (#8665)

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

---------

Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-08 23:27:01 +00:00
callmeYe
39377fcff3
feat(daemon): add batch skill toggle API (#8664)
* feat(daemon): add batch skill toggle API

* test(serve): update capability integration baseline

* fix(daemon): apply skill batches atomically

* test(daemon): pin Skill batch toggle contracts and fix docs examples

* test(daemon): pin Skill batch toggle mutants flagged in review

* test(daemon): cover Skill batch toggle edge cases

* docs(daemon): clarify Skill batch toggle contract notes from review

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

* test(daemon): pin Skill batch toggle cap semantics and SDK surface shape

* test(daemon): pin Skill batch toggle mutants flagged in round-5 review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.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-08 23:22:04 +00:00
Nothing Chan
1774771317
fix(telemetry): ignore unsupported OTel exporter selectors (#8703)
* fix(telemetry): ignore unsupported OTel exporter selectors (#8697)

* test(telemetry): guard exporter env scrub and its finally restore (#8697)

The env-scrub assertions ran inside the mocked start(), where
initializeTelemetry's init-failure catch swallowed them — the test
passed even with the fix reverted. Record observations during start()
and assert after init resolves, add a throw-path test proving the
finally block restores the caller's environment, and note in sdk-impl
that spanProcessors/logRecordProcessors must stay unconditional arrays
because the sdk-node logs env fallback runs in the NodeSDK constructor,
outside the scrub window around start().

---------

Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
2026-08-08 23:12:43 +00:00
易良
1a5d1c445e
fix(core): confirm read-only git commands when repo config executes programs (#8575) (#8645)
* fix(core): confirm read-only git commands when repo config executes programs (#8575)

Whitelisted read-only git sub-commands (status, diff, log, show, ...) are
auto-approved based purely on command text, but git can execute programs
configured in the repository-local config while running them: diff.external,
core.fsmonitor, core.pager / pager overrides, diff driver textconv,
core.askpass, credential.helper, core.sshCommand, remote proxies,
ext:: remote URLs, gpg.program. A planted .git/config could turn an
auto-approved command into arbitrary code execution.

Add a synchronous repo-local config probe (bounded stat walk + small file
reads, fail-closed) shared by the AST and regex classifiers: when a git
command would classify as read-only and the repo-local config reachable
from the execution cwd contains program-executing keys, the verdict is
downgraded so the command requires confirmation. Global/system config is
deliberately out of scope (the user's own setup, not a cloned-repo
attack surface). All permission entry points (shell tool, monitor tool,
permission manager, memory-scoped agent policy) now pass the execution
cwd to the classifier. Classifier APIs only gain an optional parameter;
behavior without cwd is unchanged.

* fix(core): close two probe gaps from review of #8575

- Speculation gate now receives the execution cwd: speculated shell
  calls bypass the permission flow, so evaluateToolCall passes cwd
  (and the shell directory arg, which takes precedence) into
  classifyShellCommandSafety. A speculated `git diff` in a repo with
  diff.external planted now hits the boundary instead of executing.
- Probe reads `.git/config.worktree` of the main checkout too — with
  extensions.worktreeConfig enabled git reads it for the main
  worktree, so a key planted there no longer bypasses the probe.
- plan-mode shell policy passes its effective cwd to the classifier
  for consistent classification (no execution hole there; consistency).
- Document bare repos as out of scope.
- Add end-to-end integration test driving the real probe + classifier
  through ShellToolInvocation.getDefaultPermission (no fs mocking).

* fix(core): honor Git worktree config semantics

* fix(core): fail closed on opaque git config constructs (#8575)

Round-3 hardening of the config probe, closing bypasses found in local
security review (all empirically reachable via attacker-written
.git/config):

- Section headers the minimal parser cannot interpret (e.g. `]` inside a
  quoted subsection) now fail closed instead of silently dropping the
  entries beneath them.
- Inline `[section] key = value` lines are parsed instead of discarded.
- Unparseable `.git` pointer files fail closed like unreadable ones.
- include/includeIf entries are flagged rather than resolved: their
  targets can live outside `.git` (e.g. tracked working-tree files).
- core.gitProxy added to the program-valued keys (git:// transport via
  whitelisted `git remote show`).
- Document the cd-into-another-repo limitation in the module doc.
- Add the missing PermissionManager cwd-threading contract test
  (dirty repo config → ask, clean → allow) and regression tests for
  each behavior above.

* fix(core): track cd in git config probe; close filter/url bypasses (#8575)

Round-4 hardening from the local security/correctness review — each item
was empirically demonstrated against the prior head:

- Compound commands now track cd/pushd/popd: statically resolvable
  targets move the probe's base directory (same-repo `cd subdir` stays
  read-only), unresolvable targets (`cd`, `cd -`, `cd $VAR`, `popd`,
  quoted/expanded targets) downgrade later git segments. Closes the
  `cd <dirty-repo> && git status` bypass in both the AST and regex
  classifiers, including tree-sitter's nested-list chains.
- filter.<name>.clean/smudge/process flagged: `git diff` runs worktree
  content through the configured clean filter with no extra flags.
- url.<base>.insteadOf rewrite targets starting with ext:: flagged
  (combined with protocol.ext.allow in the same file this executes on
  whitelisted `git remote show`).
- Config reads are size-capped at 1 MiB and fail closed above it (DoS
  guard for the synchronous permission path).
- Boolean pager overrides (pager.<cmd> = true/false) no longer flagged.
- Added the missing wiring contract tests: PermissionManager
  config.getCwd() fallback, memory-scoped agent shell policy, plan-mode
  shell policy (including the directory-param override).

* fix(core): provide getTargetDir in speculation test mocks (#8575)

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

* fix(core): harden git-config exec probe against cd-tracking bypasses (#8575)

Address review round 1 findings on the repository-local git config
execution probe:

- Track cd/pushd across every sibling-statement sequence (program, brace
  group, subshell body), not only `&&` lists; propagate the directory out
  of brace groups and redirected/negated wrappers.
- Respect list-operator semantics: cd state no longer leaks across `||`
  or `&`, and non-`&&` sequential statements keep the prior directory in
  the safety equation.
- Resolve cd targets strictly: skip flag arguments (`-P`/`-L`/`-e`/`--`),
  reject flag-only and multi-operand forms, accept only statically
  unquotable word/string/raw_string targets (no concatenation, ANSI-C
  quoting, backslash escapes, or expansions), and fail closed when the
  target is missing or not a directory.
- Probe git discovery more faithfully: resolve symlinks (realpath), treat
  a directory that is itself a git directory (bare repos, submodule
  storage) as a repo, fail closed when the search-depth budget exhausts,
  decode config values and subsections the way git does (quoted-segment
  concatenation, escapes), and add diff.<driver>.command and
  core.alternateRefsCommand to the program-valued keys.
- Scope fixes: fall back to the scoped execution root when the memory
  agent shell probe has no cwd; resolve compound-command defaults against
  the full command so a segment rule cannot override the cd-aware verdict;
  keep sub-commands after a directory change in the confirmation scope for
  both the shell and monitor tools.
- Tests: regression coverage for every fix plus mutation-checked wiring
  tests; skip the chmod-based EACCES simulation on Windows/root; use a
  relative submodule gitdir pointer; parametrize filter clean/smudge/process.

* fix(core): close round-2 review findings for git-config exec probe (#8645)

* fix(core): flag deprecated dot-form git config sections in exec probe (#8645)

* fix(core): probe core.hooksPath targets in git-config exec probe (#8645)

core.hooksPath was listed in PROGRAM_VALUED_KEYS, so any repo with the
key set — every husky/lefthook install, and the worktrees Qwen itself
creates — downgraded all whitelisted read-only git commands to ask.
The key names no program, it only redirects hook lookup, so resolve it
the way git does (~ expansion, relative anchored at the worktree root)
and probe the target directory for executable read-only-triggered hooks
exactly like the default hooks directory.

* fix(core): probe submodule storage configs in git-config exec probe (#8645)

* fix(core): tighten git config safety checks

* fix(core): address verification findings for git-config exec probe (#8645)

* refactor(core): reset git config probe to issue scope

* fix(core): use Git config semantics for read-only probes

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-08 23:12:14 +00:00
易良
1cbf2e8fc7
feat(ci): auto-assign issues to area owners from labels (#8668)
* feat(ci): auto-assign issues to area owners from labels

Route labelled issues to a maintainer with push access, without putting
issue text in the path of a write token.

Assignment is a pure function of the issue's labels and a checked-in
label -> owner map, evaluated by a standalone workflow on issues:labeled.
No model runs in the assignment path and the script never reads issue
title, body, or comments, so untrusted issue text cannot select an
assignee. Owners come from CODEOWNERS (asserted by test) and every
candidate is re-checked against the collaborator permission API before
the write, so editing the map cannot grant access. Among eligible owners
the least loaded wins, rotating by issue number to break ties.

* fix(ci): decouple issue owner map from CODEOWNERS

CODEOWNERS answers who owns a code path, which is narrower than who may
be assigned an issue in an area: the repository has ~44 collaborators
with push access against 7 CODEOWNERS entries, so the membership test
would have rejected legitimate additions such as admins and maintainers
who own no path.

Drop that assertion and document the actual process for adding owners.
The live collaborator permission check remains the boundary. Add the
validation the map does need: duplicate owners would skew load balancing,
and duplicate area names would silently shadow each other under
first-match-wins.

* fix(ci): keep issue ownership triggers disjoint

* fix(ci): recheck issue owner assignment before write

* test(ci): cover issue owner label recheck

* docs(ci): Correct CODEOWNERS count in issue assignment rationale

* fix(automation): preserve autofix issue ownership

* feat(ci): widen core issue owner pool to active repository maintainers

* feat(ci): add four more collaborators to core issue owner pool

* fix(ci): tighten issue-owner map validation and sync trigger docs

* fix(ci): tighten issue-owner assignment tests and login validation (#8668)

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
2026-08-08 23:01:03 +00:00
callmeYe
33b124321c
feat(core): support Qoder plugin extensions (#8661)
* feat(core): support Qoder plugin extensions

* fix(core): address Qoder extension review feedback

* fix(core): handle annotated tags and unsafe parse errors

* fix(core): harden Qoder conversion edge cases

* fix(core): sanitize Qoder conversion inputs

* fix(core): address Qoder extension round-3 review feedback

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

* fix(core): honor explicit marketplace selection over Qoder manifest

* fix(core): preserve nested plugin update provenance

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-08 22:35:54 +00:00
qwen-code-ci-bot
4d6246bd88
chore(release): v0.21.8 (#8757)
* chore(release): v0.21.8

* docs(changelog): sync for v0.21.8

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-08 19:25:22 +00:00
jinye
3037744602
fix(integration-tests): make the project typecheckable and fix what that found (#8693)
* fix(integration-tests): make the project typecheckable and fix what that found

`tsc -p integration-tests/tsconfig.json` could not run at all. The config
carried a `"//"` documentation key inside `compilerOptions.paths`, and every
value there must be an array, so tsc aborted with TS5063 before checking a
single file. Nothing in CI runs it either, so the directory has been
unchecked for its whole life -- which is how PR #8620 shipped an
`integration-tests/cli/qwen-serve-streaming.test.ts` that referenced an
undeclared `REPO_ROOT`, swallowed the ReferenceError in a bare catch, and
reported a green skip for a security regression test.

Moving that note out of `paths` exposed 404 errors. Three more config
defects accounted for 353 of them:

- `composite: true` is inherited from the root config for the packages that
  are actually referenced. Composite requires every file in the program to
  appear in `include`, and these tests import package sources by relative
  path, so it produced 324 TS6307. Nothing references this project and it
  emits nothing, so it is now `composite: false`.
- The root `lib` is ES2023 only. The suite drives browser-side code in
  `terminal-capture/` and pulls SDK sources that name `WebSocket` and
  `HeadersInit`, so 21 identifiers resolved to nothing. Now DOM +
  DOM.Iterable + ES2023, matching packages/cli.
- Workspace packages resolved through `packages/core/dist` via a project
  reference, so with core unbuilt the checker reported a dozen members as
  missing from `Storage` that are right there in the source. They now
  resolve from source through `paths`, mirroring packages/cli, and the
  reference is gone.

node-pty declares `types` at the top level but its `exports` map is a bare
string with no `types` condition, so nodenext never reached the
declarations and every pty handle degraded to `any` -- which is what
silently untyped the `data` and `exitCode` callbacks in test-helper.ts. It
now resolves through `paths` as well. `@types/jsdom` is added for the one
file that uses it; DefinitelyTyped has no release matching jsdom 26 (it
jumps 21 -> 27), so this pins the current 28.x.

Two real defects fell out of the remaining 51:

- write_file.test.ts built a detailed tool-call failure message and passed
  it to `toBeTruthy()`, which takes no arguments. It was discarded on every
  failure, leaving only a bare literal.
- Two terminal-capture scenarios set `gif: true` inside `streaming`, where
  the runner never reads it. It is a scenario-level switch.

The rest was making an existing `undefined` visible. `readToolLogs()`
promised `name: string` for fields copied straight out of telemetry
attributes that nothing validates; the stdout fallback can promise them,
the telemetry branch cannot, and claiming otherwise just moved the
`undefined` past the type checker into the assertions.

This is type resolution only. `integration-tests/vitest.config.ts` keeps
its own hardcoded aliases onto the built SDK bundle, so the suite still
exercises the published-bundle shape at runtime.

Not wired into CI here, but not for cost reasons: a cold run of
`tsc -p integration-tests/tsconfig.json` takes about 106s on an idle
developer box. The program is 2679 files, of which 103 are integration
tests and roughly 1100 are package sources their own projects already
check, so there is duplicated work available to reclaim by resolving the
packages from their built declarations -- but at ~106s it is already cheap
enough to gate on as-is.

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

* fix(integration-tests): isolate jsdom types and complete source-resolution paths

Address review round 1:

- external-context: override `types` to ["node"]. The root @types/jsdom
  entered its program through vitest's optional jsdom types and injected
  lib dom, flipping @types/node's fetch globals to DOM variants whose
  ReadableStream is not async-iterable (TS2504 in http-client.ts), which
  failed every CI job during the npm ci prepare build.
- integration-tests tsconfig: explicit nodenext paths entries for every
  workspace subpath the program imports (sdk/daemon, 19 acp-bridge
  subpaths, core goalWire/memoryScopes/userPromptSubmitContext, webui
  daemon-react-sdk, channel-base); drop the dead `*` wildcards; include
  **/*.tsx. Typechecks green with the source packages' dists removed.
- Relax noPropertyAccessFromIndexSignature in integration-tests and
  revert the six bracket-access rewrites it forced in SDK sources.
- channel-plugin: import channels/base from src and map
  @qwen-code/channel-base to source so both declarations agree.
- qwen-serve-streaming: asAccepted delegates to the SDK's exported
  isNonBlockingAccepted type predicate instead of a drifted copy.
- sleep-interception: tighten blocked predicates to success === false
  and fix the comment describing them.
- Declare jsdom at the root next to @types/jsdom.

* fix(integration-tests): complete source-resolution paths and restore single channel-base instance

Address review round 2:

- Map the eight builtin channel adapters and web-templates to source.
  channel-registry.ts and html.ts still resolved them through their
  exports maps to dist, so the typecheck's build-independence was
  incomplete: on a tree without built dists it failed with the exact
  9 x TS2307 the maintainer verification measured.
- channel-plugin.test.ts: import @qwen-code/channel-base by bare
  specifier instead of a relative src path. At runtime the test and
  plugin-example now resolve the same dist/index.js through the
  exports map, restoring the single ChannelBase / SessionRouter
  instance the relative src import silently split; type resolution
  still maps to source through paths, and vitest.config.ts keeps
  pointing e2e runs at the built bundles.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-08-08 16:32:31 +00:00
Shaojin Wen
bb8f2c0129
fix(cli): scrub inherited loader env vars from daemon session subprocesses (#8663)
Some checks are pending
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
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
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 / 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 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
* fix(cli): scrub inherited loader env vars from daemon session subprocesses

Daemon-mode sessions bound to one workspace inherited loader-affecting
env vars (NODE_OPTIONS with dev-harness --import hooks, NODE_PATH,
preload-class vars) from whatever shell launched the daemon, so
subprocesses in another workspace resolved modules through the
launching checkout's tree (fixes #8653).

Scrub the loader subset of RELOAD_EXCLUDED_KEYS from process.env at the
two process boundaries that host sessions: the daemon after freezing its
boot env (the frozen copy keeps loader vars so dev-mode ACP children can
still boot), and the ACP child after the relaunch/sandbox handoff (the
respawned child re-scrubs itself).

Fixes #8653

* fix(cli): reject loader env keys in initial .env load and log scrubs

A trusted workspace's .env could re-populate the loader-key slots that
scrubInheritedLoaderEnv() emptied in the daemon process, because
canApplyParsedEnvKey applied RELOAD_EXCLUDED_KEYS only on reloads.
Reject the loader subset on every .env application path so one
workspace's loader hook cannot reach other workspaces' session
subprocesses through the shared daemon env.

Also make the scrub return the removed keys and emit a stderr
breadcrumb naming them at both boundaries, so a session subprocess
missing an inherited var can be traced back to the scrub.

* fix(cli): reject loader env keys in serve fast path before env freeze

* fix(cli): deny npm_config_node_options and report rejected loader keys

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

* fix(cli): match loader env keys case-insensitively and report settings.env rejections

* fix(cli): canonicalize loader env key spellings and scrub channel daemon workers

npm maps non-leading underscores in npm_config_* keys onto hyphens, so
npm_config_node-options injected NODE_OPTIONS exactly like
npm_config_node_options while slipping past every loader gate and scrub.
Canonicalize case and underscore/hyphen spelling on both sides of the
loader-key membership test, covering .env loads, settings.env
application, the serve fast path, and the inherited scrubs.

Channel daemon workers are spawned with the daemon's pre-scrub base env
but are not ACP children, so they never ran the self-scrub; mirror the
ACP-child scrub at the worker entry so nothing a worker spawns inherits
loader vars into another workspace.

Scope the settings.env rejection warning per workspace so a
multi-workspace daemon reports every workspace's rejection instead of
deduping them all under one label, revert the unread
loadServeFastPathEnvironment return value to void, and pin the
buildRuntimeEnvironment settings.env gate and the consume-once stash
reset with discriminating tests.

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

* fix(cli): address review findings for loader env denylist

- report rejected loader keys through the daemon log after boot (per-workspace .env loads were silent once boot stderr was gone)

- accumulate serve fast-path rejected keys across loads instead of overwriting, dedupe, and use normalized env file paths for rejection sources

- restore scrubbed inherited loader env vars on embedded runQwenServe close()

- add regression tests for ENV scope coverage, reporter dedupe, fast-path accumulation, and post-boot daemon-log diagnostics

- scope docs: top-level env rejection does not apply to mcpServers[].env / hooks[].env; document serve loader-scrub behavior

* fix(tests): add writeStderrLineSafe to stdioHelpers mocks and sync env guard allowlist

* fix(cli): scrub loader vars from the daemon base env and retighten the denylist

The frozen daemonRuntimeBaseEnv was captured before the launch-env scrub,
so daemon-spawned session processes still booted under the inherited
loader — the child-side post-boot scrub ran after Node had already
consumed NODE_OPTIONS. The base env is now scrubbed before the freeze
(except under the DEV=true harness, whose .ts entries need the tsx
loader), and close() restores the host's launch env from a pristine
snapshot.

Denylist scope now follows the injection-vs-search-path split: adds the
npm config-file redirect keys, ZDOTDIR, and a BASH_FUNC_* prefix rule;
moves ENV/LD_LIBRARY_PATH/DYLD_LIBRARY_PATH back to their reload-only
tier (mainstream toolchain compatibility); blocks QWEN_CLI_ENTRY and
NODE_EXTRA_CA_CERTS from project .env files. The ACP-child scrub is
gated on the daemon stamp (QWEN_CODE_SERVE) so direct editor ACP
integrations keep the user's exported environment, and the daemon's
per-workspace .env rejections are now reported from
buildRuntimeEnvironment.

* fix(cli): block DEV spoofing, case-insensitive env exclusions, serve boot env restore (#8663)

Address round-6 review: DEV joins the hardcoded project-env exclusions so a
workspace file cannot disable the daemon's loader-env scrub; the hardcoded
tier is enforced case-insensitively (Windows env lookup is case-insensitive)
via isHardcodedProjectEnvExclusion at every application gate; runQwenServe's
catch restores the scrubbed launch env and detaches the rejection reporter
when startup fails after the scrub. Tests gain the matching regressions,
home-env hermeticity, source-scoped warning filters, and tmpdir cleanup; the
unreachable reload delete-pass loader guard and its vacuous test are removed.

* fix(cli): match the reload-excluded env tier case-insensitively too

Round-6 follow-up: R6-3 named RELOAD_EXCLUDED_KEYS.has() among the gates a
case variant slips, but the hardcoded-tier fix left the reload-only keys
(QWEN_SERVER_TOKEN, PATH, HOME, TMPDIR, …) on exact-case matching. On
Windows a lowercase twin names the same OS variable, so a mid-session
settings.env/.env edit could still rotate the daemon token or move PATH
through a case respelling. Fold the reload tier the same way and pin it
with a reload-behavior regression test. Also note DEV in the settings.md
exclusion docs.

* test(cli): redirect HOME in environment.test.ts for full home-env hermeticity

The source-scoped warning filters fixed the warning-count assertions, but
the process.env assertions (e.g. 'never applies entrypoint or trust-anchor
keys') still read state a real home .env can pollute: home scope
deliberately bypasses the hardcoded exclusions, so a dev machine with
QWEN_CLI_ENTRY in ~/.env applies it and fails the test while CI stays
green. Redirect HOME/USERPROFILE to an empty temp dir in beforeEach —
verified by running the suite with HOME pointed at a poisoned home.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.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-08 16:11:39 +00:00
Shaojin Wen
9381827c88
fix(ci): enter Critical-only mode after five change rounds (#8751)
The suggestion cutoff sat at ten change-producing rounds, but the strict
round cap discards a plain PR at round ten before that threshold can
engage — so in practice it only ever bound takeover PRs, which spent ten
rounds growing their diff on lower-severity feedback before the brake
applied. Lower it to five so the loop stops implementing suggestions
while the diff is still reviewable.

The autofix skill already documented the five-round boundary; the
workflow now matches it.
2026-08-08 16:09:32 +00:00
Shaojin Wen
6ddb0307ac
perf(review): bake a soft tool-call budget into finder and auditor briefs (#8708)
* perf(review): bake a soft tool-call budget into finder and auditor briefs

A fan-out wave's wall clock is its slowest agent, and the slowest agent is
reliably a wanderer: two measured runs of the same 14-agent wave took 11.7
and 41 minutes on comparable diffs, the difference being individual agents
spending 40-100 model calls exploring the tree while healthy agents settle
at 25-45 with indistinguishable findings.

plan.budget gains agentToolBudget - clamp(30 + effective/20, 30, 60),
computed and recorded like every other budget arm so no caller can inflate
it - and agent-prompt bakes it into every finder, chunk, invariant,
persona and reverse-audit brief. The verifier is exempt (verifyShard
governs its load, and its per-finding re-trace must not stop early), as is
Build & Test (deterministic commands).

The ceiling is soft and worded against the two failure modes a budget
invites: at the budget an agent stops EXPLORING, never reporting -
findings in hand are filed, open checks are disclosed as "did not get to"
into the same receipt machinery that judges whiffs - and the recall rule
is restated beside it so it cannot read as a reporting cap. A plan without
the field (older CLI) gets no ceiling: the fallback errs toward coverage,
matching the documented pre-budget behaviour.

* perf(review): declare budget on agent-prompt's local PlanReport

The CI build's tsc caught what vitest's transpile-only path did not: the
prompt builder reads plan fields through its own unknown-typed local
PlanReport interface, which had no budget member. Declared in the local
house style (unknown-typed) with explicit numeric narrowing in
toolBudgetBlock.

* perf(review): scope the tool budget per launch and close the disclosure loop

Round-1 review rework. The ceiling is now per LAUNCH, not per plan: a
chunk or invariant agent's allowance derives from its own territory
(launchToolBudget, same constants as reviewBudget), and every launch's
mandatory reads ride on top of the allowance instead of inside it, so a
whole-diff role on a huge diff is never exhausted by its assigned chunk
reads. Agent 8's whole-diff block now carries the budget too, and Agent 0
joins the exemptions — its mandatory work is issue-sized, not diff-sized.

The disclosure side becomes deterministic: briefs mandate the fixed
`Budget gap: <the check>` line format, coverageFromTranscripts parses
those lines into the report's budgetGaps (a NOTE, never a gate failure —
punishing disclosure teaches agents not to disclose), and SKILL.md's
Step 3D says how each gap is ruled: an incomplete required trace joins
unreviewedDimensions, optional depth goes to "Not reviewed".

Tests: launchToolBudget scaling/rides-on-top/garbled inputs, the runtime
shape of reviewBudget's return, per-launch numbers for chunk, invariant,
whole-diff and Agent 8 briefs, a full-roster exemption sweep, garbled
agentToolBudget values falling back to no ceiling, and budgetGaps
parsing with ok staying true.

* perf(review): make the tool budget plan-authoritative and give disclosures teeth

Round-2 review rework, three threads of it.

The number: launchToolBudget now takes the plan's recorded value and
clamps it into the budget's own band in both directions, and a scoped
launch's territory-derived allowance can only lower it, never raise it —
the plan is again the one number every launch answers to, under version
skew included. Reads estimates count the launch's whole reading list
(brief, diff pages, the reverse auditor's findings file, an invariant
agent's file paging scaled by its added lines), garbled chunk entries
degrade to the scoped floor instead of NaN or whole-diff headroom, an
UNCOVERABLE chunk gets no budget block beside its exact-receipt
instruction, and the exemptions are declared on the briefs
(budgetExempt) rather than hardcoded names — with the roster test
walking BRIEFS so a new role must declare.

The disclosure: one parser (budgetGapDisclosures in lib/budget.ts)
shared by every reader — markdown-tolerant, placeholder-dropping,
control-char-stripping, length- and count-capped. coverage collects
gaps inside the guarded record walk (idle/blind/superseded agents'
copied templates no longer count), narrows a disclosing agent's chunk
credit to the lines the harness saw it read (told-list credit was
making budget stops invisible to the gate), and retirement refuses to
count a gap-bearing return as a dry audit — an agent's admission that
it stopped can no longer retire the chunk that still owes the work.
compose-review renders every parsed gap into the body's "Not reviewed"
section mechanically — on the Approve too — as a disclosure channel
that never caps; the capping ruling stays with the orchestrator per
Step 3D.

Also: check-coverage's missing-chunks NOTE no longer tells operators to
paste --whole-diff ahead of role briefs (that double-budgets; the block
is Agent 8's alone), and its budget-gap NOTE puts the directives before
the agent-authored text.

* perf(review): unpunish disclosure, converge under it, and bound its blast radius

Round-3 review rework — 27 findings, two of them reversals of round-2
decisions, owned as such.

Reversal one: a disclosing agent's coverage credit is NOT narrowed.
rangeOf records only reads carrying a positive limit, so the narrowing
zeroed exactly the compliant offset-paged reader while an agent that
stopped WITHOUT disclosing kept full credit — an asymmetry that only
ever bites the discloser teaches agents not to disclose. The told
presumption is the same for every agent; a gap changes the Step 3D
ruling, never the arithmetic. This also restores the Uncoverable-claim
evaluation order the narrowing had broken.

Reversal two: a gap-bearing return is no longer blanket-unknown to the
reverse-audit retirement. The receipt is judged with its Budget gap
lines STRIPPED: a return whose only substance was its disclosures still
never retires, but a receipt substantive without them does — the
blanket rule made convergence impossible (a reverse auditor's ceiling
is routinely met) and ran every budgeted loop to the round cap.

The parser is now line-based: no cross-line prefix class (the multiline
regex measured seconds on ordinary pathological returns), same-line
capture only (a bare header no longer swallows the next line), fenced
code and blockquotes are quotation not use, non-answers are dropped in
any punctuation (`None.`), duplicates fold, the sanitizer covers C1 /
U+2028/29 / bidi overrides, wrappers strip only in pairs, truncation
cuts on code points. Gap supersession is gap-aware: only a GAP-FREE
relaunch silences a record's disclosures.

The body channel is bounded and inert: each gap rides through mdField
(no @-mentions, #refs, links or stray </details> from agent prose), the
sentence caps at five attributed items with an "and N more" tail, a gap
the orchestrator promoted into unreviewedDimensions is dropped here so
the body never says it twice, and a disclosed gap denies the
"no blockers" certification.

Reads estimates: invariant paging from fileLines (the plan DOES record
post-change length; added-lines was a 6x undercount on volume-heavy
files), the chunkless 3A reverse auditor counts its findings-list pages
(keyed on acceptsFindings), and chunk territory is source-weighted like
the plan allowance it mirrors — a lockfile chunk no longer out-earns a
source chunk. Discriminating fixtures pin all three past their floors.

* perf(review): close the disclosure format's language and boundary holes

Four follow-on review findings. The whole-diff reading list now counts
each chunk's PAGES, not a flat one per chunk — an oversized chunk's
isTruncated paging was being paid out of the analysis allowance, and
the fixture itself encoded the contradiction (40k chars = 2 reads in
the chunk test, 1 in the whole-diff test). The disclosure matcher
accepts the zh forms (预算缺口/不足/用尽, with either colon) — the
receipt regex next door accepts zh receipts by design, so a
zh-narrating auditor's budget stop was invisible to every consumer.
The retirement clause is cut at an INLINE disclosure marker before its
substance is judged — a one-line return put the disclosure after the
receipt separator where the line-based strip cannot see it, and the
clause capture absorbed the gap text as its own substance. And
launchToolBudget caps the TOTAL at 200: the reads term comes from the
same unchecked-cast plan as the allowance, and a garbled chars of 1e9
was rendering a forty-thousand-call brief around the clamp.

---------

Co-authored-by: verify <verify@local>
2026-08-08 15:20:23 +00:00
Shaojin Wen
d91c66119b
fix(ci): match /review commands followed by a newline (#8723)
A comment of `@qwen-code /review` plus a newline and a body has never
triggered anything. The shape match tried to accept it with

  startsWith(body, format('@qwen-code /review{0}', '\n'))

but GitHub expression string literals are NOT escape-processed: that
'\n' is a literal backslash + n, so the branch matched nothing. The
command was silently ignored — no run, no feedback, in a path whose
whole job is to be the manual escape hatch.

Measured on a live runner rather than assumed:

  startsWith(<LF body>,   format(…, '\n'))              => false
  startsWith(<LF body>,   format(…, fromJSON('"\n"')))  => true
  startsWith(<CRLF body>, format(…, fromJSON('"\n"')))  => false
  startsWith(<CRLF body>, format(…, fromJSON('"\r"')))  => true

fromJSON parses JSON, which IS escape-processed, so it yields a real
newline. Both endings are needed: the REST API sends LF, the web UI
sends CRLF, and an LF pattern does not match a CRLF body. Applied to all
7 shape matches (6 /review, 1 /resolve).

The shell half had the matching gap: the command line is taken as
everything before the first LF, which on CRLF keeps a trailing CR. IFS
has no CR, so word splitting produced tokens like `--timeout=300<CR>`
that failed the numeric check with no visible cause. Strip it.

Three tests pin this: no shape match may use a non-escape-processed
literal, every shape match must carry both endings, and the CR strip
must follow the first-line split. All three fail against the workflow on
main. Mutation-tested: dropping the CR branches or the CR strip each
fails exactly its own test.

Found while re-triggering the PRs stranded by the #8648 outage — 19
multi-line trigger comments were accepted by `authorize`, then silently
dropped by this branch.

Co-authored-by: verify <verify@local>
2026-08-08 12:22:39 +00:00
DDDonut
9e87453497
fix(security): honor explicit distrust over inherited trust (#8628)
* fix: let explicit distrust override inherited trust

* fix(trust): apply most-specific folder rule

* fix(cli): satisfy trust precedence lint

* test(cli): align TrustDialog inherited-trust assertion with the new wording

This PR changed the isInheritedTrustFromParent note in TrustDialog.tsx
but the test still asserted the old copy, failing Test (ubuntu-latest,
Node 22.x). Assert the stable fragment instead of the full sentence so
Ink line wrapping cannot break toContain.

Suggested-by: @qqqys in https://github.com/QwenLM/qwen-code/pull/8628#pullrequestreview-4888282916

---------

Co-authored-by: daleselaji-dev <daleselaji-dev@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-08 11:26:24 +00:00
qqqys
d58474f651
feat(core): checkpoint long-running Goal evidence (#8465)
* feat(core): checkpoint long-running Goal evidence

* fix(core): reject blocked Goals on evidence exhaustion

* test(core): pin Goal checkpoint recovery contracts

* fix: retain Goal rejection feedback and dedupe checkpoint records (#8465)

Address the review findings on the Goal evidence checkpoint change:

- Keep the verifier rejection feedback when a follow-up checkpoint
  fails, so the resumed continuation still learns why the proposal
  was rejected.
- State the cumulative claim byte budget in the checkpoint verifier
  prompt so conforming output is steered inside the materialize gate.
- Suppress checkpoint-bookkeeping goal_state records in ACP transcript
  replay and CLI resume rendering, matching the live TUI display path.
- Pin the reviewed behaviors with mutant-killing tests and drop five
  unneeded runtime-options casts.

* fix: make Goal replay suppression and checkpoint recovery cause-aware (#8465)

* fix: persist goalCause in transcript replay state across page handoffs (#8465)

The cause-aware bookkeeping suppression only worked within one replay
machine: the persisted replay state dropped goalCause, so the first
goal_state record after a page boundary re-emitted the duplicate
bookkeeping card the check exists to suppress. Persist goalCause in
TranscriptReplayStateV1 (snapshot, parse, constructor) and thread it
through the page plumbing and the backward-pagination seed.

Also align parseClaim with materializeGoalEvidenceCheckpoint on the
shared claim limit: trim before measuring and count code points, so a
max-length claim with trailing padding no longer sinks the checkpoint
side query.

* fix(core): harden Goal evidence checkpoint gates and wire parsing (#8465)

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

* fix: settle post-commit Goal checkpoint failures and tighten evidence gates (#8465)

* fix(core): resolve goal checkpoint review findings (#8465)

* fix(core): align goal checkpoint byte budget with enforcement (#8465)

Count only claim text against the checkpoint byte cap so the budget the verifier prompt advertises matches what materialization enforces, share the proof-kind guard with the persisted-record parser, and restore reference_not_catalogued coverage.

* test(core): pin unknown-source checkpoint rejection message (#8465)

* fix(core): bound goal checkpoint windows and soften verifier failures (#8465)

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.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-08 10:14:46 +00:00
Shaojin Wen
ee98f7420b
perf(ci): make the autofix fleet caps operator-tunable and raise them (#8731)
The three caps that bound the autofix review loop were compiled-in
literals sized for a much smaller takeover pool, so growing the pool
meant editing the workflow, opening a PR and waiting for review every
time. The takeover pool is not static — it grew to 37 PRs by
2026-08-08, and it keeps growing.

Back all three with repository variables, keeping the literals as
fallbacks, so the loop is resized in Settings → Variables with no code
change:

  QWEN_AUTOFIX_MAX_PARALLEL              (fallback 20, was 5)
  QWEN_AUTOFIX_MAX_TARGETS_PER_SCAN      (fallback 30, was 10)
  QWEN_AUTOFIX_MAX_CANDIDATE_INSPECTIONS (fallback 60, unchanged)

Verified on a live runner that `max-parallel` accepts the expression and
schedules by it — a 6-leg matrix resolving to 3 started exactly 3 legs
and began the 4th only after a slot freed. Not assumed: an invalid
expression here makes the whole file invalid, which this repository just
paid 12.9 hours of dead review automation for.

The raised fallbacks are sized against measurements, not guesses. At 5
slots the fleet served ~14% of the takeover pool at once, reproducing at
a larger scale the 81-minute tail measured back at 3. The ecs-qwen fleet
is 84 runners, so 20 concurrent legs take under a quarter of it, and the
legs sampled that day finished in 3-28 minutes. Worst case rises to 100
runner-hours across the fleet (20 slots x the 300-minute job cap), and
per-PR head-write concurrency groups are per-PR, so this adds no push
contention.

MAX_TARGETS_PER_SCAN has to stay above max-parallel or the scan cannot
emit enough legs to fill the matrix. That relation is pinned for the
fallbacks by an existing test and stated at both definitions for the
variables, where it becomes an operator invariant.

Mutation-tested, 4 of 4 caught: fallback equal to the budget, fallback
above it, and dropping either variable back to a literal.

Co-authored-by: verify <verify@local>
2026-08-08 08:53:07 +00:00
jinye
59b750fc4d
feat(serve): Expose active work state (#8588)
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
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
* feat(serve): expose active work state

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

* refactor(serve): rebuild active-work reporting on channel-wide snapshots

Reworks the active-work signal after review. Three changes of substance.

Drops the 45s heartbeat watchdog entirely. It inferred "this channel is
dead" from "one Session stopped reporting" and killed the whole channel,
taking every Session on that process with it — including on a suspend,
a long event-loop stall, or a single dropped notification. Channel
liveness is a transport concern and gets its own mechanism.

Replaces the per-Session boolean with a channel-wide snapshot of named
holds, derived on every report from the owners of the work (the
registry's unfinalized set, the notification queue) rather than from a
ledger kept alongside them. Full snapshots make a dropped report
self-correcting in both directions, and a Session's absence from one is
positive evidence the child released it. Agent holds now use
hasUnfinalizedTasks()'s predicate, closing the cancel to
finalizeCancelled() window where a cancelled agent looked idle and its
terminal notification could be stranded.

Leaves prompts out of the child's report: the daemon accepts, queues,
dispatches, and settles them, so its own count is authoritative and
covers the FIFO wait the child cannot see. A snapshot is flushed ahead
of the prompt response so a hold the prompt left behind is on the wire
before the daemon drops that count.

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

* feat(serve): confirm idle before closing, and grade the health signal

Completes the active-work rework with the two facts a restart controller
was still missing and the one guarantee automatic cleanup was missing.

Automatic cleanup no longer destroys a Session on the strength of a
cached snapshot. It asks the child to close only if unheld, and the
child answers under its own close gate — with the gate held no prompt is
admitted and no automatic turn starts, so a hold cannot appear between
the check and the teardown. A refusal hands back the current holds and
the daemon adopts them. An unanswered request is neither retried nor
assumed: the Session stays, and the next snapshot settles it, because a
Session absent from one has provably been released. Every automatic path
— detach, attach rollback, prompt settle, notification settle, a child
reporting itself idle — now funnels through one decision point instead
of four near-copies.

Health gains activeWorkReporting and activeWorkStaleMs. Without them
activeWork:false cannot be told apart from "no child told me anything",
which is the one case where acting on it is unsafe. Freshness is graded
by the daemon rather than the controller, since the cadence is negotiated
per channel; a stale snapshot or a child omitting a category degrades the
grade instead of silently narrowing what the boolean covers.

Tests: acp-bridge 489/489, acpAgent 383/383, Session 534/534,
serve suites 1188 with one pre-existing cross-file flake in the Live
Appshot integration tests (reproduces on the unmodified tree, failing a
different test each run).

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

* fix(serve): contain snapshot-collection failures, and repair two Session mocks

CI caught two things the local runs missed.

The reporter's snapshot construction was unguarded. Only the send was
wrapped, so a throw while collecting a Session's holds escaped through
setInterval and queueMicrotask as an uncaught exception — capable of
taking down the ACP child — and through flush() into the prompt path,
turning a reporting problem into a failed prompt. Collection is now
wrapped and a failed snapshot is abandoned whole rather than sent
partially: a Session missing from a report reads as released, and one
reported with no holds reads as safe to close, so publishing a partial
snapshot would actively invite the daemon to destroy live work. Sending
nothing lets the daemon's copy age instead, which its freshness grading
already treats as untrustworthy and retains. flush() no longer rejects.

Session.review-lease and Session.worktree mock the background-task
registry without setStatusChangeCallback, so constructing a Session threw.
That break arrived with the original commit, which verified only
Session.test.ts; the sibling Session.*.test.ts files were never run. Both
mocks now carry the methods the constructor and the hold collector need.

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

* test(serve): prove the reporter contains collection and transport failures

The previous commit added the guard but could not have demonstrated it:
the same commit also gave the acpAgent Session mock a
collectActiveWorkHolds, removing the very condition that triggered the
throw. The unhandled error disappearing was therefore explained by the
mock alone, and active-work-reporter.ts had no tests at all.

These cover the escape routes that matter — the interval timer, the
coalescing microtask, and flush() on the prompt path — plus the choice to
abandon a whole snapshot rather than send a partial one, since a session
omitted from a report reads as released and one reported with no holds
reads as safe to close.

Verified by removing the guard: five of the nine fail with the collection
error escaping, and pass again once it is restored.

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

* fix(serve): make every automatic teardown ask before destroying

Self-review of the previous revision found that this PR had promoted a
cached child report from a hint into the authority that permits destroying
a Session. Four teardown paths consulted it, their guards disagreed with
each other, and each was weaker than what main had. The four are one
defect with four exits, so they are fixed as one change.

Absence from a snapshot no longer authorizes teardown. Because reports
are complete, a Session the child omits holds nothing on the child side —
so absence and reported-with-no-holds are the same fact and now take the
same path. The separate absence loop is gone; it lacked the subscriber
and client guards `maybeCloseIdleSession` applies, so one snapshot could
destroy a Session with a live SSE subscriber and a registered client.
That contradicted this PR's own claim that an unreported Session is
retained, and the old test asserted the destruction. Both are corrected.

A conditional close is now marked in flight across the whole confirm-then-
teardown span, and attach, prompt, and rewind refuse a Session in that
state exactly as they refuse one already closing. `closeSessionImpl` sets
`closing` synchronously, but the round trip in front of it is an await of
up to ten seconds; on main the guard sequence ran straight into teardown,
so splitting it is what opened the window.

A snapshot older than the freshness window stops counting as evidence.
Staleness was already computed, but only to grade health, never to gate
destruction — so a child that went quiet after one empty report left a
cache that permitted reaping indefinitely. Never-reported and gone-quiet
now land in the same retained bucket. Reclaiming a channel that has truly
stopped answering belongs to transport liveness, not here.

The idle reaper asks the child too. Its TTL says the client stopped
caring, which is not the same as the child having nothing left to run.

Health coverage is exposed as counts and graded once daemon-wide, because
grades do not compose: a runtime with zero Sessions is vacuously `full`,
and folding that in let an empty workspace vouch for another workspace's
unreported Sessions. `activeWorkStaleMs` now measures only covered
Sessions, so it can no longer report positive staleness beside a grade
saying nothing is covered.

Also: bound snapshot `sessions[]` and `holds[]` so a buggy child cannot
make the daemon walk an unbounded structure per report, and retract the
background-task status callback by identity rather than blanking a
single-slot setter the TUI also uses.

Tests: the absence test now asserts retention under a registered client
and under a live subscriber; new regressions cover the recovered lost
close response, the stale-snapshot gate, admission refusal during a
conditional close, the reaper's confirmation, the oversized-snapshot
discard, and the mixed empty/uncovered health aggregate.

* fix(serve): make unknown a reason to ask, not a reason to skip

Triage review found that the design doc, the PR description, and the
comment on `entryHasActiveWork` all promised the daemon *asks* the child
about a Session it has not heard about, while no code path ever did:
`entryHasActiveWork` returns true when the child's side is unknown, and
the cleanup path returned early on exactly that. The finding predates the
guard rework and survived it unchanged.

Skipping on unknown looks like the safe direction and is in fact the worse
failure. Nothing resolves it — a Session on a channel that went quiet is
retained forever, and the idle reaper skips it too, so there is no path
out at all. Asking resolves it definitively: the child answers under its
own close gate whether or not its snapshots are arriving, the round trip
is bounded, and every non-answer still retains.

So the predicate is split by what it actually knows. `childReportsHeldWork`
is positive knowledge only; `childWorkIsUnknown` is the absence of a
gradeable report. The health surface ORs both, because a controller must
never read "nobody told me" as "nothing is running". Automatic cleanup
blocks only on known work and lets unknown through to `confirmChildUnheld`.

Also moves `parseActiveWorkSnapshot` out from between two import blocks
(pure relocation, no logic change) and aligns the doc wording, including
the shared-guard table, with what the code now does.

* fix(serve): close three teardown races the confirm window opened

Review found three ways the conditional close can still destroy a live
Session. All three share a cause: the round trip turned a synchronous
guard-then-teardown into an awaited span, and three things that were
previously impossible to observe mid-teardown now are.

**A restore in flight looks exactly like an abandoned Session.**
`session/load` registers the entry before awaiting `artifacts.restore()`
and `seedSessionUpdates()`, and registers its first client only after —
so for that whole window there are no clients, no subscribers, nothing
held, and the child answers the conditional close truthfully. The
snapshot trigger this PR added fires inside it. Excluded in
`entryIsAutoCloseCandidate` rather than at the snapshot trigger, so the
reaper's TTL elapsing inside a slow restore is covered too.
`pendingRestoreIds` already existed but was read only by
`hasNoChannelWork`, never by the close funnel.

**Teardown re-resolved the target by id without re-checking identity.**
`closeSessionImpl` does a fresh `byId.get`, and the id can be
re-registered to a different entry during the round trip: an explicit
kill removes this one (kill ignores the in-flight flag by design, keeping
its force semantics) and a `session/load` for the same persisted id
registers a fresh one. The stale continuation then tore down the newly
restored Session under its just-attached client. One identity re-check
after the await.

**The restore path was not upgraded to the new admission predicate.**
`sendPrompt`, `rewindSession`, and single-scope attach check
`isClosingOrAuthorizingClose`; `restoreSession` still checked bare
`closing` at both its guards, so a client could attach inside the window
and lose the session under it. That directly contradicted the
`closeIfChildUnheld` comment claiming every admission path checks the
flag. Its `racedEntry` branch had no closing guard at all — a narrower
pre-existing hole, same defect, same predicate.

Regression test covers the restore-path admission refusal. The other two
need a mid-restore snapshot and a kill-then-reload interleave that the
mocked-channel harness cannot stage honestly; both are pinned by reading
the code paths, which is weaker and worth saying.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 06:34:20 +00:00
Shaojin Wen
6cae50c7ea
fix(ci): keep the review workflow under the expression-length limit (#8720)
The review workflow has been invalid since #8648 merged, so every event
it declares has been dead for ~12 hours:

  Invalid workflow file: .github/workflows/qwen-code-pr-review.yml#L1
  (Line: 751, Col: 14): Exceeded max expression length 21000

A `run:` body containing `${{ }}` is evaluated as ONE expression
template, and GitHub caps a single expression at 21000 characters.
"Run review" went 17705 -> 22282 chars in #8648 (17:00:32 on 2026-08-07);
the first startup failure is stamped 17:00:50. #8683 took it to 24042.

An over-limit expression does not fail a job — it invalidates the whole
file, so no run is created at all. Across the 400 runs since that merge
there is not one success, not one `pull_request_target` and not one
`issue_comment`: both automatic review and `@qwen-code /review` were
unreachable, while CI stayed green throughout because nothing covered it.

Pass the three context values the script reads through the step's env,
leaving the body free of `${{ }}`. The runner then never templates it and
its length stops mattering. No behaviour changes: each substitution is a
rename of the same value.

Pin it with a test that walks every workflow and fails any templated run
block over the limit, plus one that keeps this body untemplated — it is
past 21000 on its own, so a single `${{ }}` added back takes the whole
workflow down again. Both fail against the file currently on main,
naming it: `qwen-code-pr-review.yml > review-pr > Run review: 24042 chars`.

Mutation-tested, 4 of 4 caught: restoring a `${{ }}` in the body (3
tests), dropping either env binding, and hardcoding the value the env
used to carry.

Co-authored-by: verify <verify@local>
2026-08-08 05:53:35 +00:00
Nothing Chan
761272542d
fix(core): use Singapore Token Plan DeepSeek model id (#8650) (#8705)
Co-authored-by: nothing <nothing@U-DQY4PXFJ-0222.local>
2026-08-08 05:48:03 +00:00